kopia lustrzana https://github.com/simonw/datasette
can_render mechanism for register_output_renderer, closes #770
rodzic
75cd432e5a
commit
5ab411c733
|
@ -228,7 +228,7 @@ class Datasette:
|
|||
if config_dir and (config_dir / "config.json").exists() and not config:
|
||||
config = json.load((config_dir / "config.json").open())
|
||||
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.executor = futures.ThreadPoolExecutor(
|
||||
max_workers=self.config("num_sql_threads")
|
||||
|
@ -574,7 +574,7 @@ class Datasette:
|
|||
def register_renderers(self):
|
||||
""" Register output renderers which output data in custom formats. """
|
||||
# Built-in renderers
|
||||
self.renderers["json"] = json_renderer
|
||||
self.renderers["json"] = (json_renderer, lambda: True)
|
||||
|
||||
# Hooks
|
||||
hook_renderers = []
|
||||
|
@ -588,8 +588,8 @@ class Datasette:
|
|||
for renderer in hook_renderers:
|
||||
self.renderers[renderer["extension"]] = (
|
||||
# It used to be called "callback" - remove this in Datasette 1.0
|
||||
renderer.get("render")
|
||||
or renderer["callback"]
|
||||
renderer.get("render") or renderer["callback"],
|
||||
renderer.get("can_render") or (lambda: True),
|
||||
)
|
||||
|
||||
async def render_template(
|
||||
|
|
|
@ -811,6 +811,10 @@ def call_with_supported_arguments(fn, **kwargs):
|
|||
call_with = []
|
||||
for parameter in parameters:
|
||||
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])
|
||||
return fn(*call_with)
|
||||
|
|
|
@ -389,7 +389,7 @@ class DataView(BaseView):
|
|||
# Dispatch request to the correct output format renderer
|
||||
# (CSV is not handled here due to streaming)
|
||||
result = call_with_supported_arguments(
|
||||
self.ds.renderers[_format],
|
||||
self.ds.renderers[_format][0],
|
||||
datasette=self.ds,
|
||||
columns=data.get("columns") or [],
|
||||
rows=data.get("rows") or [],
|
||||
|
@ -426,10 +426,27 @@ class DataView(BaseView):
|
|||
if data.get("expandable_columns"):
|
||||
url_labels_extra = {"_labels": "on"}
|
||||
|
||||
renderers = {
|
||||
key: path_with_format(request, key, {**url_labels_extra})
|
||||
for key in self.ds.renderers.keys()
|
||||
}
|
||||
renderers = {}
|
||||
for key, (_, can_render) in self.ds.renderers.items():
|
||||
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 = path_with_format(request, "csv", url_csv_args)
|
||||
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):
|
||||
return {
|
||||
"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`
|
||||
For accessing plugin configuration and executing queries.
|
||||
|
@ -798,7 +801,7 @@ A simple example of an output renderer callback function:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
def render_test():
|
||||
def render_demo():
|
||||
return {
|
||||
"body": "Hello World"
|
||||
}
|
||||
|
@ -807,7 +810,7 @@ Here is a more complex example:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
async def render_test(datasette, columns, rows):
|
||||
async def render_demo(datasette, columns, rows):
|
||||
db = next(iter(datasette.databases.values()))
|
||||
result = await db.execute("select sqlite_version()")
|
||||
first_row = " | ".join(columns)
|
||||
|
@ -821,6 +824,13 @@ Here is a more complex example:
|
|||
"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>`_
|
||||
|
||||
.. _plugin_register_facet_classes:
|
||||
|
|
|
@ -2,6 +2,26 @@ from datasette import hookimpl
|
|||
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(
|
||||
datasette, columns, rows, sql, query_name, database, table, request, view_name, data
|
||||
):
|
||||
|
@ -39,6 +59,10 @@ def render_test_no_parameters():
|
|||
@hookimpl
|
||||
def register_output_renderer(datasette):
|
||||
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},
|
||||
]
|
||||
|
|
|
@ -9,7 +9,7 @@ from .fixtures import (
|
|||
from datasette.app import Datasette
|
||||
from datasette import cli
|
||||
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
|
||||
import base64
|
||||
import json
|
||||
|
@ -411,6 +411,41 @@ def test_register_output_renderer_custom_headers(app_client):
|
|||
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
|
||||
async def test_prepare_jinja2_environment(app_client):
|
||||
template = app_client.ds.jinja_env.from_string(
|
||||
|
|
Ładowanie…
Reference in New Issue