mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Port Datasette from Sanic to ASGI + Uvicorn (#518)
Datasette now uses ASGI internally, and no longer depends on Sanic. It now uses Uvicorn as the underlying HTTP server. This was thirteen months in the making... for full details see the issue: https://github.com/simonw/datasette/issues/272 And for a full sequence of commits plus commentary, see the pull request: https://github.com/simonw/datasette/pull/518
This commit is contained in:
parent
35429f9089
commit
ba8db9679f
19 changed files with 1510 additions and 947 deletions
|
|
@ -7,9 +7,8 @@ import urllib
|
|||
|
||||
import jinja2
|
||||
import pint
|
||||
from sanic import response
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
from html import escape
|
||||
|
||||
from datasette import __version__
|
||||
from datasette.plugins import pm
|
||||
|
|
@ -26,6 +25,14 @@ from datasette.utils import (
|
|||
sqlite3,
|
||||
to_css_class,
|
||||
)
|
||||
from datasette.utils.asgi import (
|
||||
AsgiStream,
|
||||
AsgiWriter,
|
||||
AsgiRouter,
|
||||
AsgiView,
|
||||
NotFound,
|
||||
Response,
|
||||
)
|
||||
|
||||
ureg = pint.UnitRegistry()
|
||||
|
||||
|
|
@ -49,7 +56,14 @@ class DatasetteError(Exception):
|
|||
self.messagge_is_html = messagge_is_html
|
||||
|
||||
|
||||
class BaseView(HTTPMethodView):
|
||||
class BaseView(AsgiView):
|
||||
ds = None
|
||||
|
||||
async def head(self, *args, **kwargs):
|
||||
response = await self.get(*args, **kwargs)
|
||||
response.body = b""
|
||||
return response
|
||||
|
||||
def _asset_urls(self, key, template, context):
|
||||
# Flatten list-of-lists from plugins:
|
||||
seen_urls = set()
|
||||
|
|
@ -104,7 +118,7 @@ class BaseView(HTTPMethodView):
|
|||
datasette=self.ds,
|
||||
):
|
||||
body_scripts.append(jinja2.Markup(script))
|
||||
return response.html(
|
||||
return Response.html(
|
||||
template.render(
|
||||
{
|
||||
**context,
|
||||
|
|
@ -136,7 +150,7 @@ class DataView(BaseView):
|
|||
self.ds = datasette
|
||||
|
||||
def options(self, request, *args, **kwargs):
|
||||
r = response.text("ok")
|
||||
r = Response.text("ok")
|
||||
if self.ds.cors:
|
||||
r.headers["Access-Control-Allow-Origin"] = "*"
|
||||
return r
|
||||
|
|
@ -146,7 +160,7 @@ class DataView(BaseView):
|
|||
path = "{}?{}".format(path, request.query_string)
|
||||
if remove_args:
|
||||
path = path_with_removed_args(request, remove_args, path=path)
|
||||
r = response.redirect(path)
|
||||
r = Response.redirect(path)
|
||||
r.headers["Link"] = "<{}>; rel=preload".format(path)
|
||||
if self.ds.cors:
|
||||
r.headers["Access-Control-Allow-Origin"] = "*"
|
||||
|
|
@ -195,17 +209,17 @@ class DataView(BaseView):
|
|||
kwargs["table"] = table
|
||||
if _format:
|
||||
kwargs["as_format"] = ".{}".format(_format)
|
||||
elif "table" in kwargs:
|
||||
elif kwargs.get("table"):
|
||||
kwargs["table"] = urllib.parse.unquote_plus(kwargs["table"])
|
||||
|
||||
should_redirect = "/{}-{}".format(name, expected)
|
||||
if "table" in kwargs:
|
||||
if kwargs.get("table"):
|
||||
should_redirect += "/" + urllib.parse.quote_plus(kwargs["table"])
|
||||
if "pk_path" in kwargs:
|
||||
if kwargs.get("pk_path"):
|
||||
should_redirect += "/" + kwargs["pk_path"]
|
||||
if "as_format" in kwargs:
|
||||
if kwargs.get("as_format"):
|
||||
should_redirect += kwargs["as_format"]
|
||||
if "as_db" in kwargs:
|
||||
if kwargs.get("as_db"):
|
||||
should_redirect += kwargs["as_db"]
|
||||
|
||||
if (
|
||||
|
|
@ -246,7 +260,7 @@ class DataView(BaseView):
|
|||
response_or_template_contexts = await self.data(
|
||||
request, database, hash, **kwargs
|
||||
)
|
||||
if isinstance(response_or_template_contexts, response.HTTPResponse):
|
||||
if isinstance(response_or_template_contexts, Response):
|
||||
return response_or_template_contexts
|
||||
else:
|
||||
data, _, _ = response_or_template_contexts
|
||||
|
|
@ -282,13 +296,13 @@ class DataView(BaseView):
|
|||
if not first:
|
||||
data, _, _ = await self.data(request, database, hash, **kwargs)
|
||||
if first:
|
||||
writer.writerow(headings)
|
||||
await writer.writerow(headings)
|
||||
first = False
|
||||
next = data.get("next")
|
||||
for row in data["rows"]:
|
||||
if not expanded_columns:
|
||||
# Simple path
|
||||
writer.writerow(row)
|
||||
await writer.writerow(row)
|
||||
else:
|
||||
# Look for {"value": "label": } dicts and expand
|
||||
new_row = []
|
||||
|
|
@ -298,10 +312,10 @@ class DataView(BaseView):
|
|||
new_row.append(cell["label"])
|
||||
else:
|
||||
new_row.append(cell)
|
||||
writer.writerow(new_row)
|
||||
await writer.writerow(new_row)
|
||||
except Exception as e:
|
||||
print("caught this", e)
|
||||
r.write(str(e))
|
||||
await r.write(str(e))
|
||||
return
|
||||
|
||||
content_type = "text/plain; charset=utf-8"
|
||||
|
|
@ -315,7 +329,7 @@ class DataView(BaseView):
|
|||
)
|
||||
headers["Content-Disposition"] = disposition
|
||||
|
||||
return response.stream(stream_fn, headers=headers, content_type=content_type)
|
||||
return AsgiStream(stream_fn, headers=headers, content_type=content_type)
|
||||
|
||||
async def get_format(self, request, database, args):
|
||||
""" Determine the format of the response from the request, from URL
|
||||
|
|
@ -363,7 +377,7 @@ class DataView(BaseView):
|
|||
response_or_template_contexts = await self.data(
|
||||
request, database, hash, **kwargs
|
||||
)
|
||||
if isinstance(response_or_template_contexts, response.HTTPResponse):
|
||||
if isinstance(response_or_template_contexts, Response):
|
||||
return response_or_template_contexts
|
||||
|
||||
else:
|
||||
|
|
@ -414,17 +428,11 @@ class DataView(BaseView):
|
|||
if result is None:
|
||||
raise NotFound("No data")
|
||||
|
||||
response_args = {
|
||||
"content_type": result.get("content_type", "text/plain"),
|
||||
"status": result.get("status_code", 200),
|
||||
}
|
||||
|
||||
if type(result.get("body")) == bytes:
|
||||
response_args["body_bytes"] = result.get("body")
|
||||
else:
|
||||
response_args["body"] = result.get("body")
|
||||
|
||||
r = response.HTTPResponse(**response_args)
|
||||
r = Response(
|
||||
body=result.get("body"),
|
||||
status=result.get("status_code", 200),
|
||||
content_type=result.get("content_type", "text/plain"),
|
||||
)
|
||||
else:
|
||||
extras = {}
|
||||
if callable(extra_template_data):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import os
|
||||
|
||||
from sanic import response
|
||||
|
||||
from datasette.utils import to_css_class, validate_sql_select
|
||||
from datasette.utils.asgi import AsgiFileDownload
|
||||
|
||||
from .base import DataView, DatasetteError
|
||||
from .base import DatasetteError, DataView
|
||||
|
||||
|
||||
class DatabaseView(DataView):
|
||||
|
|
@ -79,8 +78,8 @@ class DatabaseDownload(DataView):
|
|||
if not db.path:
|
||||
raise DatasetteError("Cannot download database", status=404)
|
||||
filepath = db.path
|
||||
return await response.file_stream(
|
||||
return AsgiFileDownload(
|
||||
filepath,
|
||||
filename=os.path.basename(filepath),
|
||||
mime_type="application/octet-stream",
|
||||
content_type="application/octet-stream",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import hashlib
|
||||
import json
|
||||
|
||||
from sanic import response
|
||||
|
||||
from datasette.utils import CustomJSONEncoder
|
||||
from datasette.utils.asgi import Response
|
||||
from datasette.version import __version__
|
||||
|
||||
from .base import BaseView
|
||||
|
|
@ -104,9 +103,9 @@ class IndexView(BaseView):
|
|||
headers = {}
|
||||
if self.ds.cors:
|
||||
headers["Access-Control-Allow-Origin"] = "*"
|
||||
return response.HTTPResponse(
|
||||
return Response(
|
||||
json.dumps({db["name"]: db for db in databases}, cls=CustomJSONEncoder),
|
||||
content_type="application/json",
|
||||
content_type="application/json; charset=utf-8",
|
||||
headers=headers,
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
from sanic import response
|
||||
from datasette.utils.asgi import Response
|
||||
from .base import BaseView
|
||||
|
||||
|
||||
|
|
@ -17,8 +17,10 @@ class JsonDataView(BaseView):
|
|||
headers = {}
|
||||
if self.ds.cors:
|
||||
headers["Access-Control-Allow-Origin"] = "*"
|
||||
return response.HTTPResponse(
|
||||
json.dumps(data), content_type="application/json", headers=headers
|
||||
return Response(
|
||||
json.dumps(data),
|
||||
content_type="application/json; charset=utf-8",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ import itertools
|
|||
import json
|
||||
|
||||
import jinja2
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.request import RequestParameters
|
||||
|
||||
from datasette.plugins import pm
|
||||
from datasette.utils import (
|
||||
CustomRow,
|
||||
QueryInterrupted,
|
||||
RequestParameters,
|
||||
append_querystring,
|
||||
compound_keys_after_sql,
|
||||
escape_sqlite,
|
||||
|
|
@ -24,6 +23,7 @@ from datasette.utils import (
|
|||
urlsafe_components,
|
||||
value_as_boolean,
|
||||
)
|
||||
from datasette.utils.asgi import NotFound
|
||||
from datasette.filters import Filters
|
||||
from .base import DataView, DatasetteError, ureg
|
||||
|
||||
|
|
@ -219,8 +219,7 @@ class TableView(RowTableShared):
|
|||
if is_view:
|
||||
order_by = ""
|
||||
|
||||
# We roll our own query_string decoder because by default Sanic
|
||||
# drops anything with an empty value e.g. ?name__exact=
|
||||
# Ensure we don't drop anything with an empty value e.g. ?name__exact=
|
||||
args = RequestParameters(
|
||||
urllib.parse.parse_qs(request.query_string, keep_blank_values=True)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue