This commit is contained in:
Simon Willison 2018-12-30 20:21:44 -08:00
commit dcfdc0c49a
4 changed files with 127 additions and 29 deletions

View file

@ -439,35 +439,47 @@ class Datasette:
self.executor, sql_operation_in_thread
)
def asgi_app(self):
self.configure_jinja()
from starlette.routing import Router, Mount, Route
from starlette.responses import FileResponse
from starlette.staticfiles import StaticFiles
async def favicon(request):
return FileResponse(
path=str(app_root / "datasette" / "static" / "favicon.ico"),
media_type="image/x-icon"
)
return Router([
Route(
'/',
endpoint=IndexView(self).starlette_asgi_endpoint(),
methods=['GET']
),
Route(
'/favicon.ico',
favicon,
methods=['GET']
),
Mount(
'/-/static',
app=StaticFiles(directory=app_root / "datasette" / "static")
),
Route(
'/{db_name}',
endpoint=DatabaseView(self).starlette_asgi_endpoint(),
methods=['GET']
),
Route(
'/{db_name}/{table}',
endpoint=TableView(self).starlette_asgi_endpoint(),
methods=['GET']
),
])
def app(self):
app = Sanic(__name__)
default_templates = str(app_root / "datasette" / "templates")
template_paths = []
if self.template_dir:
template_paths.append(self.template_dir)
template_paths.extend(
[
plugin["templates_path"]
for plugin in get_plugins(pm)
if plugin["templates_path"]
]
)
template_paths.append(default_templates)
template_loader = ChoiceLoader(
[
FileSystemLoader(template_paths),
# Support {% extends "default:table.html" %}:
PrefixLoader(
{"default": FileSystemLoader(default_templates)}, delimiter=":"
),
]
)
self.jinja_env = Environment(loader=template_loader, autoescape=True)
self.jinja_env.filters["escape_css_string"] = escape_css_string
self.jinja_env.filters["quote_plus"] = lambda u: urllib.parse.quote_plus(u)
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)
self.configure_jinja()
app.add_route(IndexView.as_view(self), r"/<as_format:(\.jsono?)?$>")
# TODO: /favicon.ico and /-/static/ deserve far-future cache expires
app.add_route(favicon, "/favicon.ico")
@ -561,3 +573,33 @@ class Datasette:
return response.html(template.render(info), status=status)
return app
def configure_jinja(self):
default_templates = str(app_root / "datasette" / "templates")
template_paths = []
if self.template_dir:
template_paths.append(self.template_dir)
template_paths.extend(
[
plugin["templates_path"]
for plugin in get_plugins(pm)
if plugin["templates_path"]
]
)
template_paths.append(default_templates)
template_loader = ChoiceLoader(
[
FileSystemLoader(template_paths),
# Support {% extends "default:table.html" %}:
PrefixLoader(
{"default": FileSystemLoader(default_templates)}, delimiter=":"
),
]
)
self.jinja_env = Environment(loader=template_loader, autoescape=True)
self.jinja_env.filters["escape_css_string"] = escape_css_string
self.jinja_env.filters["quote_plus"] = lambda u: urllib.parse.quote_plus(u)
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)

View file

@ -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,
@ -373,4 +377,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)

View file

@ -30,6 +30,9 @@ from datasette.utils import (
value_as_boolean,
)
from starlette.endpoints import HTTPEndpoint as StarletteEndpoint
from starlette.responses import Response as StarletteResponse
ureg = pint.UnitRegistry()
HASH_LENGTH = 7
@ -45,6 +48,39 @@ 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 url(self):
return self._request.url._url
@property
def path(self):
return self._request.url.path
@property
def query_string(self):
q = self._request._scope["query_string"].decode("utf8")
return q
@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.items():
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 _asset_urls(self, key, template, context):
@ -73,6 +109,17 @@ class RenderMixin(HTTPMethodView):
else:
yield {"url": url}
def starlette_asgi_endpoint(self):
class App(StarletteEndpoint):
async def get(other_self, request):
#import pdb; pdb.set_trace()
sanic_response = await self.get(
RequestWrapper(request), **request.path_params,
)
# TODO: media_type, status_code, headers etc
return StarletteResponse(sanic_response.body)
return App
def render(self, templates, **context):
template = self.ds.jinja_env.select_template(templates)
select_templates = [

View file

@ -13,7 +13,7 @@ class IndexView(RenderMixin):
def __init__(self, datasette):
self.ds = datasette
async def get(self, request, as_format):
async def get(self, request, as_format=''):
databases = []
for key, info in sorted(self.ds.inspect().items()):
tables = [t for t in info["tables"].values() if not t["hidden"]]