mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
can_render mechanism for register_output_renderer, closes #770
This commit is contained in:
parent
75cd432e5a
commit
5ab411c733
6 changed files with 108 additions and 18 deletions
|
|
@ -228,7 +228,7 @@ class Datasette:
|
||||||
if config_dir and (config_dir / "config.json").exists() and not config:
|
if config_dir and (config_dir / "config.json").exists() and not config:
|
||||||
config = json.load((config_dir / "config.json").open())
|
config = json.load((config_dir / "config.json").open())
|
||||||
self._config = dict(DEFAULT_CONFIG, **(config or {}))
|
self._config = dict(DEFAULT_CONFIG, **(config or {}))
|
||||||
self.renderers = {} # File extension -> renderer function
|
self.renderers = {} # File extension -> (renderer, can_render) functions
|
||||||
self.version_note = version_note
|
self.version_note = version_note
|
||||||
self.executor = futures.ThreadPoolExecutor(
|
self.executor = futures.ThreadPoolExecutor(
|
||||||
max_workers=self.config("num_sql_threads")
|
max_workers=self.config("num_sql_threads")
|
||||||
|
|
@ -574,7 +574,7 @@ class Datasette:
|
||||||
def register_renderers(self):
|
def register_renderers(self):
|
||||||
""" Register output renderers which output data in custom formats. """
|
""" Register output renderers which output data in custom formats. """
|
||||||
# Built-in renderers
|
# Built-in renderers
|
||||||
self.renderers["json"] = json_renderer
|
self.renderers["json"] = (json_renderer, lambda: True)
|
||||||
|
|
||||||
# Hooks
|
# Hooks
|
||||||
hook_renderers = []
|
hook_renderers = []
|
||||||
|
|
@ -588,8 +588,8 @@ class Datasette:
|
||||||
for renderer in hook_renderers:
|
for renderer in hook_renderers:
|
||||||
self.renderers[renderer["extension"]] = (
|
self.renderers[renderer["extension"]] = (
|
||||||
# It used to be called "callback" - remove this in Datasette 1.0
|
# It used to be called "callback" - remove this in Datasette 1.0
|
||||||
renderer.get("render")
|
renderer.get("render") or renderer["callback"],
|
||||||
or renderer["callback"]
|
renderer.get("can_render") or (lambda: True),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def render_template(
|
async def render_template(
|
||||||
|
|
|
||||||
|
|
@ -811,6 +811,10 @@ def call_with_supported_arguments(fn, **kwargs):
|
||||||
call_with = []
|
call_with = []
|
||||||
for parameter in parameters:
|
for parameter in parameters:
|
||||||
if parameter not in kwargs:
|
if parameter not in kwargs:
|
||||||
raise TypeError("{} requires parameters {}".format(fn, tuple(parameters)))
|
raise TypeError(
|
||||||
|
"{} requires parameters {}, missing: {}".format(
|
||||||
|
fn, tuple(parameters), set(parameters) - set(kwargs.keys())
|
||||||
|
)
|
||||||
|
)
|
||||||
call_with.append(kwargs[parameter])
|
call_with.append(kwargs[parameter])
|
||||||
return fn(*call_with)
|
return fn(*call_with)
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,7 @@ class DataView(BaseView):
|
||||||
# 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 = call_with_supported_arguments(
|
result = call_with_supported_arguments(
|
||||||
self.ds.renderers[_format],
|
self.ds.renderers[_format][0],
|
||||||
datasette=self.ds,
|
datasette=self.ds,
|
||||||
columns=data.get("columns") or [],
|
columns=data.get("columns") or [],
|
||||||
rows=data.get("rows") or [],
|
rows=data.get("rows") or [],
|
||||||
|
|
@ -426,10 +426,27 @@ class DataView(BaseView):
|
||||||
if data.get("expandable_columns"):
|
if data.get("expandable_columns"):
|
||||||
url_labels_extra = {"_labels": "on"}
|
url_labels_extra = {"_labels": "on"}
|
||||||
|
|
||||||
renderers = {
|
renderers = {}
|
||||||
key: path_with_format(request, key, {**url_labels_extra})
|
for key, (_, can_render) in self.ds.renderers.items():
|
||||||
for key in self.ds.renderers.keys()
|
it_can_render = call_with_supported_arguments(
|
||||||
}
|
can_render,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if asyncio.iscoroutine(it_can_render):
|
||||||
|
it_can_render = await it_can_render
|
||||||
|
if it_can_render:
|
||||||
|
renderers[key] = path_with_format(
|
||||||
|
request, key, {**url_labels_extra}
|
||||||
|
)
|
||||||
|
|
||||||
url_csv_args = {"_size": "max", **url_labels_extra}
|
url_csv_args = {"_size": "max", **url_labels_extra}
|
||||||
url_csv = path_with_format(request, "csv", url_csv_args)
|
url_csv = path_with_format(request, "csv", url_csv_args)
|
||||||
url_csv_path = url_csv.split("?")[0]
|
url_csv_path = url_csv.split("?")[0]
|
||||||
|
|
|
||||||
|
|
@ -744,14 +744,17 @@ Registers a new output renderer, to output data in a custom format. The hook fun
|
||||||
def register_output_renderer(datasette):
|
def register_output_renderer(datasette):
|
||||||
return {
|
return {
|
||||||
"extension": "test",
|
"extension": "test",
|
||||||
"render": render_test
|
"render": render_demo,
|
||||||
|
"can_render": can_render_demo, # Optional
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
This will register ``render_demo`` to be called when paths with the extension ``.test`` (for example ``/database.test``, ``/database/table.test``, or ``/database/table/row.test``) are requested.
|
||||||
|
|
||||||
``render_test`` is a Python function. It can be a regular function or an ``async def render_test()`` awaitable function, depending on if it needs to make any asynchronous calls.
|
``render_demo`` is a Python function. It can be a regular function or an ``async def render_demo()`` awaitable function, depending on if it needs to make any asynchronous calls.
|
||||||
|
|
||||||
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.
|
``can_render_demo`` is a Python function (or ``async def`` function) which acepts the same arguments as ``render_demo`` but just returns ``True`` or ``False``. It lets Datasette know if the current SQL query can be represented by the plugin - and hence influnce if a link to this output format is displayed in the user interface. If you omit the ``"can_render"`` key from the dictionary every query will be treated as being supported by the plugin.
|
||||||
|
|
||||||
|
When a request is received, the ``"render"`` 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.
|
||||||
|
|
||||||
``datasette`` - :ref:`internals_datasette`
|
``datasette`` - :ref:`internals_datasette`
|
||||||
For accessing plugin configuration and executing queries.
|
For accessing plugin configuration and executing queries.
|
||||||
|
|
@ -798,7 +801,7 @@ A simple example of an output renderer callback function:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def render_test():
|
def render_demo():
|
||||||
return {
|
return {
|
||||||
"body": "Hello World"
|
"body": "Hello World"
|
||||||
}
|
}
|
||||||
|
|
@ -807,7 +810,7 @@ Here is a more complex example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
async def render_test(datasette, columns, rows):
|
async def render_demo(datasette, columns, rows):
|
||||||
db = next(iter(datasette.databases.values()))
|
db = next(iter(datasette.databases.values()))
|
||||||
result = await db.execute("select sqlite_version()")
|
result = await db.execute("select sqlite_version()")
|
||||||
first_row = " | ".join(columns)
|
first_row = " | ".join(columns)
|
||||||
|
|
@ -821,6 +824,13 @@ Here is a more complex example:
|
||||||
"headers": {"x-sqlite-version": result.first()[0]},
|
"headers": {"x-sqlite-version": result.first()[0]},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
And here is an example ``can_render`` function which returns ``True`` only if the query results contain the columns ``atom_id``, ``atom_title`` and ``atom_updated``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def can_render_demo(columns):
|
||||||
|
return {"atom_id", "atom_title", "atom_updated"}.issubset(columns)
|
||||||
|
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,26 @@ from datasette import hookimpl
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
async def can_render(
|
||||||
|
datasette, columns, rows, sql, query_name, database, table, request, view_name
|
||||||
|
):
|
||||||
|
# We stash this on datasette so the calling unit test can see it
|
||||||
|
datasette._can_render_saw = {
|
||||||
|
"datasette": datasette,
|
||||||
|
"columns": columns,
|
||||||
|
"rows": rows,
|
||||||
|
"sql": sql,
|
||||||
|
"query_name": query_name,
|
||||||
|
"database": database,
|
||||||
|
"table": table,
|
||||||
|
"request": request,
|
||||||
|
"view_name": view_name,
|
||||||
|
}
|
||||||
|
if request.args.get("_no_can_render"):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def render_test_all_parameters(
|
async def render_test_all_parameters(
|
||||||
datasette, columns, rows, sql, query_name, database, table, request, view_name, data
|
datasette, columns, rows, sql, query_name, database, table, request, view_name, data
|
||||||
):
|
):
|
||||||
|
|
@ -39,6 +59,10 @@ def render_test_no_parameters():
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def register_output_renderer(datasette):
|
def register_output_renderer(datasette):
|
||||||
return [
|
return [
|
||||||
{"extension": "testall", "render": render_test_all_parameters},
|
{
|
||||||
|
"extension": "testall",
|
||||||
|
"render": render_test_all_parameters,
|
||||||
|
"can_render": can_render,
|
||||||
|
},
|
||||||
{"extension": "testnone", "callback": render_test_no_parameters},
|
{"extension": "testnone", "callback": render_test_no_parameters},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from .fixtures import (
|
||||||
from datasette.app import Datasette
|
from datasette.app import Datasette
|
||||||
from datasette import cli
|
from datasette import cli
|
||||||
from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
|
from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
|
||||||
from datasette.utils import sqlite3
|
from datasette.utils import sqlite3, CustomRow
|
||||||
from jinja2.environment import Template
|
from jinja2.environment import Template
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
@ -411,6 +411,41 @@ def test_register_output_renderer_custom_headers(app_client):
|
||||||
assert "2" == response.headers["x-gosh"]
|
assert "2" == response.headers["x-gosh"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_output_renderer_can_render(app_client):
|
||||||
|
response = app_client.get("/fixtures/facetable?_no_can_render=1")
|
||||||
|
assert response.status == 200
|
||||||
|
links = (
|
||||||
|
Soup(response.body, "html.parser")
|
||||||
|
.find("p", {"class": "export-links"})
|
||||||
|
.findAll("a")
|
||||||
|
)
|
||||||
|
actual = [l["href"].split("/")[-1] for l in links]
|
||||||
|
# Should not be present because we sent ?_no_can_render=1
|
||||||
|
assert "facetable.testall?_labels=on" not in actual
|
||||||
|
# Check that it was passed the values we expected
|
||||||
|
assert hasattr(app_client.ds, "_can_render_saw")
|
||||||
|
assert {
|
||||||
|
"datasette": app_client.ds,
|
||||||
|
"columns": [
|
||||||
|
"pk",
|
||||||
|
"created",
|
||||||
|
"planet_int",
|
||||||
|
"on_earth",
|
||||||
|
"state",
|
||||||
|
"city_id",
|
||||||
|
"neighborhood",
|
||||||
|
"tags",
|
||||||
|
"complex_array",
|
||||||
|
"distinct_some_null",
|
||||||
|
],
|
||||||
|
"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",
|
||||||
|
"view_name": "table",
|
||||||
|
}.items() <= app_client.ds._can_render_saw.items()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_prepare_jinja2_environment(app_client):
|
async def test_prepare_jinja2_environment(app_client):
|
||||||
template = app_client.ds.jinja_env.from_string(
|
template = app_client.ds.jinja_env.from_string(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue