mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Redesigned register_output_renderer plugin hook, closes #581
This commit is contained in:
parent
446e5de65d
commit
52c4387c7d
8 changed files with 202 additions and 20 deletions
|
|
@ -586,7 +586,11 @@ class Datasette:
|
||||||
hook_renderers.append(hook)
|
hook_renderers.append(hook)
|
||||||
|
|
||||||
for renderer in hook_renderers:
|
for renderer in hook_renderers:
|
||||||
self.renderers[renderer["extension"]] = renderer["callback"]
|
self.renderers[renderer["extension"]] = (
|
||||||
|
# It used to be called "callback" - remove this in Datasette 1.0
|
||||||
|
renderer.get("render")
|
||||||
|
or renderer["callback"]
|
||||||
|
)
|
||||||
|
|
||||||
async def render_template(
|
async def render_template(
|
||||||
self, templates, context=None, request=None, view_name=None
|
self, templates, context=None, request=None, view_name=None
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from datasette.database import QueryInterrupted
|
||||||
from datasette.utils import (
|
from datasette.utils import (
|
||||||
InvalidSql,
|
InvalidSql,
|
||||||
LimitedWriter,
|
LimitedWriter,
|
||||||
|
call_with_supported_arguments,
|
||||||
is_url,
|
is_url,
|
||||||
path_with_added_args,
|
path_with_added_args,
|
||||||
path_with_removed_args,
|
path_with_removed_args,
|
||||||
|
|
@ -387,7 +388,21 @@ class DataView(BaseView):
|
||||||
if _format in self.ds.renderers.keys():
|
if _format in self.ds.renderers.keys():
|
||||||
# Dispatch request to the correct output format renderer
|
# Dispatch request to the correct output format renderer
|
||||||
# (CSV is not handled here due to streaming)
|
# (CSV is not handled here due to streaming)
|
||||||
result = self.ds.renderers[_format](request.args, data, self.name)
|
result = call_with_supported_arguments(
|
||||||
|
self.ds.renderers[_format],
|
||||||
|
datasette=self.ds,
|
||||||
|
columns=data.get("columns") or [],
|
||||||
|
rows=data.get("rows") or [],
|
||||||
|
sql=data.get("query", {}).get("sql", None),
|
||||||
|
query_name=data.get("query_name"),
|
||||||
|
database=database,
|
||||||
|
table=data.get("table"),
|
||||||
|
request=request,
|
||||||
|
view_name=self.name,
|
||||||
|
# These will be deprecated in Datasette 1.0:
|
||||||
|
args=request.args,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
raise NotFound("No data")
|
raise NotFound("No data")
|
||||||
|
|
||||||
|
|
@ -395,6 +410,7 @@ class DataView(BaseView):
|
||||||
body=result.get("body"),
|
body=result.get("body"),
|
||||||
status=result.get("status_code", 200),
|
status=result.get("status_code", 200),
|
||||||
content_type=result.get("content_type", "text/plain"),
|
content_type=result.get("content_type", "text/plain"),
|
||||||
|
headers=result.get("headers"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
extras = {}
|
extras = {}
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ class QueryView(DataView):
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
"database": database,
|
"database": database,
|
||||||
|
"query_name": canned_query,
|
||||||
"rows": results.rows,
|
"rows": results.rows,
|
||||||
"truncated": results.truncated,
|
"truncated": results.truncated,
|
||||||
"columns": columns,
|
"columns": columns,
|
||||||
|
|
|
||||||
|
|
@ -744,19 +744,37 @@ Allows the plugin to register a new output renderer, to output data in a custom
|
||||||
def register_output_renderer(datasette):
|
def register_output_renderer(datasette):
|
||||||
return {
|
return {
|
||||||
"extension": "test",
|
"extension": "test",
|
||||||
"callback": render_test
|
"render": render_test
|
||||||
}
|
}
|
||||||
|
|
||||||
This will register ``render_test`` to be called when paths with the extension ``.test`` (for example ``/database.test``, ``/database/table.test``, or ``/database/table/row.test``) are requested. When a request is received, the callback function is called with three positional arguments:
|
This will register ``render_test`` to be called when paths with the extension ``.test`` (for example ``/database.test``, ``/database/table.test``, or ``/database/table/row.test``) are requested. When a request is received, the callback function is called with zero or more of the following arguments. Datasette will inspect your callback function and pass arguments that match its function signature.
|
||||||
|
|
||||||
``args`` - dictionary
|
``datasette`` - :ref:`internals_datasette`
|
||||||
The GET parameters of the request
|
For accessing plugin configuration and executing queries.
|
||||||
|
|
||||||
``data`` - dictionary
|
``columns`` - list of strings
|
||||||
The data to be rendered
|
The names of the columns returned by this query.
|
||||||
|
|
||||||
|
``rows`` - list of ``sqlite3.Row`` objects
|
||||||
|
The rows returned by the query.
|
||||||
|
|
||||||
|
``sql`` - string
|
||||||
|
The SQL query that was executed.
|
||||||
|
|
||||||
|
``query_name`` - string or None
|
||||||
|
If this was the execution of a :ref:`canned query <canned_queries>`, the name of that query.
|
||||||
|
|
||||||
|
``database`` - string
|
||||||
|
The name of the database.
|
||||||
|
|
||||||
|
``table`` - string or None
|
||||||
|
The table or view, if one is being rendered.
|
||||||
|
|
||||||
|
``request`` - :ref:`internals_request`
|
||||||
|
The incoming HTTP request.
|
||||||
|
|
||||||
``view_name`` - string
|
``view_name`` - string
|
||||||
The name of the view where the renderer is being called. (``index``, ``database``, ``table``, and ``row`` are the most important ones.)
|
The name of the current view being called. ``index``, ``database``, ``table``, and ``row`` are the most important ones.
|
||||||
|
|
||||||
The callback function can return ``None``, if it is unable to render the data, or a dictionary with the following keys:
|
The callback function can return ``None``, if it is unable to render the data, or a dictionary with the following keys:
|
||||||
|
|
||||||
|
|
@ -769,15 +787,34 @@ The callback function can return ``None``, if it is unable to render the data, o
|
||||||
``status_code`` - integer, optional
|
``status_code`` - integer, optional
|
||||||
The HTTP status code, default 200
|
The HTTP status code, default 200
|
||||||
|
|
||||||
|
``headers`` - dictionary, optional
|
||||||
|
Extra HTTP headers to be returned in the response.
|
||||||
|
|
||||||
A simple example of an output renderer callback function:
|
A simple example of an output renderer callback function:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def render_test(args, data, view_name):
|
def render_test():
|
||||||
return {
|
return {
|
||||||
"body": "Hello World"
|
"body": "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Here is a more complex example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def render_test(columns, rows):
|
||||||
|
first_row = " | ".join(columns)
|
||||||
|
lines = [first_row]
|
||||||
|
lines.append("=" * len(first_row))
|
||||||
|
for row in rows:
|
||||||
|
lines.append(" | ".join(row))
|
||||||
|
return {
|
||||||
|
"body": "Hello World",
|
||||||
|
"content_type": "text/plain; charset=utf-8",
|
||||||
|
"headers": {"x-pipes": "yay-pipes"}
|
||||||
|
}
|
||||||
|
|
||||||
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_facet_classes:
|
.. _plugin_register_facet_classes:
|
||||||
|
|
|
||||||
42
tests/plugins/register_output_renderer.py
Normal file
42
tests/plugins/register_output_renderer.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
from datasette import hookimpl
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def render_test_all_parameters(
|
||||||
|
datasette, columns, rows, sql, query_name, database, table, request, view_name, data
|
||||||
|
):
|
||||||
|
headers = {}
|
||||||
|
for custom_header in request.args.getlist("header") or []:
|
||||||
|
key, value = custom_header.split(":")
|
||||||
|
headers[key] = value
|
||||||
|
return {
|
||||||
|
"body": json.dumps(
|
||||||
|
{
|
||||||
|
"datasette": datasette,
|
||||||
|
"columns": columns,
|
||||||
|
"rows": rows,
|
||||||
|
"sql": sql,
|
||||||
|
"query_name": query_name,
|
||||||
|
"database": database,
|
||||||
|
"table": table,
|
||||||
|
"request": request,
|
||||||
|
"view_name": view_name,
|
||||||
|
},
|
||||||
|
default=repr,
|
||||||
|
),
|
||||||
|
"content_type": request.args.get("content_type", "text/plain"),
|
||||||
|
"status_code": int(request.args.get("status_code", 200)),
|
||||||
|
"headers": headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_test_no_parameters():
|
||||||
|
return {"body": "Hello"}
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def register_output_renderer(datasette):
|
||||||
|
return [
|
||||||
|
{"extension": "testall", "render": render_test_all_parameters},
|
||||||
|
{"extension": "testnone", "callback": render_test_no_parameters},
|
||||||
|
]
|
||||||
|
|
@ -1259,16 +1259,16 @@ def test_threads_json(app_client):
|
||||||
|
|
||||||
def test_plugins_json(app_client):
|
def test_plugins_json(app_client):
|
||||||
response = app_client.get("/-/plugins.json")
|
response = app_client.get("/-/plugins.json")
|
||||||
assert [
|
expected = [
|
||||||
{"name": "my_plugin.py", "static": False, "templates": False, "version": None},
|
{"name": name, "static": False, "templates": False, "version": None}
|
||||||
{
|
for name in (
|
||||||
"name": "my_plugin_2.py",
|
"my_plugin.py",
|
||||||
"static": False,
|
"my_plugin_2.py",
|
||||||
"templates": False,
|
"register_output_renderer.py",
|
||||||
"version": None,
|
"view_name.py",
|
||||||
},
|
)
|
||||||
{"name": "view_name.py", "static": False, "templates": False, "version": None},
|
]
|
||||||
] == sorted(response.json, key=lambda p: p["name"])
|
assert expected == sorted(response.json, key=lambda p: p["name"])
|
||||||
|
|
||||||
|
|
||||||
def test_versions_json(app_client):
|
def test_versions_json(app_client):
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,8 @@ def test_table_csv_json_export_interface(app_client):
|
||||||
actual = [l["href"].split("/")[-1] for l in links]
|
actual = [l["href"].split("/")[-1] for l in links]
|
||||||
expected = [
|
expected = [
|
||||||
"simple_primary_key.json?id__gt=2",
|
"simple_primary_key.json?id__gt=2",
|
||||||
|
"simple_primary_key.testall?id__gt=2",
|
||||||
|
"simple_primary_key.testnone?id__gt=2",
|
||||||
"simple_primary_key.csv?id__gt=2&_size=max",
|
"simple_primary_key.csv?id__gt=2&_size=max",
|
||||||
"#export",
|
"#export",
|
||||||
]
|
]
|
||||||
|
|
@ -582,6 +584,8 @@ def test_csv_json_export_links_include_labels_if_foreign_keys(app_client):
|
||||||
actual = [l["href"].split("/")[-1] for l in links]
|
actual = [l["href"].split("/")[-1] for l in links]
|
||||||
expected = [
|
expected = [
|
||||||
"facetable.json?_labels=on",
|
"facetable.json?_labels=on",
|
||||||
|
"facetable.testall?_labels=on",
|
||||||
|
"facetable.testnone?_labels=on",
|
||||||
"facetable.csv?_labels=on&_size=max",
|
"facetable.csv?_labels=on&_size=max",
|
||||||
"#export",
|
"#export",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import textwrap
|
||||||
import pytest
|
import pytest
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
|
at_memory_re = re.compile(r" at 0x\w+")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail
|
@pytest.mark.xfail
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -329,3 +331,79 @@ def test_view_names(view_names_client, path, view_name):
|
||||||
response = view_names_client.get(path)
|
response = view_names_client.get(path)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert "view_name:{}".format(view_name) == response.body.decode("utf8")
|
assert "view_name:{}".format(view_name) == response.body.decode("utf8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_output_renderer_no_parameters(app_client):
|
||||||
|
response = app_client.get("/fixtures/facetable.testnone")
|
||||||
|
assert 200 == response.status
|
||||||
|
assert b"Hello" == response.body
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_output_renderer_all_parameters(app_client):
|
||||||
|
response = app_client.get("/fixtures/facetable.testall")
|
||||||
|
assert 200 == response.status
|
||||||
|
# Lots of 'at 0x103a4a690' in here - replace those so we can do
|
||||||
|
# an easy comparison
|
||||||
|
body = response.body.decode("utf-8")
|
||||||
|
body = at_memory_re.sub(" at 0xXXX", body)
|
||||||
|
assert {
|
||||||
|
"datasette": "<datasette.app.Datasette object at 0xXXX>",
|
||||||
|
"columns": [
|
||||||
|
"pk",
|
||||||
|
"created",
|
||||||
|
"planet_int",
|
||||||
|
"on_earth",
|
||||||
|
"state",
|
||||||
|
"city_id",
|
||||||
|
"neighborhood",
|
||||||
|
"tags",
|
||||||
|
"complex_array",
|
||||||
|
"distinct_some_null",
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
"<sqlite3.Row object at 0xXXX>",
|
||||||
|
],
|
||||||
|
"sql": "select pk, created, planet_int, on_earth, state, city_id, neighborhood, tags, complex_array, distinct_some_null from facetable order by pk limit 51",
|
||||||
|
"query_name": None,
|
||||||
|
"database": "fixtures",
|
||||||
|
"table": "facetable",
|
||||||
|
"request": "<datasette.utils.asgi.Request object at 0xXXX>",
|
||||||
|
"view_name": "table",
|
||||||
|
} == json.loads(body)
|
||||||
|
# Test that query_name is set correctly
|
||||||
|
query_response = app_client.get("/fixtures/pragma_cache_size.testall")
|
||||||
|
assert "pragma_cache_size" == json.loads(query_response.body)["query_name"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_output_renderer_custom_status_code(app_client):
|
||||||
|
response = app_client.get("/fixtures/pragma_cache_size.testall?status_code=202")
|
||||||
|
assert 202 == response.status
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_output_renderer_custom_content_type(app_client):
|
||||||
|
response = app_client.get(
|
||||||
|
"/fixtures/pragma_cache_size.testall?content_type=text/blah"
|
||||||
|
)
|
||||||
|
assert "text/blah" == response.headers["content-type"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_output_renderer_custom_headers(app_client):
|
||||||
|
response = app_client.get(
|
||||||
|
"/fixtures/pragma_cache_size.testall?header=x-wow:1&header=x-gosh:2"
|
||||||
|
)
|
||||||
|
assert "1" == response.headers["x-wow"]
|
||||||
|
assert "2" == response.headers["x-gosh"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue