mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Link: HTTP header pagination, closes #1014
This commit is contained in:
parent
7e70643852
commit
e34e84901d
3 changed files with 71 additions and 1 deletions
|
|
@ -5,6 +5,7 @@ from datasette.utils import (
|
||||||
CustomJSONEncoder,
|
CustomJSONEncoder,
|
||||||
path_from_row_pks,
|
path_from_row_pks,
|
||||||
)
|
)
|
||||||
|
from datasette.utils.asgi import Response
|
||||||
|
|
||||||
|
|
||||||
def convert_specific_columns_to_json(rows, columns, json_cols):
|
def convert_specific_columns_to_json(rows, columns, json_cols):
|
||||||
|
|
@ -44,6 +45,9 @@ def json_renderer(args, data, view_name):
|
||||||
|
|
||||||
# Deal with the _shape option
|
# Deal with the _shape option
|
||||||
shape = args.get("_shape", "arrays")
|
shape = args.get("_shape", "arrays")
|
||||||
|
|
||||||
|
next_url = data.get("next_url")
|
||||||
|
|
||||||
if shape == "arrayfirst":
|
if shape == "arrayfirst":
|
||||||
data = [row[0] for row in data["rows"]]
|
data = [row[0] for row in data["rows"]]
|
||||||
elif shape in ("objects", "object", "array"):
|
elif shape in ("objects", "object", "array"):
|
||||||
|
|
@ -71,6 +75,7 @@ def json_renderer(args, data, view_name):
|
||||||
data = {"ok": False, "error": error}
|
data = {"ok": False, "error": error}
|
||||||
elif shape == "array":
|
elif shape == "array":
|
||||||
data = data["rows"]
|
data = data["rows"]
|
||||||
|
|
||||||
elif shape == "arrays":
|
elif shape == "arrays":
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
@ -89,4 +94,9 @@ def json_renderer(args, data, view_name):
|
||||||
else:
|
else:
|
||||||
body = json.dumps(data, cls=CustomJSONEncoder)
|
body = json.dumps(data, cls=CustomJSONEncoder)
|
||||||
content_type = "application/json; charset=utf-8"
|
content_type = "application/json; charset=utf-8"
|
||||||
return {"body": body, "status_code": status_code, "content_type": content_type}
|
headers = {}
|
||||||
|
if next_url:
|
||||||
|
headers["link"] = '<{}>; rel="next"'.format(next_url)
|
||||||
|
return Response(
|
||||||
|
body, status=status_code, headers=headers, content_type=content_type
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
.. _json_api:
|
||||||
|
|
||||||
JSON API
|
JSON API
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
@ -18,6 +20,8 @@ requests to fetch the data.
|
||||||
If you start Datasette without the ``--cors`` option only JavaScript running on
|
If you start Datasette without the ``--cors`` option only JavaScript running on
|
||||||
the same domain as Datasette will be able to access the API.
|
the same domain as Datasette will be able to access the API.
|
||||||
|
|
||||||
|
.. _json_api_shapes:
|
||||||
|
|
||||||
Different shapes
|
Different shapes
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
@ -138,6 +142,34 @@ this format.
|
||||||
The ``object`` keys are always strings. If your table has a compound primary
|
The ``object`` keys are always strings. If your table has a compound primary
|
||||||
key, the ``object`` keys will be a comma-separated string.
|
key, the ``object`` keys will be a comma-separated string.
|
||||||
|
|
||||||
|
.. _json_api_pagination:
|
||||||
|
|
||||||
|
Pagination
|
||||||
|
----------
|
||||||
|
|
||||||
|
The default JSON representation includes a ``"next_url"`` key which can be used to access the next page of results. If that key is null or missing then it means you have reached the final page of results.
|
||||||
|
|
||||||
|
Other representations include pagination information in the ``link`` HTTP header. That header will look something like this::
|
||||||
|
|
||||||
|
link: <https://latest.datasette.io/fixtures/sortable.json?_next=d%2Cv>; rel="next"
|
||||||
|
|
||||||
|
Here is an example Python function built using `requests <https://requests.readthedocs.io/>`__ that returns a list of all of the paginated items from one of these API endpoints:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def paginate(url):
|
||||||
|
items = []
|
||||||
|
while url:
|
||||||
|
response = requests.get(url)
|
||||||
|
try:
|
||||||
|
url = response.links.get("next").get("url")
|
||||||
|
except AttributeError:
|
||||||
|
url = None
|
||||||
|
items.extend(response.json())
|
||||||
|
return items
|
||||||
|
|
||||||
|
.. _json_api_special:
|
||||||
|
|
||||||
Special JSON arguments
|
Special JSON arguments
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1828,3 +1828,31 @@ def test_binary_data_in_json(app_client, path, expected_json, expected_text):
|
||||||
assert response.json == expected_json
|
assert response.json == expected_json
|
||||||
else:
|
else:
|
||||||
assert response.text == expected_text
|
assert response.text == expected_text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"qs",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"?_shape=arrays",
|
||||||
|
"?_shape=arrayfirst",
|
||||||
|
"?_shape=object",
|
||||||
|
"?_shape=objects",
|
||||||
|
"?_shape=array",
|
||||||
|
"?_shape=array&_nl=on",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_paginate_using_link_header(app_client, qs):
|
||||||
|
path = "/fixtures/compound_three_primary_keys.json{}".format(qs)
|
||||||
|
num_pages = 0
|
||||||
|
while path:
|
||||||
|
response = app_client.get(path)
|
||||||
|
num_pages += 1
|
||||||
|
link = response.headers.get("link")
|
||||||
|
if link:
|
||||||
|
assert link.startswith("<")
|
||||||
|
assert link.endswith('>; rel="next"')
|
||||||
|
path = link[1:].split(">")[0]
|
||||||
|
else:
|
||||||
|
path = None
|
||||||
|
assert num_pages == 21
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue