From 13ed3069f2b243dd11f99cff237aae00cd824720 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 7 Jul 2018 19:58:00 -0700 Subject: [PATCH 1/4] Started trying to get --asgi mode working --- datasette/app.py | 6 ++++++ datasette/cli.py | 11 ++++++++++- datasette/views/base.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/datasette/app.py b/datasette/app.py index 052131d0..ae6c11a6 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -430,6 +430,12 @@ class Datasette: self.executor, sql_operation_in_thread ) + def asgi_app(self): + from starlette import Router, Path + return Router([ + Path('/', app=IndexView(self).asgi_app(), methods=['GET']), + ]) + def app(self): app = Sanic(__name__) default_templates = str(app_root / "datasette" / "templates") diff --git a/datasette/cli.py b/datasette/cli.py index 820367ac..07c8bc99 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -256,6 +256,9 @@ def package( "-h", "--host", default="127.0.0.1", help="host for server, defaults to 127.0.0.1" ) @click.option("-p", "--port", default=8001, help="port for server, defaults to 8001") +@click.option( + "--asgi", is_flag=True, help="Run in ASGI mode" +) @click.option( "--debug", is_flag=True, help="Enable debug mode - useful for development" ) @@ -316,6 +319,7 @@ def serve( files, host, port, + asgi, debug, reload, cors, @@ -372,4 +376,9 @@ def serve( ) # Force initial hashing/table counting ds.inspect() - ds.app().run(host=host, port=port, debug=debug) + if asgi: + from uvicorn.run import UvicornServer + app = ds.asgi_app() + UvicornServer().run(app, host=host, port=port) + else: + ds.app().run(host=host, port=port, debug=debug) diff --git a/datasette/views/base.py b/datasette/views/base.py index 45bc8183..6e6cb342 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -12,6 +12,8 @@ from sanic import response from sanic.exceptions import NotFound from sanic.views import HTTPMethodView +from starlette import asgi_application, Response + from datasette import __version__ from datasette.utils import ( CustomJSONEncoder, @@ -43,8 +45,45 @@ class DatasetteError(Exception): self.messagge_is_html = messagge_is_html +class RequestWrapper: + # Implements the subset of the Sanic request that my code uses + def __init__(self, starlette_request): + self._request = starlette_request + + @property + def path(self): + return self._request.url.path + + @property + def query_string(self): + return str(self._request.query_string) + + @property + def args(self): + # Key/list-of-values + # There's probably a better way to do this: + d = {} + for key, value in self._request.query_string: + d.setdefault(key, []).append(value) + return d + + @property + def raw_args(self): + # Flat key/first-value dictionary + return self._request.query_string._dict + + class RenderMixin(HTTPMethodView): + def asgi_app(self): + @asgi_application + async def app(request): + sanic_response = await self.get( + RequestWrapper(request), **request["kwargs"] + ) + return Response(sanic_response.body) + return app + def render(self, templates, **context): template = self.ds.jinja_env.select_template(templates) select_templates = [ From 563fab27cc1e8c94f4f9a66c62797ce8942b4905 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 26 Jul 2018 06:07:24 -0700 Subject: [PATCH 2/4] Mostly working index, database and table pages --- datasette/app.py | 44 +++++++++++++++++++++++++++++++++++++---- datasette/cli.py | 4 ++-- datasette/views/base.py | 6 +++--- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index ae6c11a6..0307828e 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -431,13 +431,45 @@ class Datasette: ) def asgi_app(self): - from starlette import Router, Path + self.configure_jinja() + from starlette.routing import Router, Path, PathPrefix + from starlette.staticfiles import StaticFile, StaticFiles return Router([ - Path('/', app=IndexView(self).asgi_app(), methods=['GET']), + Path( + '/(?P\.jsono?)?$', + app=IndexView(self).asgi_app(), + methods=['GET'] + ), + Path( + '/favicon.ico', + app=StaticFile( + path=str(app_root / "datasette" / "static" / "favicon.ico") + ) + ), + PathPrefix( + '/-/static/', + app=StaticFiles( + directory=str(app_root / "datasette" / "static") + ) + ), + Path( + "/(?P[^/]+?)(?P(\.jsono?|\.csv))?$", + app=DatabaseView(self).asgi_app(), + ), + Path( + "/(?P[^/]+?)/(?P[^/]+?)$", + app=TableView(self).asgi_app(), + ), ]) + # app.add_route( + # DatabaseView.as_view(self), "/" + # ) + # app.add_route( + # TableView.as_view(self), + # "//", + # ) - def app(self): - app = Sanic(__name__) + def configure_jinja(self): default_templates = str(app_root / "datasette" / "templates") template_paths = [] if self.template_dir: @@ -465,6 +497,10 @@ class Datasette: self.jinja_env.filters["escape_sqlite"] = escape_sqlite self.jinja_env.filters["to_css_class"] = to_css_class pm.hook.prepare_jinja2_environment(env=self.jinja_env) + + def app(self): + app = Sanic(__name__) + self.configure_jinja() app.add_route(IndexView.as_view(self), "/") # TODO: /favicon.ico and /-/static/ deserve far-future cache expires app.add_route(favicon, "/favicon.ico") diff --git a/datasette/cli.py b/datasette/cli.py index 07c8bc99..a9e69877 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -377,8 +377,8 @@ def serve( # Force initial hashing/table counting ds.inspect() if asgi: - from uvicorn.run import UvicornServer + import uvicorn app = ds.asgi_app() - UvicornServer().run(app, host=host, port=port) + uvicorn.run(app, host, port, log_level="info") else: ds.app().run(host=host, port=port, debug=debug) diff --git a/datasette/views/base.py b/datasette/views/base.py index 6e6cb342..2e2d981d 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -56,21 +56,21 @@ class RequestWrapper: @property def query_string(self): - return str(self._request.query_string) + return str(self._request._scope["query_string"]) @property def args(self): # Key/list-of-values # There's probably a better way to do this: d = {} - for key, value in self._request.query_string: + for key, value in self._request.query_params: d.setdefault(key, []).append(value) return d @property def raw_args(self): # Flat key/first-value dictionary - return self._request.query_string._dict + return dict(self.args) class RenderMixin(HTTPMethodView): From 2302b76a2e14ca8b857268e1ffcafb9c2080fc80 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 26 Jul 2018 06:10:34 -0700 Subject: [PATCH 3/4] New dependencies: starlette and uvicorn --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 4a38faba..2de3a139 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,8 @@ setup( 'hupper==1.0', 'pint==0.8.1', 'pluggy>=0.1.0,<1.0', + 'starlette==0.1.14', + 'uvicorn==0.2.21', ], entry_points=''' [console_scripts] From f1c3cc362d3a33522dd456b41b33381476f2e821 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 26 Jul 2018 06:50:51 -0700 Subject: [PATCH 4/4] Upgrade to starlette==0.1.15 Fixes this bug: https://github.com/encode/starlette/issues/32 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2de3a139..c221fbca 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( 'hupper==1.0', 'pint==0.8.1', 'pluggy>=0.1.0,<1.0', - 'starlette==0.1.14', + 'starlette==0.1.15', 'uvicorn==0.2.21', ], entry_points='''