-
-
Notifications
You must be signed in to change notification settings - Fork 699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ?_extra= mechanism for requesting extra properties in JSON #262
Comments
Idea: |
I built a version of that a while ago as the |
Are there any interesting use-cases for a plugin hook that allows plugins to define their own |
Just realized I added an undocumented That will need to be made consistent with the new mechanism. I think So I could support I think I prefer allowing commas in |
In the documentation for |
I think it's worth having a plugin hook for this - it can be same hook that is used internally. Maybe Things like suggested facets will become |
This is now blocking simonw/datasette-graphql#61 because that issue needs a way to turn off suggested facets when retrieving the results of a table query. |
I think I should prioritize the facets component of this, since that could have significant performance wins while also supporting |
This is relevant to the big refactor in: |
I'm going to write code which parses It will return an error if you ask for an extra that does not exist. |
Got first prototype working using diff --git a/datasette/views/table.py b/datasette/views/table.py
index ad45ecd3..c8690b22 100644
--- a/datasette/views/table.py
+++ b/datasette/views/table.py
@@ -2,6 +2,7 @@ import asyncio
import itertools
import json
+from asyncinject import Registry
import markupsafe
from datasette.plugins import pm
@@ -538,57 +539,60 @@ class TableView(DataView):
# Execute the main query!
results = await db.execute(sql, params, truncate=True, **extra_args)
- # Calculate the total count for this query
- count = None
- if (
- not db.is_mutable
- and self.ds.inspect_data
- and count_sql == f"select count(*) from {table_name} "
- ):
- # We can use a previously cached table row count
- try:
- count = self.ds.inspect_data[database_name]["tables"][table_name][
- "count"
- ]
- except KeyError:
- pass
-
- # Otherwise run a select count(*) ...
- if count_sql and count is None and not nocount:
- try:
- count_rows = list(await db.execute(count_sql, from_sql_params))
- count = count_rows[0][0]
- except QueryInterrupted:
- pass
-
- # Faceting
- if not self.ds.setting("allow_facet") and any(
- arg.startswith("_facet") for arg in request.args
- ):
- raise BadRequest("_facet= is not allowed")
+ # Resolve extras
+ extras = _get_extras(request)
+ if request.args.getlist("_facet"):
+ extras.add("facet_results")
- # pylint: disable=no-member
- facet_classes = list(
- itertools.chain.from_iterable(pm.hook.register_facet_classes())
- )
- facet_results = {}
- facets_timed_out = []
- facet_instances = []
- for klass in facet_classes:
- facet_instances.append(
- klass(
- self.ds,
- request,
- database_name,
- sql=sql_no_order_no_limit,
- params=params,
- table=table_name,
- metadata=table_metadata,
- row_count=count,
- )
+ async def extra_count():
+ # Calculate the total count for this query
+ count = None
+ if (
+ not db.is_mutable
+ and self.ds.inspect_data
+ and count_sql == f"select count(*) from {table_name} "
+ ):
+ # We can use a previously cached table row count
+ try:
+ count = self.ds.inspect_data[database_name]["tables"][table_name][
+ "count"
+ ]
+ except KeyError:
+ pass
+
+ # Otherwise run a select count(*) ...
+ if count_sql and count is None and not nocount:
+ try:
+ count_rows = list(await db.execute(count_sql, from_sql_params))
+ count = count_rows[0][0]
+ except QueryInterrupted:
+ pass
+ return count
+
+ async def facet_instances(extra_count):
+ facet_instances = []
+ facet_classes = list(
+ itertools.chain.from_iterable(pm.hook.register_facet_classes())
)
+ for facet_class in facet_classes:
+ facet_instances.append(
+ facet_class(
+ self.ds,
+ request,
+ database_name,
+ sql=sql_no_order_no_limit,
+ params=params,
+ table=table_name,
+ metadata=table_metadata,
+ row_count=extra_count,
+ )
+ )
+ return facet_instances
+
+ async def extra_facet_results(facet_instances):
+ facet_results = {}
+ facets_timed_out = []
- async def execute_facets():
if not nofacet:
# Run them in parallel
facet_awaitables = [facet.facet_results() for facet in facet_instances]
@@ -607,9 +611,13 @@ class TableView(DataView):
facet_results[key] = facet_info
facets_timed_out.extend(instance_facets_timed_out)
- suggested_facets = []
+ return {
+ "results": facet_results,
+ "timed_out": facets_timed_out,
+ }
- async def execute_suggested_facets():
+ async def extra_suggested_facets(facet_instances):
+ suggested_facets = []
# Calculate suggested facets
if (
self.ds.setting("suggest_facets")
@@ -624,8 +632,15 @@ class TableView(DataView):
]
for suggest_result in await gather(*facet_suggest_awaitables):
suggested_facets.extend(suggest_result)
+ return suggested_facets
+
+ # Faceting
+ if not self.ds.setting("allow_facet") and any(
+ arg.startswith("_facet") for arg in request.args
+ ):
+ raise BadRequest("_facet= is not allowed")
- await gather(execute_facets(), execute_suggested_facets())
+ # pylint: disable=no-member
# Figure out columns and rows for the query
columns = [r[0] for r in results.description]
@@ -732,17 +747,56 @@ class TableView(DataView):
rows = rows[:page_size]
# human_description_en combines filters AND search, if provided
- human_description_en = filters.human_description_en(
- extra=extra_human_descriptions
- )
+ async def extra_human_description_en():
+ human_description_en = filters.human_description_en(
+ extra=extra_human_descriptions
+ )
+ if sort or sort_desc:
+ human_description_en = " ".join(
+ [b for b in [human_description_en, sorted_by] if b]
+ )
+ return human_description_en
if sort or sort_desc:
sorted_by = "sorted by {}{}".format(
(sort or sort_desc), " descending" if sort_desc else ""
)
- human_description_en = " ".join(
- [b for b in [human_description_en, sorted_by] if b]
- )
+
+ async def extra_next_url():
+ return next_url
+
+ async def extra_columns():
+ return columns
+
+ async def extra_primary_keys():
+ return pks
+
+ registry = Registry(
+ extra_count,
+ extra_facet_results,
+ extra_suggested_facets,
+ facet_instances,
+ extra_human_description_en,
+ extra_next_url,
+ extra_columns,
+ extra_primary_keys,
+ )
+
+ results = await registry.resolve_multi(
+ ["extra_{}".format(extra) for extra in extras]
+ )
+ data = {
+ "ok": True,
+ "rows": rows[:page_size],
+ "next": next_value and str(next_value) or None,
+ }
+ data.update({
+ key.replace("extra_", ""): value
+ for key, value in results.items()
+ if key.startswith("extra_")
+ and key.replace("extra_", "") in extras
+ })
+ return Response.json(data, default=repr)
async def extra_template():
nonlocal sort
@@ -1334,3 +1388,11 @@ class TableDropView(BaseView):
await db.execute_write_fn(drop_table)
return Response.json({"ok": True}, status=200)
+
+
+def _get_extras(request):
+ extra_bits = request.args.getlist("_extra")
+ extras = set()
+ for bit in extra_bits:
+ extras.update(bit.split(","))
+ return extras With that in place, {
"ok": true,
"rows": [
{
"html_url": "https://github.com/eyeseast/geocode-sqlite/releases/tag/0.1.2",
"id": 30926270,
"author": {
"value": 25778,
"label": "eyeseast"
},
"node_id": "MDc6UmVsZWFzZTMwOTI2Mjcw",
"tag_name": "0.1.2",
"target_commitish": "master",
"name": "v0.1.2",
"draft": 0,
"prerelease": 1,
"created_at": "2020-09-08T17:48:24Z",
"published_at": "2020-09-08T17:50:15Z",
"body": "Basic API is in place, with CLI support for Google, Bing, MapQuest and Nominatum (OSM) geocoders.",
"repo": {
"value": 293361514,
"label": "geocode-sqlite"
},
"reactions": null,
"mentions_count": null
}
],
"next": "30926270",
"primary_keys": [
"id"
],
"columns": [
"html_url",
"id",
"author",
"node_id",
"tag_name",
"target_commitish",
"name",
"draft",
"prerelease",
"created_at",
"published_at",
"body",
"repo",
"reactions",
"mentions_count"
],
"count": 25,
"facet_results": {
"results": {
"author": {
"name": "author",
"type": "column",
"hideable": true,
"toggle_url": "/content/releases?author=25778&_size=1&_extra=count%2Cprimary_keys%2Ccolumns",
"results": [
{
"value": 25778,
"label": "eyeseast",
"count": 25,
"toggle_url": "http://127.0.0.1:8001/content/releases?_size=1&_extra=count%2Cprimary_keys%2Ccolumns&_facet=author",
"selected": true
}
],
"truncated": false
}
},
"timed_out": []
}
} |
Implementing this to work with the The challenge here is that we're working with the whole
I want this to work completely differently: I want the formats (including HTML) to have the option of adding some extra |
I pushed my prototype so far, going to start a draft PR for it. |
It's annoying that the https://docs.datasette.io/en/0.64.1/plugin_hooks.html#register-output-renderer-datasette plugin hook passes https://docs.datasette.io/en/0.64.1/plugin_hooks.html#render-cell-row-value-column-table-database-datasette is documented as accepting |
Maybe This could be quite neat, in that EVERY key in the JSON representation would be defined as an extra - just some would be on by default. There could even be a mechanism for turning them back off again, maybe using In which case maybe Being able to pass Although Would |
This issue here would benefit from some kid of mechanism for returning just the HTML of the table itself, without any of the surrounding material. I'm not sure if that would make sense as an extra or not: |
I think that does make sense: |
Just realized that it's useful to be able to tell what parameters were used to generate a page... but reflecting things like So I'm going to add an extra for that information too. Not sure what to call it though:
I'm going to experiment with a |
* Implemented ?_extra= option for JSON views, refs #262 * New dependency: asyncinject * Remove now-obsolete TableView class
I just landed this PR so this feature is now in Still needs documentation and maybe some extra tests too. |
I need to get the arbitrary query page to return the same format. It likely won't have nearly as many extras. |
Also refs #262 - started a test suite for extras.
Datasette views currently work by creating a set of data that should be returned as JSON, then defining an additional, optional
template_data()
function which is called if the view is being rendered as HTML.This
template_data()
function calculates extra template context variables which are necessary for the HTML view but should not be included in the JSON.Example of how that is used today:
datasette/datasette/views/table.py
Lines 672 to 704 in 2b79f2b
With features like Facets in #255 I'm beginning to want to move more items into the
template_data()
- in the case of facets it's thesuggested_facets
array. This saves that feature from being calculated (involving several SQL queries) for the JSON case where it is unlikely to be used.But... as an API user, I want to still optionally be able to access that information.
Solution: Add a
?_extra=suggested_facets&_extra=table_metadata
argument which can be used to optionally request additional blocks to be added to the JSON API.Then redefine as many of the current
template_data()
features as extra arguments instead, and teach Datasette to return certain extras by default when rendering templates.This could allow the JSON representation to be slimmed down further (removing e.g. the
table_definition
andview_definition
keys) while still making that information available to API users who need it.The text was updated successfully, but these errors were encountered: