diff --git a/datasette/app.py b/datasette/app.py index 052131d0..0307828e 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -430,8 +430,46 @@ class Datasette: self.executor, sql_operation_in_thread ) - def app(self): - app = Sanic(__name__) + def asgi_app(self): + self.configure_jinja() + from starlette.routing import Router, Path, PathPrefix + from starlette.staticfiles import StaticFile, StaticFiles + return Router([ + 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 configure_jinja(self): default_templates = str(app_root / "datasette" / "templates") template_paths = [] if self.template_dir: @@ -459,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 820367ac..a9e69877 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: + import uvicorn + app = ds.asgi_app() + 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 45bc8183..2e2d981d 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._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_params: + d.setdefault(key, []).append(value) + return d + + @property + def raw_args(self): + # Flat key/first-value dictionary + return dict(self.args) + + 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 = [ diff --git a/setup.py b/setup.py index 4a38faba..c221fbca 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.15', + 'uvicorn==0.2.21', ], entry_points=''' [console_scripts]