Fix blacken-docs errors and warnings, refs #1718

api-extras
Simon Willison 2022-04-24 08:51:09 -07:00
rodzic 36573638b0
commit 92b26673d8
4 zmienionych plików z 289 dodań i 141 usunięć

Wyświetl plik

@ -381,11 +381,10 @@ Authentication plugins can set signed ``ds_actor`` cookies themselves like so:
.. code-block:: python .. code-block:: python
response = Response.redirect("/") response = Response.redirect("/")
response.set_cookie("ds_actor", datasette.sign({ response.set_cookie(
"a": { "ds_actor",
"id": "cleopaws" datasette.sign({"a": {"id": "cleopaws"}}, "actor"),
} )
}, "actor"))
Note that you need to pass ``"actor"`` as the namespace to :ref:`datasette_sign`. Note that you need to pass ``"actor"`` as the namespace to :ref:`datasette_sign`.
@ -412,12 +411,16 @@ To include an expiry, add a ``"e"`` key to the cookie value containing a `base62
expires_at = int(time.time()) + (24 * 60 * 60) expires_at = int(time.time()) + (24 * 60 * 60)
response = Response.redirect("/") response = Response.redirect("/")
response.set_cookie("ds_actor", datasette.sign({ response.set_cookie(
"a": { "ds_actor",
"id": "cleopaws" datasette.sign(
}, {
"e": baseconv.base62.encode(expires_at), "a": {"id": "cleopaws"},
}, "actor")) "e": baseconv.base62.encode(expires_at),
},
"actor",
),
)
The resulting cookie will encode data that looks something like this: The resulting cookie will encode data that looks something like this:

Wyświetl plik

@ -70,10 +70,10 @@ And a class method that can be used to create fake request objects for use in te
from datasette import Request from datasette import Request
from pprint import pprint from pprint import pprint
request = Request.fake("/fixtures/facetable/", url_vars={ request = Request.fake(
"database": "fixtures", "/fixtures/facetable/",
"table": "facetable" url_vars={"database": "fixtures", "table": "facetable"},
}) )
pprint(request.scope) pprint(request.scope)
This outputs:: This outputs::
@ -146,7 +146,7 @@ For example:
response = Response( response = Response(
"<xml>This is XML</xml>", "<xml>This is XML</xml>",
content_type="application/xml; charset=utf-8" content_type="application/xml; charset=utf-8",
) )
The quickest way to create responses is using the ``Response.text(...)``, ``Response.html(...)``, ``Response.json(...)`` or ``Response.redirect(...)`` helper methods: The quickest way to create responses is using the ``Response.text(...)``, ``Response.html(...)``, ``Response.json(...)`` or ``Response.redirect(...)`` helper methods:
@ -157,9 +157,13 @@ The quickest way to create responses is using the ``Response.text(...)``, ``Resp
html_response = Response.html("This is HTML") html_response = Response.html("This is HTML")
json_response = Response.json({"this_is": "json"}) json_response = Response.json({"this_is": "json"})
text_response = Response.text("This will become utf-8 encoded text") text_response = Response.text(
"This will become utf-8 encoded text"
)
# Redirects are served as 302, unless you pass status=301: # Redirects are served as 302, unless you pass status=301:
redirect_response = Response.redirect("https://latest.datasette.io/") redirect_response = Response.redirect(
"https://latest.datasette.io/"
)
Each of these responses will use the correct corresponding content-type - ``text/html; charset=utf-8``, ``application/json; charset=utf-8`` or ``text/plain; charset=utf-8`` respectively. Each of these responses will use the correct corresponding content-type - ``text/html; charset=utf-8``, ``application/json; charset=utf-8`` or ``text/plain; charset=utf-8`` respectively.
@ -207,13 +211,17 @@ To set cookies on the response, use the ``response.set_cookie(...)`` method. The
httponly=False, httponly=False,
samesite="lax", samesite="lax",
): ):
...
You can use this with :ref:`datasette.sign() <datasette_sign>` to set signed cookies. Here's how you would set the :ref:`ds_actor cookie <authentication_ds_actor>` for use with Datasette :ref:`authentication <authentication>`: You can use this with :ref:`datasette.sign() <datasette_sign>` to set signed cookies. Here's how you would set the :ref:`ds_actor cookie <authentication_ds_actor>` for use with Datasette :ref:`authentication <authentication>`:
.. code-block:: python .. code-block:: python
response = Response.redirect("/") response = Response.redirect("/")
response.set_cookie("ds_actor", datasette.sign({"a": {"id": "cleopaws"}}, "actor")) response.set_cookie(
"ds_actor",
datasette.sign({"a": {"id": "cleopaws"}}, "actor"),
)
return response return response
.. _internals_datasette: .. _internals_datasette:
@ -236,13 +244,16 @@ You can create your own instance of this - for example to help write tests for a
datasette = Datasette(files=["/path/to/my-database.db"]) datasette = Datasette(files=["/path/to/my-database.db"])
# Pass metadata as a JSON dictionary like this # Pass metadata as a JSON dictionary like this
datasette = Datasette(files=["/path/to/my-database.db"], metadata={ datasette = Datasette(
"databases": { files=["/path/to/my-database.db"],
"my-database": { metadata={
"description": "This is my database" "databases": {
"my-database": {
"description": "This is my database"
}
} }
} },
}) )
Constructor parameters include: Constructor parameters include:
@ -345,7 +356,7 @@ This is useful when you need to check multiple permissions at once. For example,
("view-table", (database, table)), ("view-table", (database, table)),
("view-database", database), ("view-database", database),
"view-instance", "view-instance",
] ],
) )
.. _datasette_check_visibilty: .. _datasette_check_visibilty:
@ -406,11 +417,13 @@ The ``db`` parameter should be an instance of the ``datasette.database.Database`
from datasette.database import Database from datasette.database import Database
datasette.add_database(Database( datasette.add_database(
datasette, Database(
path="path/to/my-new-database.db", datasette,
is_mutable=True path="path/to/my-new-database.db",
)) is_mutable=True,
)
)
This will add a mutable database and serve it at ``/my-new-database``. This will add a mutable database and serve it at ``/my-new-database``.
@ -418,8 +431,12 @@ This will add a mutable database and serve it at ``/my-new-database``.
.. code-block:: python .. code-block:: python
db = datasette.add_database(Database(datasette, memory_name="statistics")) db = datasette.add_database(
await db.execute_write("CREATE TABLE foo(id integer primary key)") Database(datasette, memory_name="statistics")
)
await db.execute_write(
"CREATE TABLE foo(id integer primary key)"
)
.. _datasette_add_memory_database: .. _datasette_add_memory_database:
@ -438,10 +455,9 @@ This is a shortcut for the following:
from datasette.database import Database from datasette.database import Database
datasette.add_database(Database( datasette.add_database(
datasette, Database(datasette, memory_name="statistics")
memory_name="statistics" )
))
Using either of these pattern will result in the in-memory database being served at ``/statistics``. Using either of these pattern will result in the in-memory database being served at ``/statistics``.
@ -516,7 +532,9 @@ Returns the absolute URL for the given path, including the protocol and host. Fo
.. code-block:: python .. code-block:: python
absolute_url = datasette.absolute_url(request, "/dbname/table.json") absolute_url = datasette.absolute_url(
request, "/dbname/table.json"
)
# Would return "http://localhost:8001/dbname/table.json" # Would return "http://localhost:8001/dbname/table.json"
The current request object is used to determine the hostname and protocol that should be used for the returned URL. The :ref:`setting_force_https_urls` configuration setting is taken into account. The current request object is used to determine the hostname and protocol that should be used for the returned URL. The :ref:`setting_force_https_urls` configuration setting is taken into account.
@ -578,7 +596,9 @@ These methods can be used with :ref:`internals_datasette_urls` - for example:
table_json = ( table_json = (
await datasette.client.get( await datasette.client.get(
datasette.urls.table("fixtures", "facetable", format="json") datasette.urls.table(
"fixtures", "facetable", format="json"
)
) )
).json() ).json()
@ -754,6 +774,7 @@ Example usage:
"select sqlite_version()" "select sqlite_version()"
).fetchall()[0][0] ).fetchall()[0][0]
version = await db.execute_fn(get_version) version = await db.execute_fn(get_version)
.. _database_execute_write: .. _database_execute_write:
@ -789,7 +810,7 @@ Like ``execute_write()`` but uses the ``sqlite3`` `conn.executemany() <https://d
await db.execute_write_many( await db.execute_write_many(
"insert into characters (id, name) values (?, ?)", "insert into characters (id, name) values (?, ?)",
[(1, "Melanie"), (2, "Selma"), (2, "Viktor")] [(1, "Melanie"), (2, "Selma"), (2, "Viktor")],
) )
.. _database_execute_write_fn: .. _database_execute_write_fn:
@ -811,10 +832,15 @@ For example:
def delete_and_return_count(conn): def delete_and_return_count(conn):
conn.execute("delete from some_table where id > 5") conn.execute("delete from some_table where id > 5")
return conn.execute("select count(*) from some_table").fetchone()[0] return conn.execute(
"select count(*) from some_table"
).fetchone()[0]
try: try:
num_rows_left = await database.execute_write_fn(delete_and_return_count) num_rows_left = await database.execute_write_fn(
delete_and_return_count
)
except Exception as e: except Exception as e:
print("An error occurred:", e) print("An error occurred:", e)
@ -1021,6 +1047,7 @@ This example uses trace to record the start, end and duration of any HTTP GET re
from datasette.tracer import trace from datasette.tracer import trace
import httpx import httpx
async def fetch_url(url): async def fetch_url(url):
with trace("fetch-url", url=url): with trace("fetch-url", url=url):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -1051,9 +1078,9 @@ This example uses the :ref:`register_routes() <plugin_register_routes>` plugin h
from datasette import hookimpl from datasette import hookimpl
from datasette import tracer from datasette import tracer
@hookimpl @hookimpl
def register_routes(): def register_routes():
async def parallel_queries(datasette): async def parallel_queries(datasette):
db = datasette.get_database() db = datasette.get_database()
with tracer.trace_child_tasks(): with tracer.trace_child_tasks():
@ -1061,7 +1088,12 @@ This example uses the :ref:`register_routes() <plugin_register_routes>` plugin h
db.execute("select 1"), db.execute("select 1"),
db.execute("select 2"), db.execute("select 2"),
) )
return Response.json({"one": one.single_value(), "two": two.single_value()}) return Response.json(
{
"one": one.single_value(),
"two": two.single_value(),
}
)
return [ return [
(r"/parallel-queries$", parallel_queries), (r"/parallel-queries$", parallel_queries),

Wyświetl plik

@ -446,7 +446,7 @@ Most of the HTML pages served by Datasette provide a mechanism for discovering t
You can find this near the top of the source code of those pages, looking like this: You can find this near the top of the source code of those pages, looking like this:
.. code-block:: python .. code-block:: html
<link rel="alternate" <link rel="alternate"
type="application/json+datasette" type="application/json+datasette"

Wyświetl plik

@ -44,9 +44,12 @@ aggregates and collations. For example:
from datasette import hookimpl from datasette import hookimpl
import random import random
@hookimpl @hookimpl
def prepare_connection(conn): def prepare_connection(conn):
conn.create_function('random_integer', 2, random.randint) conn.create_function(
"random_integer", 2, random.randint
)
This registers a SQL function called ``random_integer`` which takes two This registers a SQL function called ``random_integer`` which takes two
arguments and can be called like this:: arguments and can be called like this::
@ -72,9 +75,10 @@ example:
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def prepare_jinja2_environment(env): def prepare_jinja2_environment(env):
env.filters['uppercase'] = lambda u: u.upper() env.filters["uppercase"] = lambda u: u.upper()
You can now use this filter in your custom templates like so:: You can now use this filter in your custom templates like so::
@ -127,9 +131,7 @@ Here's an example plugin that adds a ``"user_agent"`` variable to the template c
@hookimpl @hookimpl
def extra_template_vars(request): def extra_template_vars(request):
return { return {"user_agent": request.headers.get("user-agent")}
"user_agent": request.headers.get("user-agent")
}
This example returns an awaitable function which adds a list of ``hidden_table_names`` to the context: This example returns an awaitable function which adds a list of ``hidden_table_names`` to the context:
@ -140,9 +142,12 @@ This example returns an awaitable function which adds a list of ``hidden_table_n
async def hidden_table_names(): async def hidden_table_names():
if database: if database:
db = datasette.databases[database] db = datasette.databases[database]
return {"hidden_table_names": await db.hidden_table_names()} return {
"hidden_table_names": await db.hidden_table_names()
}
else: else:
return {} return {}
return hidden_table_names return hidden_table_names
And here's an example which adds a ``sql_first(sql_query)`` function which executes a SQL statement and returns the first column of the first row of results: And here's an example which adds a ``sql_first(sql_query)`` function which executes a SQL statement and returns the first column of the first row of results:
@ -152,8 +157,15 @@ And here's an example which adds a ``sql_first(sql_query)`` function which execu
@hookimpl @hookimpl
def extra_template_vars(datasette, database): def extra_template_vars(datasette, database):
async def sql_first(sql, dbname=None): async def sql_first(sql, dbname=None):
dbname = dbname or database or next(iter(datasette.databases.keys())) dbname = (
return (await datasette.execute(dbname, sql)).rows[0][0] dbname
or database
or next(iter(datasette.databases.keys()))
)
return (await datasette.execute(dbname, sql)).rows[
0
][0]
return {"sql_first": sql_first} return {"sql_first": sql_first}
You can then use the new function in a template like so:: You can then use the new function in a template like so::
@ -178,6 +190,7 @@ This can be a list of URLs:
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def extra_css_urls(): def extra_css_urls():
return [ return [
@ -191,10 +204,12 @@ Or a list of dictionaries defining both a URL and an
@hookimpl @hookimpl
def extra_css_urls(): def extra_css_urls():
return [{ return [
"url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css", {
"sri": "sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4", "url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css",
}] "sri": "sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4",
}
]
This function can also return an awaitable function, useful if it needs to run any async code: This function can also return an awaitable function, useful if it needs to run any async code:
@ -204,7 +219,9 @@ This function can also return an awaitable function, useful if it needs to run a
def extra_css_urls(datasette): def extra_css_urls(datasette):
async def inner(): async def inner():
db = datasette.get_database() db = datasette.get_database()
results = await db.execute("select url from css_files") results = await db.execute(
"select url from css_files"
)
return [r[0] for r in results] return [r[0] for r in results]
return inner return inner
@ -225,12 +242,15 @@ return a list of URLs, a list of dictionaries or an awaitable function that retu
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def extra_js_urls(): def extra_js_urls():
return [{ return [
"url": "https://code.jquery.com/jquery-3.3.1.slim.min.js", {
"sri": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo", "url": "https://code.jquery.com/jquery-3.3.1.slim.min.js",
}] "sri": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
}
]
You can also return URLs to files from your plugin's ``static/`` directory, if You can also return URLs to files from your plugin's ``static/`` directory, if
you have one: you have one:
@ -239,9 +259,7 @@ you have one:
@hookimpl @hookimpl
def extra_js_urls(): def extra_js_urls():
return [ return ["/-/static-plugins/your-plugin/app.js"]
"/-/static-plugins/your-plugin/app.js"
]
Note that `your-plugin` here should be the hyphenated plugin name - the name that is displayed in the list on the `/-/plugins` debug page. Note that `your-plugin` here should be the hyphenated plugin name - the name that is displayed in the list on the `/-/plugins` debug page.
@ -251,9 +269,11 @@ If your code uses `JavaScript modules <https://developer.mozilla.org/en-US/docs/
@hookimpl @hookimpl
def extra_js_urls(): def extra_js_urls():
return [{ return [
"url": "/-/static-plugins/your-plugin/app.js", {
"module": True "url": "/-/static-plugins/your-plugin/app.js",
"module": True,
}
] ]
Examples: `datasette-cluster-map <https://datasette.io/plugins/datasette-cluster-map>`_, `datasette-vega <https://datasette.io/plugins/datasette-vega>`_ Examples: `datasette-cluster-map <https://datasette.io/plugins/datasette-cluster-map>`_, `datasette-vega <https://datasette.io/plugins/datasette-vega>`_
@ -281,7 +301,7 @@ Use a dictionary if you want to specify that the code should be placed in a ``<s
def extra_body_script(): def extra_body_script():
return { return {
"module": True, "module": True,
"script": "console.log('Your JavaScript goes here...')" "script": "console.log('Your JavaScript goes here...')",
} }
This will add the following to the end of your page: This will add the following to the end of your page:
@ -311,7 +331,9 @@ Let's say you want to build a plugin that adds a ``datasette publish my_hosting_
.. code-block:: python .. code-block:: python
from datasette import hookimpl from datasette import hookimpl
from datasette.publish.common import add_common_publish_arguments_and_options from datasette.publish.common import (
add_common_publish_arguments_and_options,
)
import click import click
@ -345,7 +367,7 @@ Let's say you want to build a plugin that adds a ``datasette publish my_hosting_
about_url, about_url,
api_key, api_key,
): ):
# Your implementation goes here ...
Examples: `datasette-publish-fly <https://datasette.io/plugins/datasette-publish-fly>`_, `datasette-publish-vercel <https://datasette.io/plugins/datasette-publish-vercel>`_ Examples: `datasette-publish-fly <https://datasette.io/plugins/datasette-publish-fly>`_, `datasette-publish-vercel <https://datasette.io/plugins/datasette-publish-vercel>`_
@ -400,7 +422,9 @@ If the value matches that pattern, the plugin returns an HTML link element:
if not isinstance(value, str): if not isinstance(value, str):
return None return None
stripped = value.strip() stripped = value.strip()
if not stripped.startswith("{") and stripped.endswith("}"): if not stripped.startswith("{") and stripped.endswith(
"}"
):
return None return None
try: try:
data = json.loads(value) data = json.loads(value)
@ -412,14 +436,18 @@ If the value matches that pattern, the plugin returns an HTML link element:
return None return None
href = data["href"] href = data["href"]
if not ( if not (
href.startswith("/") or href.startswith("http://") href.startswith("/")
or href.startswith("http://")
or href.startswith("https://") or href.startswith("https://")
): ):
return None return None
return markupsafe.Markup('<a href="{href}">{label}</a>'.format( return markupsafe.Markup(
href=markupsafe.escape(data["href"]), '<a href="{href}">{label}</a>'.format(
label=markupsafe.escape(data["label"] or "") or "&nbsp;" href=markupsafe.escape(data["href"]),
)) label=markupsafe.escape(data["label"] or "")
or "&nbsp;",
)
)
Examples: `datasette-render-binary <https://datasette.io/plugins/datasette-render-binary>`_, `datasette-render-markdown <https://datasette.io/plugins/datasette-render-markdown>`__, `datasette-json-html <https://datasette.io/plugins/datasette-json-html>`__ Examples: `datasette-render-binary <https://datasette.io/plugins/datasette-render-binary>`_, `datasette-render-markdown <https://datasette.io/plugins/datasette-render-markdown>`__, `datasette-json-html <https://datasette.io/plugins/datasette-json-html>`__
@ -516,7 +544,7 @@ Here is a more complex example:
return Response( return Response(
"\n".join(lines), "\n".join(lines),
content_type="text/plain; charset=utf-8", content_type="text/plain; charset=utf-8",
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``: 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``:
@ -524,7 +552,11 @@ And here is an example ``can_render`` function which returns ``True`` only if th
.. code-block:: python .. code-block:: python
def can_render_demo(columns): def can_render_demo(columns):
return {"atom_id", "atom_title", "atom_updated"}.issubset(columns) return {
"atom_id",
"atom_title",
"atom_updated",
}.issubset(columns)
Examples: `datasette-atom <https://datasette.io/plugins/datasette-atom>`_, `datasette-ics <https://datasette.io/plugins/datasette-ics>`_, `datasette-geojson <https://datasette.io/plugins/datasette-geojson>`__ Examples: `datasette-atom <https://datasette.io/plugins/datasette-atom>`_, `datasette-ics <https://datasette.io/plugins/datasette-ics>`_, `datasette-geojson <https://datasette.io/plugins/datasette-geojson>`__
@ -548,16 +580,14 @@ Return a list of ``(regex, view_function)`` pairs, something like this:
async def hello_from(request): async def hello_from(request):
name = request.url_vars["name"] name = request.url_vars["name"]
return Response.html("Hello from {}".format( return Response.html(
html.escape(name) "Hello from {}".format(html.escape(name))
)) )
@hookimpl @hookimpl
def register_routes(): def register_routes():
return [ return [(r"^/hello-from/(?P<name>.*)$", hello_from)]
(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 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.
@ -606,10 +636,13 @@ This example registers a new ``datasette verify file1.db file2.db`` command that
import click import click
import sqlite3 import sqlite3
@hookimpl @hookimpl
def register_commands(cli): def register_commands(cli):
@cli.command() @cli.command()
@click.argument("files", type=click.Path(exists=True), nargs=-1) @click.argument(
"files", type=click.Path(exists=True), nargs=-1
)
def verify(files): def verify(files):
"Verify that files can be opened by Datasette" "Verify that files can be opened by Datasette"
for file in files: for file in files:
@ -617,7 +650,9 @@ This example registers a new ``datasette verify file1.db file2.db`` command that
try: try:
conn.execute("select * from sqlite_master") conn.execute("select * from sqlite_master")
except sqlite3.DatabaseError: except sqlite3.DatabaseError:
raise click.ClickException("Invalid database: {}".format(file)) raise click.ClickException(
"Invalid database: {}".format(file)
)
The new command can then be executed like so:: The new command can then be executed like so::
@ -656,15 +691,18 @@ Each Facet subclass implements a new type of facet operation. The class should l
async def suggest(self): async def suggest(self):
# Use self.sql and self.params to suggest some facets # Use self.sql and self.params to suggest some facets
suggested_facets = [] suggested_facets = []
suggested_facets.append({ suggested_facets.append(
"name": column, # Or other unique name {
# Construct the URL that will enable this facet: "name": column, # Or other unique name
"toggle_url": self.ds.absolute_url( # Construct the URL that will enable this facet:
self.request, path_with_added_args( "toggle_url": self.ds.absolute_url(
self.request, {"_facet": column} self.request,
) path_with_added_args(
), self.request, {"_facet": column}
}) ),
),
}
)
return suggested_facets return suggested_facets
async def facet_results(self): async def facet_results(self):
@ -678,18 +716,25 @@ Each Facet subclass implements a new type of facet operation. The class should l
try: try:
facet_results_values = [] facet_results_values = []
# More calculations... # More calculations...
facet_results_values.append({ facet_results_values.append(
"value": value, {
"label": label, "value": value,
"count": count, "label": label,
"toggle_url": self.ds.absolute_url(self.request, toggle_path), "count": count,
"selected": selected, "toggle_url": self.ds.absolute_url(
}) self.request, toggle_path
facet_results.append({ ),
"name": column, "selected": selected,
"results": facet_results_values, }
"truncated": len(facet_rows_results) > facet_size, )
}) facet_results.append(
{
"name": column,
"results": facet_results_values,
"truncated": len(facet_rows_results)
> facet_size,
}
)
except QueryInterrupted: except QueryInterrupted:
facets_timed_out.append(column) facets_timed_out.append(column)
@ -728,21 +773,33 @@ This example plugin adds a ``x-databases`` HTTP header listing the currently att
def asgi_wrapper(datasette): def asgi_wrapper(datasette):
def wrap_with_databases_header(app): def wrap_with_databases_header(app):
@wraps(app) @wraps(app)
async def add_x_databases_header(scope, receive, send): async def add_x_databases_header(
scope, receive, send
):
async def wrapped_send(event): async def wrapped_send(event):
if event["type"] == "http.response.start": if event["type"] == "http.response.start":
original_headers = event.get("headers") or [] original_headers = (
event.get("headers") or []
)
event = { event = {
"type": event["type"], "type": event["type"],
"status": event["status"], "status": event["status"],
"headers": original_headers + [ "headers": original_headers
[b"x-databases", + [
", ".join(datasette.databases.keys()).encode("utf-8")] [
b"x-databases",
", ".join(
datasette.databases.keys()
).encode("utf-8"),
]
], ],
} }
await send(event) await send(event)
await app(scope, receive, wrapped_send) await app(scope, receive, wrapped_send)
return add_x_databases_header return add_x_databases_header
return wrap_with_databases_header return wrap_with_databases_header
Examples: `datasette-cors <https://datasette.io/plugins/datasette-cors>`__, `datasette-pyinstrument <https://datasette.io/plugins/datasette-pyinstrument>`__ Examples: `datasette-cors <https://datasette.io/plugins/datasette-cors>`__, `datasette-pyinstrument <https://datasette.io/plugins/datasette-pyinstrument>`__
@ -759,7 +816,9 @@ This hook fires when the Datasette application server first starts up. You can i
@hookimpl @hookimpl
def startup(datasette): def startup(datasette):
config = datasette.plugin_config("my-plugin") or {} config = datasette.plugin_config("my-plugin") or {}
assert "required-setting" in config, "my-plugin requires setting required-setting" assert (
"required-setting" in config
), "my-plugin requires setting required-setting"
Or you can return an async function which will be awaited on startup. Use this option if you need to make any database queries: Or you can return an async function which will be awaited on startup. Use this option if you need to make any database queries:
@ -770,9 +829,12 @@ Or you can return an async function which will be awaited on startup. Use this o
async def inner(): async def inner():
db = datasette.get_database() db = datasette.get_database()
if "my_table" not in await db.table_names(): if "my_table" not in await db.table_names():
await db.execute_write(""" await db.execute_write(
"""
create table my_table (mycol text) create table my_table (mycol text)
""") """
)
return inner return inner
Potential use-cases: Potential use-cases:
@ -815,6 +877,7 @@ Ues this hook to return a dictionary of additional :ref:`canned query <canned_qu
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def canned_queries(datasette, database): def canned_queries(datasette, database):
if database == "mydb": if database == "mydb":
@ -830,15 +893,20 @@ The hook can alternatively return an awaitable function that returns a list. Her
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def canned_queries(datasette, database): def canned_queries(datasette, database):
async def inner(): async def inner():
db = datasette.get_database(database) db = datasette.get_database(database)
if await db.table_exists("saved_queries"): if await db.table_exists("saved_queries"):
results = await db.execute("select name, sql from saved_queries") results = await db.execute(
return {result["name"]: { "select name, sql from saved_queries"
"sql": result["sql"] )
} for result in results} return {
result["name"]: {"sql": result["sql"]}
for result in results
}
return inner return inner
The actor parameter can be used to include the currently authenticated actor in your decision. Here's an example that returns saved queries that were saved by that actor: The actor parameter can be used to include the currently authenticated actor in your decision. Here's an example that returns saved queries that were saved by that actor:
@ -847,19 +915,23 @@ The actor parameter can be used to include the currently authenticated actor in
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def canned_queries(datasette, database, actor): def canned_queries(datasette, database, actor):
async def inner(): async def inner():
db = datasette.get_database(database) db = datasette.get_database(database)
if actor is not None and await db.table_exists("saved_queries"): if actor is not None and await db.table_exists(
"saved_queries"
):
results = await db.execute( results = await db.execute(
"select name, sql from saved_queries where actor_id = :id", { "select name, sql from saved_queries where actor_id = :id",
"id": actor["id"] {"id": actor["id"]},
}
) )
return {result["name"]: { return {
"sql": result["sql"] result["name"]: {"sql": result["sql"]}
} for result in results} for result in results
}
return inner return inner
Example: `datasette-saved-queries <https://datasette.io/plugins/datasette-saved-queries>`__ Example: `datasette-saved-queries <https://datasette.io/plugins/datasette-saved-queries>`__
@ -888,9 +960,12 @@ Here's an example that authenticates the actor based on an incoming API key:
SECRET_KEY = "this-is-a-secret" SECRET_KEY = "this-is-a-secret"
@hookimpl @hookimpl
def actor_from_request(datasette, request): def actor_from_request(datasette, request):
authorization = request.headers.get("authorization") or "" authorization = (
request.headers.get("authorization") or ""
)
expected = "Bearer {}".format(SECRET_KEY) expected = "Bearer {}".format(SECRET_KEY)
if secrets.compare_digest(authorization, expected): if secrets.compare_digest(authorization, expected):
@ -906,6 +981,7 @@ Instead of returning a dictionary, this function can return an awaitable functio
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def actor_from_request(datasette, request): def actor_from_request(datasette, request):
async def inner(): async def inner():
@ -914,7 +990,8 @@ Instead of returning a dictionary, this function can return an awaitable functio
return None return None
# Look up ?_token=xxx in sessions table # Look up ?_token=xxx in sessions table
result = await datasette.get_database().execute( result = await datasette.get_database().execute(
"select count(*) from sessions where token = ?", [token] "select count(*) from sessions where token = ?",
[token],
) )
if result.first()[0]: if result.first()[0]:
return {"token": token} return {"token": token}
@ -952,7 +1029,7 @@ The hook should return an instance of ``datasette.filters.FilterArguments`` whic
where_clauses=["id > :max_id"], where_clauses=["id > :max_id"],
params={"max_id": 5}, params={"max_id": 5},
human_descriptions=["max_id is greater than 5"], human_descriptions=["max_id is greater than 5"],
extra_context={} extra_context={},
) )
The arguments to the ``FilterArguments`` class constructor are as follows: The arguments to the ``FilterArguments`` class constructor are as follows:
@ -973,10 +1050,13 @@ This example plugin causes 0 results to be returned if ``?_nothing=1`` is added
from datasette import hookimpl from datasette import hookimpl
from datasette.filters import FilterArguments from datasette.filters import FilterArguments
@hookimpl @hookimpl
def filters_from_request(self, request): def filters_from_request(self, request):
if request.args.get("_nothing"): if request.args.get("_nothing"):
return FilterArguments(["1 = 0"], human_descriptions=["NOTHING"]) return FilterArguments(
["1 = 0"], human_descriptions=["NOTHING"]
)
Example: `datasette-leaflet-freedraw <https://datasette.io/plugins/datasette-leaflet-freedraw>`_ Example: `datasette-leaflet-freedraw <https://datasette.io/plugins/datasette-leaflet-freedraw>`_
@ -1006,6 +1086,7 @@ Here's an example plugin which randomly selects if a permission should be allowe
from datasette import hookimpl from datasette import hookimpl
import random import random
@hookimpl @hookimpl
def permission_allowed(action): def permission_allowed(action):
if action != "view-instance": if action != "view-instance":
@ -1024,11 +1105,16 @@ Here's an example that allows users to view the ``admin_log`` table only if thei
async def inner(): async def inner():
if action == "execute-sql" and resource == "staff": if action == "execute-sql" and resource == "staff":
return False return False
if action == "view-table" and resource == ("staff", "admin_log"): if action == "view-table" and resource == (
"staff",
"admin_log",
):
if not actor: if not actor:
return False return False
user_id = actor["id"] user_id = actor["id"]
return await datasette.get_database("staff").execute( return await datasette.get_database(
"staff"
).execute(
"select count(*) from admin_users where user_id = :user_id", "select count(*) from admin_users where user_id = :user_id",
{"user_id": user_id}, {"user_id": user_id},
) )
@ -1059,18 +1145,21 @@ This example registers two new magic parameters: ``:_request_http_version`` retu
from uuid import uuid4 from uuid import uuid4
def uuid(key, request): def uuid(key, request):
if key == "new": if key == "new":
return str(uuid4()) return str(uuid4())
else: else:
raise KeyError raise KeyError
def request(key, request): def request(key, request):
if key == "http_version": if key == "http_version":
return request.scope["http_version"] return request.scope["http_version"]
else: else:
raise KeyError raise KeyError
@hookimpl @hookimpl
def register_magic_parameters(datasette): def register_magic_parameters(datasette):
return [ return [
@ -1103,9 +1192,12 @@ This example returns a redirect to a ``/-/login`` page:
from datasette import hookimpl from datasette import hookimpl
from urllib.parse import urlencode from urllib.parse import urlencode
@hookimpl @hookimpl
def forbidden(request, message): def forbidden(request, message):
return Response.redirect("/-/login?=" + urlencode({"message": message})) return Response.redirect(
"/-/login?=" + urlencode({"message": message})
)
The function can alternatively return an awaitable function if it needs to make any asynchronous method calls. This example renders a template: The function can alternatively return an awaitable function if it needs to make any asynchronous method calls. This example renders a template:
@ -1114,10 +1206,15 @@ The function can alternatively return an awaitable function if it needs to make
from datasette import hookimpl from datasette import hookimpl
from datasette.utils.asgi import Response from datasette.utils.asgi import Response
@hookimpl @hookimpl
def forbidden(datasette): def forbidden(datasette):
async def inner(): async def inner():
return Response.html(await datasette.render_template("forbidden.html")) return Response.html(
await datasette.render_template(
"forbidden.html"
)
)
return inner return inner
@ -1147,11 +1244,17 @@ This example adds a new menu item but only if the signed in user is ``"root"``:
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def menu_links(datasette, actor): def menu_links(datasette, actor):
if actor and actor.get("id") == "root": if actor and actor.get("id") == "root":
return [ return [
{"href": datasette.urls.path("/-/edit-schema"), "label": "Edit schema"}, {
"href": datasette.urls.path(
"/-/edit-schema"
),
"label": "Edit schema",
},
] ]
Using :ref:`internals_datasette_urls` here ensures that links in the menu will take the :ref:`setting_base_url` setting into account. Using :ref:`internals_datasette_urls` here ensures that links in the menu will take the :ref:`setting_base_url` setting into account.
@ -1188,13 +1291,20 @@ This example adds a new table action if the signed in user is ``"root"``:
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def table_actions(datasette, actor): def table_actions(datasette, actor):
if actor and actor.get("id") == "root": if actor and actor.get("id") == "root":
return [{ return [
"href": datasette.urls.path("/-/edit-schema/{}/{}".format(database, table)), {
"label": "Edit schema for this table", "href": datasette.urls.path(
}] "/-/edit-schema/{}/{}".format(
database, table
)
),
"label": "Edit schema for this table",
}
]
Example: `datasette-graphql <https://datasette.io/plugins/datasette-graphql>`_ Example: `datasette-graphql <https://datasette.io/plugins/datasette-graphql>`_
@ -1238,6 +1348,7 @@ This example will disable CSRF protection for that specific URL path:
from datasette import hookimpl from datasette import hookimpl
@hookimpl @hookimpl
def skip_csrf(scope): def skip_csrf(scope):
return scope["path"] == "/submit-comment" return scope["path"] == "/submit-comment"
@ -1278,7 +1389,9 @@ This hook is responsible for returning a dictionary corresponding to Datasette :
"description": get_instance_description(datasette), "description": get_instance_description(datasette),
"databases": [], "databases": [],
} }
for db_name, db_data_dict in get_my_database_meta(datasette, database, table, key): for db_name, db_data_dict in get_my_database_meta(
datasette, database, table, key
):
metadata["databases"][db_name] = db_data_dict metadata["databases"][db_name] = db_data_dict
# whatever we return here will be merged with any other plugins using this hook and # whatever we return here will be merged with any other plugins using this hook and
# will be overwritten by a local metadata.yaml if one exists! # will be overwritten by a local metadata.yaml if one exists!