diff --git a/docs/authentication.rst b/docs/authentication.rst
index 0d98cf82..24960733 100644
--- a/docs/authentication.rst
+++ b/docs/authentication.rst
@@ -381,11 +381,10 @@ Authentication plugins can set signed ``ds_actor`` cookies themselves like so:
.. code-block:: python
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"),
+ )
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)
response = Response.redirect("/")
- response.set_cookie("ds_actor", datasette.sign({
- "a": {
- "id": "cleopaws"
- },
- "e": baseconv.base62.encode(expires_at),
- }, "actor"))
+ response.set_cookie(
+ "ds_actor",
+ datasette.sign(
+ {
+ "a": {"id": "cleopaws"},
+ "e": baseconv.base62.encode(expires_at),
+ },
+ "actor",
+ ),
+ )
The resulting cookie will encode data that looks something like this:
diff --git a/docs/internals.rst b/docs/internals.rst
index 76e27e5f..aad608dc 100644
--- a/docs/internals.rst
+++ b/docs/internals.rst
@@ -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 pprint import pprint
- request = Request.fake("/fixtures/facetable/", url_vars={
- "database": "fixtures",
- "table": "facetable"
- })
+ request = Request.fake(
+ "/fixtures/facetable/",
+ url_vars={"database": "fixtures", "table": "facetable"},
+ )
pprint(request.scope)
This outputs::
@@ -146,7 +146,7 @@ For example:
response = Response(
"This is 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:
@@ -157,9 +157,13 @@ The quickest way to create responses is using the ``Response.text(...)``, ``Resp
html_response = Response.html("This is HTML")
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:
- 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.
@@ -207,13 +211,17 @@ To set cookies on the response, use the ``response.set_cookie(...)`` method. The
httponly=False,
samesite="lax",
):
+ ...
You can use this with :ref:`datasette.sign() ` to set signed cookies. Here's how you would set the :ref:`ds_actor cookie ` for use with Datasette :ref:`authentication `:
.. code-block:: python
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
.. _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"])
# Pass metadata as a JSON dictionary like this
- datasette = Datasette(files=["/path/to/my-database.db"], metadata={
- "databases": {
- "my-database": {
- "description": "This is my database"
+ datasette = Datasette(
+ files=["/path/to/my-database.db"],
+ metadata={
+ "databases": {
+ "my-database": {
+ "description": "This is my database"
+ }
}
- }
- })
+ },
+ )
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-database", database),
"view-instance",
- ]
+ ],
)
.. _datasette_check_visibilty:
@@ -406,11 +417,13 @@ The ``db`` parameter should be an instance of the ``datasette.database.Database`
from datasette.database import Database
- datasette.add_database(Database(
- datasette,
- path="path/to/my-new-database.db",
- is_mutable=True
- ))
+ datasette.add_database(
+ Database(
+ datasette,
+ path="path/to/my-new-database.db",
+ is_mutable=True,
+ )
+ )
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
- db = datasette.add_database(Database(datasette, memory_name="statistics"))
- await db.execute_write("CREATE TABLE foo(id integer primary key)")
+ db = datasette.add_database(
+ Database(datasette, memory_name="statistics")
+ )
+ await db.execute_write(
+ "CREATE TABLE foo(id integer primary key)"
+ )
.. _datasette_add_memory_database:
@@ -438,10 +455,9 @@ This is a shortcut for the following:
from datasette.database import Database
- datasette.add_database(Database(
- datasette,
- memory_name="statistics"
- ))
+ datasette.add_database(
+ Database(datasette, memory_name="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
- 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"
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 = (
await datasette.client.get(
- datasette.urls.table("fixtures", "facetable", format="json")
+ datasette.urls.table(
+ "fixtures", "facetable", format="json"
+ )
)
).json()
@@ -754,6 +774,7 @@ Example usage:
"select sqlite_version()"
).fetchall()[0][0]
+
version = await db.execute_fn(get_version)
.. _database_execute_write:
@@ -789,7 +810,7 @@ Like ``execute_write()`` but uses the ``sqlite3`` `conn.executemany() 5")
- return conn.execute("select count(*) from some_table").fetchone()[0]
+ return conn.execute(
+ "select count(*) from some_table"
+ ).fetchone()[0]
+
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:
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
import httpx
+
async def fetch_url(url):
with trace("fetch-url", url=url):
async with httpx.AsyncClient() as client:
@@ -1051,9 +1078,9 @@ This example uses the :ref:`register_routes() ` plugin h
from datasette import hookimpl
from datasette import tracer
+
@hookimpl
def register_routes():
-
async def parallel_queries(datasette):
db = datasette.get_database()
with tracer.trace_child_tasks():
@@ -1061,7 +1088,12 @@ This example uses the :ref:`register_routes() ` plugin h
db.execute("select 1"),
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 [
(r"/parallel-queries$", parallel_queries),
diff --git a/docs/json_api.rst b/docs/json_api.rst
index aa6fcdaa..d3fdb1e4 100644
--- a/docs/json_api.rst
+++ b/docs/json_api.rst
@@ -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:
-.. code-block:: python
+.. code-block:: html
`_, `datasette-vega `_
@@ -281,7 +301,7 @@ Use a dictionary if you want to specify that the code should be placed in a ```_, `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):
return None
stripped = value.strip()
- if not stripped.startswith("{") and stripped.endswith("}"):
+ if not stripped.startswith("{") and stripped.endswith(
+ "}"
+ ):
return None
try:
data = json.loads(value)
@@ -412,14 +436,18 @@ If the value matches that pattern, the plugin returns an HTML link element:
return None
href = data["href"]
if not (
- href.startswith("/") or href.startswith("http://")
+ href.startswith("/")
+ or href.startswith("http://")
or href.startswith("https://")
):
return None
- return markupsafe.Markup('{label}'.format(
- href=markupsafe.escape(data["href"]),
- label=markupsafe.escape(data["label"] or "") or " "
- ))
+ return markupsafe.Markup(
+ '{label}'.format(
+ href=markupsafe.escape(data["href"]),
+ label=markupsafe.escape(data["label"] or "")
+ or " ",
+ )
+ )
Examples: `datasette-render-binary `_, `datasette-render-markdown `__, `datasette-json-html `__
@@ -516,7 +544,7 @@ Here is a more complex example:
return Response(
"\n".join(lines),
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``:
@@ -524,7 +552,11 @@ And here is an example ``can_render`` function which returns ``True`` only if th
.. code-block:: python
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 `_, `datasette-ics `_, `datasette-geojson `__
@@ -548,16 +580,14 @@ Return a list of ``(regex, view_function)`` pairs, something like this:
async def hello_from(request):
name = request.url_vars["name"]
- return Response.html("Hello from {}".format(
- html.escape(name)
- ))
+ return Response.html(
+ "Hello from {}".format(html.escape(name))
+ )
@hookimpl
def register_routes():
- return [
- (r"^/hello-from/(?P.*)$", hello_from)
- ]
+ return [(r"^/hello-from/(?P.*)$", 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.
@@ -606,10 +636,13 @@ This example registers a new ``datasette verify file1.db file2.db`` command that
import click
import sqlite3
+
@hookimpl
def register_commands(cli):
@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):
"Verify that files can be opened by Datasette"
for file in files:
@@ -617,7 +650,9 @@ This example registers a new ``datasette verify file1.db file2.db`` command that
try:
conn.execute("select * from sqlite_master")
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::
@@ -656,15 +691,18 @@ Each Facet subclass implements a new type of facet operation. The class should l
async def suggest(self):
# Use self.sql and self.params to suggest some facets
suggested_facets = []
- suggested_facets.append({
- "name": column, # Or other unique name
- # Construct the URL that will enable this facet:
- "toggle_url": self.ds.absolute_url(
- self.request, path_with_added_args(
- self.request, {"_facet": column}
- )
- ),
- })
+ suggested_facets.append(
+ {
+ "name": column, # Or other unique name
+ # Construct the URL that will enable this facet:
+ "toggle_url": self.ds.absolute_url(
+ self.request,
+ path_with_added_args(
+ self.request, {"_facet": column}
+ ),
+ ),
+ }
+ )
return suggested_facets
async def facet_results(self):
@@ -678,18 +716,25 @@ Each Facet subclass implements a new type of facet operation. The class should l
try:
facet_results_values = []
# More calculations...
- facet_results_values.append({
- "value": value,
- "label": label,
- "count": count,
- "toggle_url": self.ds.absolute_url(self.request, toggle_path),
- "selected": selected,
- })
- facet_results.append({
- "name": column,
- "results": facet_results_values,
- "truncated": len(facet_rows_results) > facet_size,
- })
+ facet_results_values.append(
+ {
+ "value": value,
+ "label": label,
+ "count": count,
+ "toggle_url": self.ds.absolute_url(
+ self.request, toggle_path
+ ),
+ "selected": selected,
+ }
+ )
+ facet_results.append(
+ {
+ "name": column,
+ "results": facet_results_values,
+ "truncated": len(facet_rows_results)
+ > facet_size,
+ }
+ )
except QueryInterrupted:
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 wrap_with_databases_header(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):
if event["type"] == "http.response.start":
- original_headers = event.get("headers") or []
+ original_headers = (
+ event.get("headers") or []
+ )
event = {
"type": event["type"],
"status": event["status"],
- "headers": original_headers + [
- [b"x-databases",
- ", ".join(datasette.databases.keys()).encode("utf-8")]
+ "headers": original_headers
+ + [
+ [
+ b"x-databases",
+ ", ".join(
+ datasette.databases.keys()
+ ).encode("utf-8"),
+ ]
],
}
await send(event)
+
await app(scope, receive, wrapped_send)
+
return add_x_databases_header
+
return wrap_with_databases_header
Examples: `datasette-cors `__, `datasette-pyinstrument `__
@@ -759,7 +816,9 @@ This hook fires when the Datasette application server first starts up. You can i
@hookimpl
def startup(datasette):
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:
@@ -770,9 +829,12 @@ Or you can return an async function which will be awaited on startup. Use this o
async def inner():
db = datasette.get_database()
if "my_table" not in await db.table_names():
- await db.execute_write("""
+ await db.execute_write(
+ """
create table my_table (mycol text)
- """)
+ """
+ )
+
return inner
Potential use-cases:
@@ -815,6 +877,7 @@ Ues this hook to return a dictionary of additional :ref:`canned query `__
@@ -888,9 +960,12 @@ Here's an example that authenticates the actor based on an incoming API key:
SECRET_KEY = "this-is-a-secret"
+
@hookimpl
def actor_from_request(datasette, request):
- authorization = request.headers.get("authorization") or ""
+ authorization = (
+ request.headers.get("authorization") or ""
+ )
expected = "Bearer {}".format(SECRET_KEY)
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
+
@hookimpl
def actor_from_request(datasette, request):
async def inner():
@@ -914,7 +990,8 @@ Instead of returning a dictionary, this function can return an awaitable functio
return None
# Look up ?_token=xxx in sessions table
result = await datasette.get_database().execute(
- "select count(*) from sessions where token = ?", [token]
+ "select count(*) from sessions where token = ?",
+ [token],
)
if result.first()[0]:
return {"token": token}
@@ -952,7 +1029,7 @@ The hook should return an instance of ``datasette.filters.FilterArguments`` whic
where_clauses=["id > :max_id"],
params={"max_id": 5},
human_descriptions=["max_id is greater than 5"],
- extra_context={}
+ extra_context={},
)
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.filters import FilterArguments
+
@hookimpl
def filters_from_request(self, request):
if request.args.get("_nothing"):
- return FilterArguments(["1 = 0"], human_descriptions=["NOTHING"])
+ return FilterArguments(
+ ["1 = 0"], human_descriptions=["NOTHING"]
+ )
Example: `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
import random
+
@hookimpl
def permission_allowed(action):
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():
if action == "execute-sql" and resource == "staff":
return False
- if action == "view-table" and resource == ("staff", "admin_log"):
+ if action == "view-table" and resource == (
+ "staff",
+ "admin_log",
+ ):
if not actor:
return False
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",
{"user_id": user_id},
)
@@ -1059,18 +1145,21 @@ This example registers two new magic parameters: ``:_request_http_version`` retu
from uuid import uuid4
+
def uuid(key, request):
if key == "new":
return str(uuid4())
else:
raise KeyError
+
def request(key, request):
if key == "http_version":
return request.scope["http_version"]
else:
raise KeyError
+
@hookimpl
def register_magic_parameters(datasette):
return [
@@ -1103,9 +1192,12 @@ This example returns a redirect to a ``/-/login`` page:
from datasette import hookimpl
from urllib.parse import urlencode
+
@hookimpl
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:
@@ -1114,10 +1206,15 @@ The function can alternatively return an awaitable function if it needs to make
from datasette import hookimpl
from datasette.utils.asgi import Response
+
@hookimpl
def forbidden(datasette):
async def inner():
- return Response.html(await datasette.render_template("forbidden.html"))
+ return Response.html(
+ await datasette.render_template(
+ "forbidden.html"
+ )
+ )
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
+
@hookimpl
def menu_links(datasette, actor):
if actor and actor.get("id") == "root":
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.
@@ -1188,13 +1291,20 @@ This example adds a new table action if the signed in user is ``"root"``:
from datasette import hookimpl
+
@hookimpl
def table_actions(datasette, actor):
if actor and actor.get("id") == "root":
- return [{
- "href": datasette.urls.path("/-/edit-schema/{}/{}".format(database, table)),
- "label": "Edit schema for this table",
- }]
+ return [
+ {
+ "href": datasette.urls.path(
+ "/-/edit-schema/{}/{}".format(
+ database, table
+ )
+ ),
+ "label": "Edit schema for this table",
+ }
+ ]
Example: `datasette-graphql `_
@@ -1238,6 +1348,7 @@ This example will disable CSRF protection for that specific URL path:
from datasette import hookimpl
+
@hookimpl
def skip_csrf(scope):
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),
"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
# 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!