kopia lustrzana https://github.com/simonw/datasette
Fix blacken-docs errors and warnings, refs #1718
rodzic
36573638b0
commit
92b26673d8
|
@ -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:
|
||||
|
||||
|
|
|
@ -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(
|
||||
"<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:
|
||||
|
@ -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() <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
|
||||
|
||||
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() <https://d
|
|||
|
||||
await db.execute_write_many(
|
||||
"insert into characters (id, name) values (?, ?)",
|
||||
[(1, "Melanie"), (2, "Selma"), (2, "Viktor")]
|
||||
[(1, "Melanie"), (2, "Selma"), (2, "Viktor")],
|
||||
)
|
||||
|
||||
.. _database_execute_write_fn:
|
||||
|
@ -811,10 +832,15 @@ For example:
|
|||
|
||||
def delete_and_return_count(conn):
|
||||
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:
|
||||
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_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_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),
|
||||
|
|
|
@ -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
|
||||
|
||||
<link rel="alternate"
|
||||
type="application/json+datasette"
|
||||
|
|
|
@ -44,9 +44,12 @@ aggregates and collations. For example:
|
|||
from datasette import hookimpl
|
||||
import random
|
||||
|
||||
|
||||
@hookimpl
|
||||
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
|
||||
arguments and can be called like this::
|
||||
|
@ -72,9 +75,10 @@ example:
|
|||
|
||||
from datasette import hookimpl
|
||||
|
||||
|
||||
@hookimpl
|
||||
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::
|
||||
|
||||
|
@ -127,9 +131,7 @@ Here's an example plugin that adds a ``"user_agent"`` variable to the template c
|
|||
|
||||
@hookimpl
|
||||
def extra_template_vars(request):
|
||||
return {
|
||||
"user_agent": request.headers.get("user-agent")
|
||||
}
|
||||
return {"user_agent": request.headers.get("user-agent")}
|
||||
|
||||
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():
|
||||
if database:
|
||||
db = datasette.databases[database]
|
||||
return {"hidden_table_names": await db.hidden_table_names()}
|
||||
return {
|
||||
"hidden_table_names": await db.hidden_table_names()
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
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:
|
||||
|
@ -152,8 +157,15 @@ And here's an example which adds a ``sql_first(sql_query)`` function which execu
|
|||
@hookimpl
|
||||
def extra_template_vars(datasette, database):
|
||||
async def sql_first(sql, dbname=None):
|
||||
dbname = dbname or database or next(iter(datasette.databases.keys()))
|
||||
return (await datasette.execute(dbname, sql)).rows[0][0]
|
||||
dbname = (
|
||||
dbname
|
||||
or database
|
||||
or next(iter(datasette.databases.keys()))
|
||||
)
|
||||
return (await datasette.execute(dbname, sql)).rows[
|
||||
0
|
||||
][0]
|
||||
|
||||
return {"sql_first": sql_first}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@hookimpl
|
||||
def extra_css_urls():
|
||||
return [
|
||||
|
@ -191,10 +204,12 @@ Or a list of dictionaries defining both a URL and an
|
|||
|
||||
@hookimpl
|
||||
def extra_css_urls():
|
||||
return [{
|
||||
"url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css",
|
||||
"sri": "sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4",
|
||||
}]
|
||||
return [
|
||||
{
|
||||
"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:
|
||||
|
||||
|
@ -204,7 +219,9 @@ This function can also return an awaitable function, useful if it needs to run a
|
|||
def extra_css_urls(datasette):
|
||||
async def inner():
|
||||
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 inner
|
||||
|
@ -225,12 +242,15 @@ return a list of URLs, a list of dictionaries or an awaitable function that retu
|
|||
|
||||
from datasette import hookimpl
|
||||
|
||||
|
||||
@hookimpl
|
||||
def extra_js_urls():
|
||||
return [{
|
||||
"url": "https://code.jquery.com/jquery-3.3.1.slim.min.js",
|
||||
"sri": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
|
||||
}]
|
||||
return [
|
||||
{
|
||||
"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 have one:
|
||||
|
@ -239,9 +259,7 @@ you have one:
|
|||
|
||||
@hookimpl
|
||||
def extra_js_urls():
|
||||
return [
|
||||
"/-/static-plugins/your-plugin/app.js"
|
||||
]
|
||||
return ["/-/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.
|
||||
|
||||
|
@ -251,9 +269,11 @@ If your code uses `JavaScript modules <https://developer.mozilla.org/en-US/docs/
|
|||
|
||||
@hookimpl
|
||||
def extra_js_urls():
|
||||
return [{
|
||||
"url": "/-/static-plugins/your-plugin/app.js",
|
||||
"module": True
|
||||
return [
|
||||
{
|
||||
"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>`_
|
||||
|
@ -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():
|
||||
return {
|
||||
"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:
|
||||
|
@ -311,7 +331,9 @@ Let's say you want to build a plugin that adds a ``datasette publish my_hosting_
|
|||
.. code-block:: python
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -345,7 +367,7 @@ Let's say you want to build a plugin that adds a ``datasette publish my_hosting_
|
|||
about_url,
|
||||
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>`_
|
||||
|
||||
|
@ -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('<a href="{href}">{label}</a>'.format(
|
||||
href=markupsafe.escape(data["href"]),
|
||||
label=markupsafe.escape(data["label"] or "") or " "
|
||||
))
|
||||
return markupsafe.Markup(
|
||||
'<a href="{href}">{label}</a>'.format(
|
||||
href=markupsafe.escape(data["href"]),
|
||||
label=markupsafe.escape(data["label"] or "")
|
||||
or " ",
|
||||
)
|
||||
)
|
||||
|
||||
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(
|
||||
"\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 <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):
|
||||
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<name>.*)$", hello_from)
|
||||
]
|
||||
return [(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.
|
||||
|
||||
|
@ -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 <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
|
||||
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 <canned_qu
|
|||
|
||||
from datasette import hookimpl
|
||||
|
||||
|
||||
@hookimpl
|
||||
def canned_queries(datasette, database):
|
||||
if database == "mydb":
|
||||
|
@ -830,15 +893,20 @@ The hook can alternatively return an awaitable function that returns a list. Her
|
|||
|
||||
from datasette import hookimpl
|
||||
|
||||
|
||||
@hookimpl
|
||||
def canned_queries(datasette, database):
|
||||
async def inner():
|
||||
db = datasette.get_database(database)
|
||||
if await db.table_exists("saved_queries"):
|
||||
results = await db.execute("select name, sql from saved_queries")
|
||||
return {result["name"]: {
|
||||
"sql": result["sql"]
|
||||
} for result in results}
|
||||
results = await db.execute(
|
||||
"select name, sql from saved_queries"
|
||||
)
|
||||
return {
|
||||
result["name"]: {"sql": result["sql"]}
|
||||
for result in results
|
||||
}
|
||||
|
||||
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:
|
||||
|
@ -847,19 +915,23 @@ The actor parameter can be used to include the currently authenticated actor in
|
|||
|
||||
from datasette import hookimpl
|
||||
|
||||
|
||||
@hookimpl
|
||||
def canned_queries(datasette, database, actor):
|
||||
async def inner():
|
||||
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(
|
||||
"select name, sql from saved_queries where actor_id = :id", {
|
||||
"id": actor["id"]
|
||||
}
|
||||
"select name, sql from saved_queries where actor_id = :id",
|
||||
{"id": actor["id"]},
|
||||
)
|
||||
return {result["name"]: {
|
||||
"sql": result["sql"]
|
||||
} for result in results}
|
||||
return {
|
||||
result["name"]: {"sql": result["sql"]}
|
||||
for result in results
|
||||
}
|
||||
|
||||
return inner
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@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 <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
|
||||
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 <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
|
||||
|
||||
|
||||
@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!
|
||||
|
|
Ładowanie…
Reference in New Issue