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 = [