mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
parent
d392dc1cfa
commit
f5e79adf26
9 changed files with 129 additions and 4 deletions
|
|
@ -39,6 +39,7 @@ from .renderer import json_renderer
|
||||||
from .database import Database, QueryInterrupted
|
from .database import Database, QueryInterrupted
|
||||||
|
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
async_call_with_supported_arguments,
|
||||||
escape_css_string,
|
escape_css_string,
|
||||||
escape_sqlite,
|
escape_sqlite,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
|
|
@ -783,6 +784,10 @@ class Datasette:
|
||||||
"Returns an ASGI app function that serves the whole of Datasette"
|
"Returns an ASGI app function that serves the whole of Datasette"
|
||||||
routes = []
|
routes = []
|
||||||
|
|
||||||
|
for routes_to_add in pm.hook.register_routes():
|
||||||
|
for regex, view_fn in routes_to_add:
|
||||||
|
routes.append((regex, wrap_view(view_fn, self)))
|
||||||
|
|
||||||
def add_route(view, regex):
|
def add_route(view, regex):
|
||||||
routes.append((regex, view))
|
routes.append((regex, view))
|
||||||
|
|
||||||
|
|
@ -1048,3 +1053,19 @@ def _cleaner_task_str(task):
|
||||||
# running at /Users/simonw/Dropbox/Development/datasette/venv-3.7.5/lib/python3.7/site-packages/uvicorn/main.py:361>
|
# running at /Users/simonw/Dropbox/Development/datasette/venv-3.7.5/lib/python3.7/site-packages/uvicorn/main.py:361>
|
||||||
# Clean up everything up to and including site-packages
|
# Clean up everything up to and including site-packages
|
||||||
return _cleaner_task_str_re.sub("", s)
|
return _cleaner_task_str_re.sub("", s)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_view(view_fn, datasette):
|
||||||
|
async def asgi_view_fn(scope, receive, send):
|
||||||
|
response = await async_call_with_supported_arguments(
|
||||||
|
view_fn,
|
||||||
|
scope=scope,
|
||||||
|
receive=receive,
|
||||||
|
send=send,
|
||||||
|
request=Request(scope, receive),
|
||||||
|
datasette=datasette,
|
||||||
|
)
|
||||||
|
if response is not None:
|
||||||
|
await response.asgi_send(send)
|
||||||
|
|
||||||
|
return asgi_view_fn
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,11 @@ def register_facet_classes():
|
||||||
"Register Facet subclasses"
|
"Register Facet subclasses"
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def register_routes():
|
||||||
|
"Register URL routes: return a list of (regex, view_function) pairs"
|
||||||
|
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def actor_from_request(datasette, request):
|
def actor_from_request(datasette, request):
|
||||||
"Return an actor dictionary based on the incoming request"
|
"Return an actor dictionary based on the incoming request"
|
||||||
|
|
|
||||||
|
|
@ -842,7 +842,7 @@ def parse_metadata(content):
|
||||||
raise BadMetadataError("Metadata is not valid JSON or YAML")
|
raise BadMetadataError("Metadata is not valid JSON or YAML")
|
||||||
|
|
||||||
|
|
||||||
def call_with_supported_arguments(fn, **kwargs):
|
def _gather_arguments(fn, kwargs):
|
||||||
parameters = inspect.signature(fn).parameters.keys()
|
parameters = inspect.signature(fn).parameters.keys()
|
||||||
call_with = []
|
call_with = []
|
||||||
for parameter in parameters:
|
for parameter in parameters:
|
||||||
|
|
@ -853,9 +853,19 @@ def call_with_supported_arguments(fn, **kwargs):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
call_with.append(kwargs[parameter])
|
call_with.append(kwargs[parameter])
|
||||||
|
return call_with
|
||||||
|
|
||||||
|
|
||||||
|
def call_with_supported_arguments(fn, **kwargs):
|
||||||
|
call_with = _gather_arguments(fn, kwargs)
|
||||||
return fn(*call_with)
|
return fn(*call_with)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_call_with_supported_arguments(fn, **kwargs):
|
||||||
|
call_with = _gather_arguments(fn, kwargs)
|
||||||
|
return await fn(*call_with)
|
||||||
|
|
||||||
|
|
||||||
def actor_matches_allow(actor, allow):
|
def actor_matches_allow(actor, allow):
|
||||||
actor = actor or {}
|
actor = actor or {}
|
||||||
if allow is None:
|
if allow is None:
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,7 @@ class Response:
|
||||||
@classmethod
|
@classmethod
|
||||||
def text(cls, body, status=200, headers=None):
|
def text(cls, body, status=200, headers=None):
|
||||||
return cls(
|
return cls(
|
||||||
body,
|
str(body),
|
||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type="text/plain; charset=utf-8",
|
content_type="text/plain; charset=utf-8",
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ Contents
|
||||||
--------
|
--------
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 3
|
||||||
|
|
||||||
getting_started
|
getting_started
|
||||||
installation
|
installation
|
||||||
|
|
|
||||||
|
|
@ -835,6 +835,55 @@ And here is an example ``can_render`` function which returns ``True`` only if th
|
||||||
|
|
||||||
Examples: `datasette-atom <https://github.com/simonw/datasette-atom>`_, `datasette-ics <https://github.com/simonw/datasette-ics>`_
|
Examples: `datasette-atom <https://github.com/simonw/datasette-atom>`_, `datasette-ics <https://github.com/simonw/datasette-ics>`_
|
||||||
|
|
||||||
|
.. _plugin_register_routes:
|
||||||
|
|
||||||
|
register_routes()
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Register additional view functions to execute for specified URL routes.
|
||||||
|
|
||||||
|
Return a list of ``(regex, async_view_function)`` pairs, something like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from datasette.utils.asgi import Response
|
||||||
|
import html
|
||||||
|
|
||||||
|
|
||||||
|
async def hello_from(scope):
|
||||||
|
name = scope["url_route"]["kwargs"]["name"]
|
||||||
|
return Response.html("Hello from {}".format(
|
||||||
|
html.escape(name)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def register_routes():
|
||||||
|
return [
|
||||||
|
(r"^/hello-from/(?P<name>.*)$"), hello_from)
|
||||||
|
]
|
||||||
|
|
||||||
|
The view functions can take a number of different optional arguments. The corresponding argument will be passed to your function depending on its named parameters - a form of dependency injection.
|
||||||
|
|
||||||
|
The optional view function arguments are as follows:
|
||||||
|
|
||||||
|
``datasette`` - :ref:`internals_datasette`
|
||||||
|
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.
|
||||||
|
|
||||||
|
``request`` - Request object
|
||||||
|
The current HTTP :ref:`internals_request`.
|
||||||
|
|
||||||
|
``scope`` - dictionary
|
||||||
|
The incoming ASGI scope dictionary.
|
||||||
|
|
||||||
|
``send`` - function
|
||||||
|
The ASGI send function.
|
||||||
|
|
||||||
|
``receive`` - function
|
||||||
|
The ASGI receive function.
|
||||||
|
|
||||||
|
The function can either return a ``Response`` or it can return nothing and instead respond directly to the request using the ASGI ``receive`` function (for advanced uses only).
|
||||||
|
|
||||||
.. _plugin_register_facet_classes:
|
.. _plugin_register_facet_classes:
|
||||||
|
|
||||||
register_facet_classes()
|
register_facet_classes()
|
||||||
|
|
@ -901,7 +950,6 @@ The plugin hook can then be used to register the new facet class like this:
|
||||||
def register_facet_classes():
|
def register_facet_classes():
|
||||||
return [SpecialFacet]
|
return [SpecialFacet]
|
||||||
|
|
||||||
|
|
||||||
.. _plugin_asgi_wrapper:
|
.. _plugin_asgi_wrapper:
|
||||||
|
|
||||||
asgi_wrapper(datasette)
|
asgi_wrapper(datasette)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ EXPECTED_PLUGINS = [
|
||||||
"prepare_connection",
|
"prepare_connection",
|
||||||
"prepare_jinja2_environment",
|
"prepare_jinja2_environment",
|
||||||
"register_facet_classes",
|
"register_facet_classes",
|
||||||
|
"register_routes",
|
||||||
"render_cell",
|
"render_cell",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from datasette import hookimpl
|
from datasette import hookimpl
|
||||||
from datasette.facets import Facet
|
from datasette.facets import Facet
|
||||||
from datasette.utils import path_with_added_args
|
from datasette.utils import path_with_added_args
|
||||||
|
from datasette.utils.asgi import asgi_send_json, Response
|
||||||
import base64
|
import base64
|
||||||
import pint
|
import pint
|
||||||
import json
|
import json
|
||||||
|
|
@ -142,3 +143,27 @@ def permission_allowed(actor, action):
|
||||||
return True
|
return True
|
||||||
elif action == "this_is_denied":
|
elif action == "this_is_denied":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def register_routes():
|
||||||
|
async def one(datasette):
|
||||||
|
return Response.text(
|
||||||
|
(await datasette.get_database().execute("select 1 + 1")).first()[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def two(request, scope):
|
||||||
|
name = scope["url_route"]["kwargs"]["name"]
|
||||||
|
greeting = request.args.get("greeting")
|
||||||
|
return Response.text("{} {}".format(greeting, name))
|
||||||
|
|
||||||
|
async def three(scope, send):
|
||||||
|
await asgi_send_json(
|
||||||
|
send, {"hello": "world"}, status=200, headers={"x-three": "1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
(r"/one/$", one),
|
||||||
|
(r"/two/(?P<name>.*)$", two),
|
||||||
|
(r"/three/$", three),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -544,3 +544,18 @@ def test_actor_json(app_client):
|
||||||
assert {"actor": {"id": "bot2", "1+1": 2}} == app_client.get(
|
assert {"actor": {"id": "bot2", "1+1": 2}} == app_client.get(
|
||||||
"/-/actor.json/?_bot2=1"
|
"/-/actor.json/?_bot2=1"
|
||||||
).json
|
).json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,body", [("/one/", "2"), ("/two/Ray?greeting=Hail", "Hail Ray"),]
|
||||||
|
)
|
||||||
|
def test_register_routes(app_client, path, body):
|
||||||
|
response = app_client.get(path)
|
||||||
|
assert 200 == response.status
|
||||||
|
assert body == response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_routes_asgi(app_client):
|
||||||
|
response = app_client.get("/three/")
|
||||||
|
assert {"hello": "world"} == response.json
|
||||||
|
assert "1" == response.headers["x-three"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue