Magic parameters for canned queries

Closes #842

Includes a new plugin hook, register_magic_parameters()
This commit is contained in:
Simon Willison 2020-06-27 19:58:16 -07:00 committed by GitHub
commit 563f5a2d3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 477 additions and 167 deletions

View file

@ -132,7 +132,7 @@ class Database:
with sqlite_timelimit(conn, time_limit_ms):
try:
cursor = conn.cursor()
cursor.execute(sql, params or {})
cursor.execute(sql, params if params is not None else {})
max_returned_rows = self.ds.max_returned_rows
if max_returned_rows == page_size:
max_returned_rows += 1

View file

@ -0,0 +1,55 @@
from datasette import hookimpl
from datasette.utils import escape_fts
import datetime
import os
import time
def header(key, request):
key = key.replace("_", "-").encode("utf-8")
headers_dict = dict(request.scope["headers"])
return headers_dict[key].decode("utf-8")
def actor(key, request):
if request.actor is None:
raise KeyError
return request.actor[key]
def cookie(key, request):
return request.cookies[key]
def timestamp(key, request):
if key == "epoch":
return int(time.time())
elif key == "date_utc":
return datetime.datetime.utcnow().date().isoformat()
elif key == "datetime_utc":
return datetime.datetime.utcnow().strftime(r"%Y-%m-%dT%H:%M:%S") + "Z"
else:
raise KeyError
def random(key, request):
if key.startswith("chars_") and key.split("chars_")[-1].isdigit():
num_chars = int(key.split("chars_")[-1])
if num_chars % 2 == 1:
urandom_len = (num_chars + 1) / 2
else:
urandom_len = num_chars / 2
return os.urandom(int(urandom_len)).hex()[:num_chars]
else:
raise KeyError
@hookimpl
def register_magic_parameters():
return [
("header", header),
("actor", actor),
("cookie", cookie),
("timestamp", timestamp),
("random", random),
]

View file

@ -83,3 +83,8 @@ def permission_allowed(datasette, actor, action, resource):
@hookspec
def canned_queries(datasette, database, actor):
"Return a dictonary of canned query definitions or an awaitable function that returns them"
@hookspec
def register_magic_parameters(datasette):
"Return a list of (name, function) magic parameter functions"

View file

@ -11,6 +11,7 @@ DEFAULT_PLUGINS = (
"datasette.sql_functions",
"datasette.actor_auth_cookie",
"datasette.default_permissions",
"datasette.default_magic_parameters",
)
pm = pluggy.PluginManager("datasette")

View file

@ -4,6 +4,7 @@ import base64
import click
import hashlib
import inspect
import itertools
import json
import mergedeep
import os
@ -17,6 +18,7 @@ import urllib
import numbers
import yaml
from .shutil_backport import copytree
from ..plugins import pm
try:
import pysqlite3 as sqlite3

View file

@ -1,4 +1,5 @@
import os
import itertools
import jinja2
from datasette.utils import (
@ -165,11 +166,12 @@ class QueryView(DataView):
named_parameter_values = {
named_parameter: params.get(named_parameter) or ""
for named_parameter in named_parameters
if not named_parameter.startswith("_")
}
# Set to blank string if missing from params
for named_parameter in named_parameters:
if named_parameter not in params:
if named_parameter not in params and not named_parameter.startswith("_"):
params[named_parameter] = ""
extra_args = {}
@ -184,9 +186,13 @@ class QueryView(DataView):
if write:
if request.method == "POST":
params = await request.post_vars()
if canned_query:
params_for_query = MagicParameters(params, request, self.ds)
else:
params_for_query = params
try:
cursor = await self.ds.databases[database].execute_write(
sql, params, block=True
sql, params_for_query, block=True
)
message = metadata.get(
"on_success_message"
@ -227,8 +233,12 @@ class QueryView(DataView):
templates,
)
else: # Not a write
if canned_query:
params_for_query = MagicParameters(params, request, self.ds)
else:
params_for_query = params
results = await self.ds.execute(
database, sql, params, truncate=True, **extra_args
database, sql, params_for_query, truncate=True, **extra_args
)
columns = [r[0] for r in results.description]
@ -298,3 +308,25 @@ class QueryView(DataView):
extra_template,
templates,
)
class MagicParameters(dict):
def __init__(self, data, request, datasette):
super().__init__(data)
self._request = request
self._magics = dict(
itertools.chain.from_iterable(
pm.hook.register_magic_parameters(datasette=datasette)
)
)
def __getitem__(self, key):
if key.startswith("_") and key.count("_") >= 2:
prefix, suffix = key[1:].split("_", 1)
if prefix in self._magics:
try:
return self._magics[prefix](suffix, self._request)
except KeyError:
return super().__getitem__(key)
else:
return super().__getitem__(key)