Skip to content

Commit

Permalink
Added asgi_wrapper plugin hook, closes #520
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Jul 7, 2019
1 parent 3ff5b2e commit ef8a512
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 1 deletion.
5 changes: 4 additions & 1 deletion datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,9 +651,12 @@ async def setup_db():
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):
Expand Down
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
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"
Expand Down
41 changes: 41 additions & 0 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 23 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = (
Expand Down
5 changes: 5 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

0 comments on commit ef8a512

Please sign in to comment.