Compare commits

...

1 commit

Author SHA1 Message Date
Simon Willison
93bfa26bfd Added asgi_wrapper plugin hook, closes #520 2019-07-02 20:57:28 -07:00
5 changed files with 78 additions and 1 deletions

View file

@ -651,9 +651,12 @@ class Datasette:
if not database.is_mutable:
await database.table_counts(limit=60 * 60 * 1000)
return AsgiLifespan(
asgi = AsgiLifespan(
AsgiTracer(DatasetteRouter(self, routes)), on_startup=setup_db
)
for wrapper in pm.hook.asgi_wrapper(datasette=self):
asgi = wrapper(asgi)
return asgi
class DatasetteRouter(AsgiRouter):

View file

@ -5,6 +5,11 @@ hookspec = HookspecMarker("datasette")
hookimpl = HookimplMarker("datasette")
@hookspec
def asgi_wrapper(datasette):
"Returns an ASGI middleware callable to wrap our ASGI application with"
@hookspec
def prepare_connection(conn):
"Modify SQLite connection in some way e.g. register custom SQL functions"

View file

@ -666,3 +666,44 @@ The plugin hook can then be used to register the new facet class like this:
@hookimpl
def register_facet_classes():
return [SpecialFacet]
.. _plugin_asgi_wrapper:
asgi_wrapper(datasette)
~~~~~~~~~~~~~~~~~~~~~~~
Return an `ASGI <https://asgi.readthedocs.io/>`__ middleware wrapper function that will be applied to the Datasette ASGI application.
This is a very powerful hook. You can use it to manipulate the entire Datasette response, or even to configure new URL routes that will be handled by your own custom code.
You can write your ASGI code directly against the low-level specification, or you can use the middleware utilites provided by an ASGI framework such as `Starlette <https://www.starlette.io/middleware/>`__.
This example plugin adds a ``x-databases`` HTTP header listing the currently attached databases:
.. code-block:: python
from datasette import hookimpl
from functools import wraps
@hookimpl
def asgi_wrapper(datasette):
def wrap_with_databases_header(app):
@wraps(app)
async def add_x_databases_header(scope, recieve, send):
async def wrapped_send(event):
if event["type"] == "http.response.start":
original_headers = event.get("headers") or []
event = {
"type": event["type"],
"status": event["status"],
"headers": original_headers + [
[b"x-databases",
", ".join(datasette.databases.keys()).encode("utf-8")]
],
}
await send(event)
await app(scope, recieve, wrapped_send)
return add_x_databases_header
return wrap_with_databases_header

View file

@ -372,6 +372,7 @@ def render_cell(value, column, table, database, datasette):
PLUGIN2 = """
from datasette import hookimpl
from functools import wraps
import jinja2
import json
@ -413,6 +414,28 @@ def render_cell(value, database):
label=jinja2.escape(data["label"] or "") or "&nbsp;"
)
)
@hookimpl
def asgi_wrapper(datasette):
def wrap_with_databases_header(app):
@wraps(app)
async def add_x_databases_header(scope, recieve, send):
async def wrapped_send(event):
if event["type"] == "http.response.start":
original_headers = event.get("headers") or []
event = {
"type": event["type"],
"status": event["status"],
"headers": original_headers + [
[b"x-databases",
", ".join(datasette.databases.keys()).encode("utf-8")]
],
}
await send(event)
await app(scope, recieve, wrapped_send)
return add_x_databases_header
return wrap_with_databases_header
"""
TABLES = (

View file

@ -162,3 +162,8 @@ def test_plugins_extra_body_script(app_client, path, expected_extra_body_script)
json_data = r.search(app_client.get(path).body.decode("utf8")).group(1)
actual_data = json.loads(json_data)
assert expected_extra_body_script == actual_data
def test_plugins_asgi_wrapper(app_client):
response = app_client.get("/fixtures")
assert "fixtures" == response.headers["x-databases"]