Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ASGI Support #532

Merged
merged 101 commits into from
Dec 4, 2019
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
5eaf1a1
Trash
dmanchon Jun 13, 2019
853f476
requirements
dmanchon Jun 13, 2019
d023abb
Tests + changes to factory
masipcat Jun 14, 2019
3bd33e9
hyper
dmanchon Jun 14, 2019
ea8c6dd
Clean a bit
dmanchon Jun 14, 2019
4acf812
make hypercorn work
masipcat Jun 14, 2019
f59e459
Using custom Request instead of aiohttp.Request
masipcat Jun 14, 2019
b11dd0d
POST fixed. Molotov test
dmanchon Jun 14, 2019
032f68c
Fix testclient fixture
masipcat Jun 14, 2019
ab8a126
Small changes
masipcat Jun 15, 2019
898142b
websockets with asgi draft
dmanchon Jun 15, 2019
f010e3a
websocket close handling
dmanchon Jun 15, 2019
e2c38c6
Move requests implementations to a module
dmanchon Jun 15, 2019
1827366
fix test
vangheem Jun 15, 2019
55a66d8
Fixed GuillotinaRequest.query_string
masipcat Jun 15, 2019
62fff01
Checkpoint. Almost all tests are green
masipcat Jun 15, 2019
093e94f
Fix ws tests
dmanchon Jun 15, 2019
d8d6a13
Implement IRequest record method on asgi
dmanchon Jun 15, 2019
27e4b2a
Deleted aiohttp request
masipcat Jun 15, 2019
db9b798
Fix debug headers handler
dmanchon Jun 16, 2019
5ae7797
Merge branch 'master' into asgi-support
masipcat Jun 16, 2019
89fafb3
Merge branch 'asgi-support' of github.com:plone/guillotina into asgi-…
masipcat Jun 16, 2019
05d10ef
Fix deleted ws import
masipcat Jun 16, 2019
309dce8
Fixed AsgiStreamReader bug
masipcat Jun 16, 2019
2fc67b2
More fixes
masipcat Jun 16, 2019
9c944af
Small fix
dmanchon Jun 16, 2019
df15f81
Simulate incoming request in chunks for uploads
dmanchon Jun 16, 2019
826dd32
Remove starlette dep
dmanchon Jun 16, 2019
29dd329
flake8
dmanchon Jun 16, 2019
d72d373
Almost there
masipcat Jun 16, 2019
1c4d5c7
All tests green
masipcat Jun 16, 2019
69adf04
Now should be all green
masipcat Jun 16, 2019
7952fb0
Trying to fix failing tests in travis
masipcat Jun 16, 2019
7c10a56
I hope now is fixed...
masipcat Jun 16, 2019
76963a7
Small fix
masipcat Jun 16, 2019
5269254
Revert "Trying to fix failing tests in travis"
masipcat Jun 16, 2019
49ab931
Fix tests when runnign with DB_SCHEMA != public
masipcat Jun 16, 2019
c62bd3a
update to last asgi test client
dmanchon Jun 17, 2019
2a0146e
Updated async-asgi-testclient
masipcat Jun 17, 2019
223f083
Merge branch 'master' into asgi-support
masipcat Jun 17, 2019
9eec6ec
Small changes
masipcat Jun 17, 2019
eee7568
Fix merge
masipcat Jun 17, 2019
70a2a0f
Merge branch 'master' into asgi-support
masipcat Jun 18, 2019
ed3e7eb
Configured pg v10 in pytest-docker-fixtures + small change in fixtures
masipcat Jun 18, 2019
99e012b
Merge branch 'master' into asgi-support
masipcat Jun 18, 2019
f19412a
Merge branch 'master' into asgi-support
masipcat Jun 19, 2019
1c6dca7
Clean up unused methods
masipcat Jun 19, 2019
841333d
Merge branch 'master' into asgi-support
masipcat Jun 19, 2019
8ecb6b8
Rearrenged code and reduced code that depends on aiohttp
masipcat Jun 19, 2019
3c7eb26
Merge branch 'master' into asgi-support
masipcat Jun 19, 2019
efe666b
Updated changelog
masipcat Jun 19, 2019
4f0714e
Fix pg catalog tests
masipcat Jun 21, 2019
c8fe97c
Remove aiohttp dependecy for websockets
dmanchon Jun 22, 2019
dcba06c
Remove aiohttp dependecy for websockets
dmanchon Jun 22, 2019
7aafe6f
Remove aiohttp dependecy for request
dmanchon Jun 22, 2019
30b58fa
Flake8
dmanchon Jun 22, 2019
8fe4e10
Replaced 'loop' fixture from 'aiohttp' for 'event_loop' from 'pytest-…
masipcat Jun 22, 2019
ca8be11
Merge branch 'master' into asgi-support
masipcat Jun 22, 2019
2086715
Fix cockroach fixture
masipcat Jun 22, 2019
f6b49ba
Reduced aiohttp dependence. TODO: traversal/router and CORS
masipcat Aug 29, 2019
147c233
BOOM! Merge branch 'master' into asgi-support
masipcat Aug 29, 2019
fbb0b76
Lot of fixes
masipcat Aug 29, 2019
b375401
Fixed flake8 and mypy
masipcat Aug 29, 2019
133a30e
black
masipcat Aug 29, 2019
f7f6669
Added some tests and cleaned unused code
masipcat Aug 30, 2019
22c6a8a
Mypy
masipcat Aug 30, 2019
00eaacd
Asgi support: no aiohttp (#654)
vangheem Aug 31, 2019
699176e
Merge branch 'master' into asgi-support
masipcat Aug 31, 2019
168c560
Merge branch 'master' into asgi-support
masipcat Sep 3, 2019
80f25f1
isort
masipcat Sep 3, 2019
f73bef1
Merge branch 'master' into asgi-support
masipcat Sep 3, 2019
98f4c11
Merge branch 'master' into asgi-support
masipcat Sep 20, 2019
fbc08df
Merge branch 'master' into asgi-support
masipcat Sep 20, 2019
f1211c8
Documented how to use differents ASGI servers + small changes
masipcat Sep 22, 2019
272f331
Small fixes
masipcat Sep 22, 2019
307a31d
Merge branch 'master' into asgi-support
masipcat Sep 24, 2019
095125f
mypy-flake8
masipcat Sep 24, 2019
6726ce1
Merge branch 'asgi-support' of github.com:plone/guillotina into asgi-…
masipcat Sep 24, 2019
3203b0d
Changes and fixes
masipcat Sep 24, 2019
e5d6a90
Removed yarl
masipcat Sep 24, 2019
0265be1
Support for middlewares
masipcat Oct 2, 2019
e0955ef
Merge branch 'master' into asgi-support
masipcat Oct 2, 2019
0333e34
Black
masipcat Oct 2, 2019
61d7d20
Merge branch 'master' into asgi-support
masipcat Oct 18, 2019
49230f1
Updated Cython for python3.8 (required by uvloop)
masipcat Oct 18, 2019
515327a
fix uvloop
masipcat Oct 18, 2019
ed069ad
requested changes
masipcat Oct 18, 2019
12deec6
Merge branch 'master' into asgi-support
masipcat Oct 23, 2019
dc73ece
Removed python 3.8 in travis
masipcat Oct 23, 2019
f925322
Merge branch 'master' into asgi-support
masipcat Oct 25, 2019
b57cd01
Changed implementation of reify
masipcat Oct 25, 2019
88751c5
Merge branch 'master' into asgi-support
masipcat Nov 7, 2019
7ae3546
Merge branch 'master' into asgi-support
masipcat Nov 21, 2019
c94cd49
Fix some tests are skiped and mypy errors
masipcat Nov 21, 2019
6295819
Install extra 'testdata' in travis
masipcat Nov 21, 2019
a82df48
Merge branch 'master' into asgi-support
masipcat Nov 24, 2019
be427dd
Fixed tests
masipcat Nov 24, 2019
5547069
Merge branch 'master' into asgi-support
masipcat Nov 26, 2019
a7bedb5
Merge branch 'master' into asgi-support
masipcat Dec 4, 2019
827e228
Small fixes
masipcat Dec 4, 2019
52f4200
fix
masipcat Dec 4, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
CHANGELOG
=========

6.0.0 (unreleased)
------------------

- Replaced aiohttp with ASGI (running with uvicorn by default)
[dmanchon,masipcat]


5.0.0a2 (unreleased)
--------------------

Expand Down
7 changes: 3 additions & 4 deletions guillotina/api/files.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import mimetypes

from aiohttp.web import StreamResponse
from guillotina.response import StreamResponse
from guillotina import configure
from guillotina._settings import app_settings
from guillotina.api.content import DefaultOPTIONS
Expand Down Expand Up @@ -76,19 +76,18 @@ async def serve_file(self, fi):
filepath = str(fi.file_path.absolute())
filename = fi.file_path.name
with open(filepath, 'rb') as f:
resp = StreamResponse()
resp = StreamResponse(status=200)
resp.content_type, _ = mimetypes.guess_type(filename)

disposition = 'filename="{}"'.format(filename)
if 'text' not in resp.content_type:
if 'text' not in (resp.content_type or ""):
disposition = 'attachment; ' + disposition

resp.headers['CONTENT-DISPOSITION'] = disposition

data = f.read()
resp.content_length = len(data)
await resp.prepare(self.request)

await resp.write(data)
await resp.write_eof()
return resp
Expand Down
3 changes: 1 addition & 2 deletions guillotina/api/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import aiohttp
import ujson
from aiohttp import web
from guillotina import configure
from guillotina import logger
from guillotina import routes
Expand Down Expand Up @@ -187,7 +186,7 @@ async def handle_ws_request(self, ws, message):
async def __call__(self):
tm = get_tm()
await tm.abort()
ws = web.WebSocketResponse()
ws = self.request.get_ws()
await ws.prepare(self.request)

async for msg in ws:
Expand Down
115 changes: 115 additions & 0 deletions guillotina/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from guillotina import glogging
from guillotina import task_vars
from guillotina.exceptions import ConflictError
from guillotina.exceptions import TIDConflictError
from guillotina.request import AsgiStreamReader
from guillotina.request import Request


logger = glogging.getLogger('guillotina')


def headers_to_list(headers):
return [[k.encode(), v.encode()] for k, v in headers.items()]


class AsgiApp:
def __init__(self, config_file, settings, loop):
self.app = None
self.config_file = config_file
self.settings = settings
self.loop = loop
self.on_cleanup = []
self.route = None

async def __call__(self, scope, receive, send):
if scope["type"] == "http" or scope["type"] == "websocket":
return await self.handler(scope, receive, send)

elif scope["type"] == "lifespan":
while True:
message = await receive()
if message['type'] == 'lifespan.startup':
await self.startup()
await send({'type': 'lifespan.startup.complete'})
elif message['type'] == 'lifespan.shutdown':
await self.shutdown()
await send({'type': 'lifespan.shutdown.complete'})
return

async def startup(self):
from guillotina.factory.app import startup_app
self.app = await startup_app(
config_file=self.config_file,
settings=self.settings,
loop=self.loop,
server_app=self
)
return self.app

async def shutdown(self):
for clean in self.on_cleanup:
await clean(self)

async def handler(self, scope, receive, send):
# Aiohttp compatible StreamReader
payload = AsgiStreamReader(receive)

if scope["type"] == "websocket":
scope["method"] = "GET"

request = Request(
scope["scheme"],
scope["method"],
scope["path"],
scope["query_string"],
scope["headers"],
payload,
loop=self.loop,
send=send,
scope=scope,
receive=receive
)

task_vars.request.set(request)
resp = await self.request_handler(request)

# WS handling after view execution missing here!!!
if scope["type"] != "websocket":
from guillotina.response import StreamResponse

if not isinstance(resp, StreamResponse):
await send(
{
"type": "http.response.start",
"status": resp.status,
"headers": headers_to_list(resp.headers)
}
)
body = resp.text or ""
await send({"type": "http.response.body", "body": body.encode()})

async def request_handler(self, request, retries=0):
try:
route = await self.app.router.resolve(request)
return await route.handler(request)

except (ConflictError, TIDConflictError) as e:
if self.settings.get('conflict_retry_attempts', 3) > retries:
label = 'DB Conflict detected'
if isinstance(e, TIDConflictError):
label = 'TID Conflict Error detected'
tid = getattr(getattr(request, '_txn', None), '_tid', 'not issued')
logger.debug(
f'{label}, retrying request, tid: {tid}, retries: {retries + 1})',
exc_info=True)
request._retry_attempt = retries + 1
request.clear_futures()
return await self.request_handler(request, retries + 1)
else:
logger.error(
'Exhausted retry attempts for conflict error on tid: {}'.format(
getattr(getattr(request, '_txn', None), '_tid', 'not issued')
))
from guillotina.response import HTTPConflict
return HTTPConflict()
15 changes: 7 additions & 8 deletions guillotina/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,15 @@ def __run_with_monitor(self, app, settings):
self.__run(app, settings)

def __run(self, app, settings):
loop = asyncio.get_event_loop()
loop.run_until_complete(app.startup())
if asyncio.iscoroutinefunction(self.run):
# Blocking call which returns when finished
loop = asyncio.get_event_loop()
loop.run_until_complete(self.run(self.arguments, settings, app))
loop.run_until_complete(self.cleanup(app))
loop.close()
else:
self.run(self.arguments, settings, app)
self.run(self.arguments, settings, app.app)
loop.run_until_complete(self.cleanup(app))
loop.close()

if self.profiler is not None:
if self.arguments.profile_output:
Expand All @@ -202,8 +203,7 @@ def __run(self, app, settings):

async def cleanup(self, app):
try:
app.freeze()
await app.on_cleanup.send(app)
await app.shutdown()
except Exception:
logger.warning('Unhandled error cleanup tasks', exc_info=True)
for task in asyncio.Task.all_tasks():
Expand Down Expand Up @@ -239,8 +239,7 @@ def signal_handler(self, signal, frame):
def make_app(self, settings):
signal.signal(signal.SIGINT, self.signal_handler)
loop = self.get_loop()
return loop.run_until_complete(
make_app(settings=settings, loop=loop))
return make_app(settings=settings, loop=loop)

def get_parser(self):
parser = argparse.ArgumentParser(description=self.description)
Expand Down
34 changes: 3 additions & 31 deletions guillotina/commands/server.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
from aiohttp import web
import uvicorn
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we default with uvicorn but I guess we'll need docs/info on how to run guillotina with uvicorn/etc by the normal startup command for those uvicorn guillotina:app_module_thingy if possible.

Copy link
Contributor Author

@masipcat masipcat Sep 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have these options (options are written in order of preference (desc)):

  1. Do somthing like serve = ServerCommand.run in guillotina/__init__.py so any ASGI server can start guillotina with G_CONFIG=./config.json uvicorn guillotina:serve (we can't pass custom arguments to our app when calling with uvicorn or any other server). With this approach the properties host and port won't be configurable from the config.json.
  2. Hard-coding in the ServerCommand a list of ASGI servers, so we can launch the server with g -c config.json serve --uvicorn or g -c config.json serve --hypercorn --host 0.0.0.0 --port 8080 (the host and port could be read from the config as well). But this would required do something like:
if arg == "--uvicorn":
    import uvicorn
    uvicorn.run(app, host=...)
elif arg == "--hypercorn":
    ...
  1. Provide the functions get_settings() and make_app(settings=) and delegate to the user this logic. For example, by doing app = make_app(settings=get_settings(file="config.json")) in app.py, and then running the server with uvicorn app:app or by calling the server from code, like if __name__ == "__main__": uvicorn.run(app, **params) and running the file with python app.py

I'm not sure about the third option, could be tricky to implement it.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do a combination of 1 and 2?

#2 would include a default server to run with(say uvicorn) ootb but then you can also override it with maybe --asgi-server=module.path.to.runner.

#2 would then depend on asgi providing a generic way to hook into for this--I'm not sure there isn't one ootb.

I think I would at the very least like an ootb implementation where the serve command worked with a default supported/tested implementation.

Thoughts?

Copy link
Contributor Author

@masipcat masipcat Sep 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#2 would include a default server to run with(say uvicorn) ootb but then you can also override it with maybe --asgi-server=module.path.to.runner.

You mean something like --asgi-server=uvicorn.run, right? In that case, there isn't a standard way to call an asgi server from code.

  • uvicorn:
import uvicorn
uvicorn.run(app, host='0.0.0.0', port='0.0.0.0')
  • hypercorn:
# https://pgjones.gitlab.io/hypercorn/api_usage.html
from hypercorn.asyncio import serve
from hypercorn.config import Config

config = Config()
config.bind = ["localhost:8080"]  # As an example configuration setting
asyncio.run(serve(app, config))
  • daphne:
# Doesn't provide documentation on how to run the server from code

I think I would at the very least like an ootb implementation where the serve command worked with a default supported/tested implementation.

I think we can provide an ootb implementation that works with uvicorn and hypercorn, and the user can select which server wants to run with --asgi-server=hypercorn (default to uvicorn). Something like:

if arguments.asgi_server == "uvicorn":
    import uvicorn
    uvicorn.run(app, host=argument.host, port=argument.port)
elif arguments.asgi_server == "hypercorn":
    from hypercorn.asyncio import serve
    from hypercorn.config import Config
    config = Config()
    config.bind = [f"{argument.host}:{argument.port}"]
    asyncio.run(serve(app, config))
else:
    raise Exception(f"Server {arguments.asgi_server} not supported")

What do you think?

PS: I'm not sure I understood what would be "a combination of [options] 1 and 2"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'm good with --asgi-server.

Maybe we can make sure to have good docs on creating app object to run with any server manually as well so users can integrate with another server before we have support for it. (or if they just want to have a different startup implementation)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done a first implementation. Suggestions are welcome :-)

from guillotina.commands import Command

import asyncio
import sys

try:
from aiohttp.web_log import AccessLogger # type: ignore
except ImportError:
from aiohttp.helpers import AccessLogger # type: ignore

try:
import aiohttp_autoreload
HAS_AUTORELOAD = True
except ImportError:
HAS_AUTORELOAD = False


class ServerCommand(Command):
description = 'Guillotina server runner'
Expand All @@ -34,21 +20,7 @@ def get_parser(self):
return parser

def run(self, arguments, settings, app):
if arguments.reload:
if not HAS_AUTORELOAD:
sys.stderr.write(
'You must install aiohttp_autoreload for the --reload option to work.\n'
'Use `pip install aiohttp_autoreload` to install aiohttp_autoreload.\n'
)
return 1
aiohttp_autoreload.start()

port = arguments.port or settings.get('address', settings.get('port'))
host = arguments.host or settings.get('host', '0.0.0.0')
log_format = settings.get('access_log_format', AccessLogger.LOG_FORMAT)
try:
web.run_app(app, host=host, port=port,
access_log_format=log_format)
except asyncio.CancelledError:
# server shut down, we're good here.
pass

uvicorn.run(app, host=host, port=port, reload=arguments.reload)
1 change: 1 addition & 0 deletions guillotina/factory/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from guillotina.factory import serialize # noqa
from guillotina.factory.app import make_app # noqa
from guillotina.factory.app import startup_app # noqa
from guillotina.factory.app import configure_application # noqa
from guillotina.factory.app import load_application # noqa
from guillotina.factory.content import ApplicationRoot # noqa
Expand Down
68 changes: 16 additions & 52 deletions guillotina/factory/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
import logging.config
from copy import deepcopy

from aiohttp import web
from guillotina import configure
from guillotina import glogging
from guillotina import task_vars
from guillotina._settings import app_settings
from guillotina._settings import default_settings
from guillotina.behaviors import apply_concrete_behaviors
Expand All @@ -23,14 +21,10 @@
from guillotina.events import ApplicationConfiguredEvent
from guillotina.events import ApplicationInitializedEvent
from guillotina.events import DatabaseInitializedEvent
from guillotina.exceptions import ConflictError
from guillotina.exceptions import TIDConflictError
from guillotina.factory.content import ApplicationRoot
from guillotina.interfaces import IApplication
from guillotina.interfaces import IDatabase
from guillotina.interfaces import IDatabaseConfigurationFactory
from guillotina.request import Request
from guillotina.response import HTTPConflict
from guillotina.traversal import TraversalRouter
from guillotina.utils import lazy_apply
from guillotina.utils import list_or_dict_items
Expand Down Expand Up @@ -119,47 +113,6 @@ def load_application(module, root, settings):
app_configurator.load_application(module)


class GuillotinaAIOHTTPApplication(web.Application):
async def _handle(self, request, retries=0):
task_vars.request.set(request)
try:
return await super()._handle(request)
except (ConflictError, TIDConflictError) as e:
if app_settings.get('conflict_retry_attempts', 3) > retries:
label = 'DB Conflict detected'
if isinstance(e, TIDConflictError):
label = 'TID Conflict Error detected'
tid = getattr(getattr(request, '_txn', None), '_tid', 'not issued')
logger.debug(
f'{label}, retrying request, tid: {tid}, retries: {retries + 1})',
exc_info=True)
request._retry_attempt = retries + 1
request.clear_futures()
return await self._handle(request, retries + 1)
logger.error(
'Exhausted retry attempts for conflict error on tid: {}'.format(
getattr(getattr(request, '_txn', None), '_tid', 'not issued')
))
return HTTPConflict()

def _make_request(self, message, payload, protocol, writer, task,
_cls=Request):
return _cls(
message, payload, protocol, writer, task,
self._loop,
client_max_size=self._client_max_size)


def make_aiohttp_application():
middlewares = [resolve_dotted_name(m) for m in app_settings.get('middlewares', [])]
router_klass = app_settings.get('router', TraversalRouter)
router = resolve_dotted_name(router_klass)()
return GuillotinaAIOHTTPApplication(
router=router,
middlewares=middlewares,
**app_settings.get('aiohttp_settings', {}))


_dotted_name_settings = (
'auth_extractors',
'auth_token_validators',
Expand Down Expand Up @@ -196,7 +149,19 @@ def optimize_settings(settings):
settings[name] = resolve_dotted_name(new_val)


async def make_app(config_file=None, settings=None, loop=None, server_app=None):
def make_app(config_file=None, settings=None, loop=None):
from guillotina.asgi import AsgiApp

# middlewares = [resolve_dotted_name(m) for m in app_settings.get('middlewares', [])]
router_klass = app_settings.get('router', TraversalRouter)
router = resolve_dotted_name(router_klass)()

app = AsgiApp(config_file, settings, loop)
app.router = router
return app


async def startup_app(config_file=None, settings=None, loop=None, server_app=None):
'''
Make application from configuration

Expand All @@ -219,8 +184,8 @@ async def make_app(config_file=None, settings=None, loop=None, server_app=None):
loop = asyncio.get_event_loop()

if config_file is not None:
with open(config_file, 'r') as config:
settings = json.load(config)
from guillotina.commands import get_settings
settings = get_settings(config_file)
elif settings is None:
raise Exception('Neither configuration or settings')

Expand Down Expand Up @@ -277,8 +242,6 @@ async def make_app(config_file=None, settings=None, loop=None, server_app=None):
app_logger.error('Could not setup logging configuration', exc_info=True)

# Make and initialize aiohttp app
if server_app is None:
server_app = make_aiohttp_application()
root.app = server_app
server_app.root = root
server_app.config = config
Expand Down Expand Up @@ -369,6 +332,7 @@ async def close_utilities(app):
app_logger.info('Removing ' + key)
await root.del_async_utility(key)


async def close_dbs(app):
root = get_utility(IApplication, name='root')
for db in root:
Expand Down
Loading