Skip to content

Commit

Permalink
New JSON design for query views (#2118)
Browse files Browse the repository at this point in the history
* Refs #2111, closes #2110
* New Context dataclass/subclass mechanism, refs #2127
* Define QueryContext and extract get_tables() method, refs #2127
* Fix OPTIONS bug by porting DaatbaseView to be a View subclass
* Expose async_view_for_class.view_class for test_routes test
* Error/truncated aruments for renderers, closes #2130
  • Loading branch information
simonw authored Aug 8, 2023
1 parent 5139c08 commit 1377a29
Show file tree
Hide file tree
Showing 15 changed files with 579 additions and 112 deletions.
3 changes: 2 additions & 1 deletion datasette/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datasette.permissions import Permission
from datasette.permissions import Permission # noqa
from datasette.version import __version_info__, __version__ # noqa
from datasette.utils.asgi import Forbidden, NotFound, Request, Response # noqa
from datasette.utils import actor_matches_allow # noqa
from datasette.views import Context # noqa
from .hookspecs import hookimpl # noqa
from .hookspecs import hookspec # noqa
16 changes: 13 additions & 3 deletions datasette/app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import asyncio
from typing import Sequence, Union, Tuple, Optional, Dict, Iterable
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
import asgi_csrf
import collections
import dataclasses
import datetime
import functools
import glob
Expand Down Expand Up @@ -33,6 +34,7 @@
from jinja2.environment import Template
from jinja2.exceptions import TemplateNotFound

from .views import Context
from .views.base import ureg
from .views.database import database_download, DatabaseView, TableCreateView
from .views.index import IndexView
Expand Down Expand Up @@ -1115,7 +1117,11 @@ def _register_renderers(self):
)

async def render_template(
self, templates, context=None, request=None, view_name=None
self,
templates: Union[List[str], str, Template],
context: Optional[Union[Dict[str, Any], Context]] = None,
request: Optional[Request] = None,
view_name: Optional[str] = None,
):
if not self._startup_invoked:
raise Exception("render_template() called before await ds.invoke_startup()")
Expand All @@ -1126,6 +1132,8 @@ async def render_template(
if isinstance(templates, str):
templates = [templates]
template = self.jinja_env.select_template(templates)
if dataclasses.is_dataclass(context):
context = dataclasses.asdict(context)
body_scripts = []
# pylint: disable=no-member
for extra_script in pm.hook.extra_body_script(
Expand Down Expand Up @@ -1368,7 +1376,8 @@ def add_route(view, regex):
r"/(?P<database>[^\/\.]+)\.db$",
)
add_route(
DatabaseView.as_view(self), r"/(?P<database>[^\/\.]+)(\.(?P<format>\w+))?$"
wrap_view(DatabaseView, self),
r"/(?P<database>[^\/\.]+)(\.(?P<format>\w+))?$",
)
add_route(TableCreateView.as_view(self), r"/(?P<database>[^\/\.]+)/-/create$")
add_route(
Expand Down Expand Up @@ -1707,6 +1716,7 @@ async def async_view_for_class(request, send):
datasette=datasette,
)

async_view_for_class.view_class = view_class
return async_view_for_class


Expand Down
21 changes: 14 additions & 7 deletions datasette/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def convert_specific_columns_to_json(rows, columns, json_cols):
return new_rows


def json_renderer(args, data, view_name):
def json_renderer(args, data, error, truncated=None):
"""Render a response as JSON"""
status_code = 200

Expand All @@ -47,8 +47,15 @@ def json_renderer(args, data, view_name):
# Deal with the _shape option
shape = args.get("_shape", "objects")
# if there's an error, ignore the shape entirely
if data.get("error"):
data["ok"] = True
if error:
shape = "objects"
status_code = 400
data["error"] = error
data["ok"] = False

if truncated is not None:
data["truncated"] = truncated

if shape == "arrayfirst":
if not data["rows"]:
Expand All @@ -64,13 +71,13 @@ def json_renderer(args, data, view_name):
if rows and columns:
data["rows"] = [dict(zip(columns, row)) for row in rows]
if shape == "object":
error = None
shape_error = None
if "primary_keys" not in data:
error = "_shape=object is only available on tables"
shape_error = "_shape=object is only available on tables"
else:
pks = data["primary_keys"]
if not pks:
error = (
shape_error = (
"_shape=object not available for tables with no primary keys"
)
else:
Expand All @@ -79,8 +86,8 @@ def json_renderer(args, data, view_name):
pk_string = path_from_row_pks(row, pks, not pks)
object_rows[pk_string] = row
data = object_rows
if error:
data = {"ok": False, "error": error}
if shape_error:
data = {"ok": False, "error": shape_error}
elif shape == "array":
data = data["rows"]

Expand Down
3 changes: 3 additions & 0 deletions datasette/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Context:
"Base class for all documented contexts"
pass
2 changes: 2 additions & 0 deletions datasette/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ async def get(self, request):
table=data.get("table"),
request=request,
view_name=self.name,
truncated=False, # TODO: support this
error=data.get("error"),
# These will be deprecated in Datasette 1.0:
args=request.args,
data=data,
Expand Down
Loading

0 comments on commit 1377a29

Please sign in to comment.