Skip to content

Commit

Permalink
use http param instead of init parameter to control range-header beha…
Browse files Browse the repository at this point in the history
…vior (#123)

* use http param instead of init parameter to control range-header behavior

* added new params to split_params method docstring

* better openapi spec field descriptions
  • Loading branch information
trondhindenes committed Jan 2, 2022
1 parent e8970b4 commit 70b4671
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 51 deletions.
27 changes: 7 additions & 20 deletions docs/source/crud/piccolo_crud.rst
Original file line number Diff line number Diff line change
Expand Up @@ -297,32 +297,19 @@ Content-Range header
In some applications it can be practical to get information about the
total number of records without invoking a separate call to
the ``count`` endpoint. Piccolo API will supply this information in the
``Content-Range`` response header if ``add_range_headers`` is set to ``True``.
You can use the ``range_header_plural_name`` parameter to configure the
``Content-Range`` response header if the ``__range_header`` http parameter
is set to ``true``.
You can use the ``__range_header_name`` http parameter to configure the
"plural name" used in the ``Content-Range`` response header.

The contents of the ``Content-Range`` header might look something like this
for the "Movie" table: ``movie 0-9/100``
for the "Movie" table: ``movie 0-9/100``.

.. code-block:: python
# app.py
from piccolo_api.crud.endpoints import PiccoloCRUD
from starlette.routing import Mount, Router
from movies.tables import Movie, Director
Example usage:

.. code-block::
app = Router([
Mount(
path='/movie',
app=PiccoloCRUD(
table=Movie,
add_range_headers=True,
range_header_plural_name="movies"
)
)
])
GET /movie/?__page=2&page_size=10&__range_header=true
-------------------------------------------------------------------------------

Expand Down
33 changes: 20 additions & 13 deletions piccolo_api/crud/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ class Params:
page: int = 1
page_size: t.Optional[int] = None
visible_fields: str = field(default="")
range_header: bool = False
range_header_name: str = field(default="")


def get_visible_fields_options(
Expand Down Expand Up @@ -143,8 +145,6 @@ def __init__(
schema_extra: t.Optional[t.Dict[str, t.Any]] = None,
max_joins: int = 0,
hooks: t.Optional[t.List[Hook]] = None,
add_range_headers: bool = False,
range_header_plural_name: t.Optional[str] = None,
) -> None:
"""
:param table:
Expand Down Expand Up @@ -198,12 +198,6 @@ def __init__(
To see which fields can be filtered in this way, you can check
the ``visible_fields_options`` value returned by the ``/schema``
endpoint.
:param add_range_headers:
if True, will add content-range headers to GET responses returning lists of data
:param range_header_plural_name:
Specify object name which is included in the Content-Range header
when `add_range_headers` is True. Defaults to table name if unset.
""" # noqa: E501
self.table = table
self.page_size = page_size
Expand All @@ -219,8 +213,6 @@ def __init__(
}
else:
self._hook_map = None # type: ignore
self.add_range_headers = add_range_headers
self.range_header_plural_name = range_header_plural_name

schema_extra = schema_extra if isinstance(schema_extra, dict) else {}
self.visible_fields_options = get_visible_fields_options(
Expand Down Expand Up @@ -542,6 +534,13 @@ def _split_params(params: t.Dict[str, t.Any]) -> Params:
You can specify which fields want to display in rows:
{'__visible_fields': 'id,name'}.
You can activate the "Content-Range" response header:
{'__range_header': True}
If the "Content-Range" response header is enabled,
you can configure the "plural name" used in the header:
{'__range_header_name': 'movies'}
This method splits the params into their different types.
"""
response = Params()
Expand Down Expand Up @@ -593,6 +592,14 @@ def _split_params(params: t.Dict[str, t.Any]) -> Params:
response.include_readable = True
continue

if key == "__range_header" and value in ("true", "True", "1"):
response.range_header = True
continue

if key == "__range_header_name":
response.range_header_name = value
continue

response.fields[key] = value

return response
Expand All @@ -608,7 +615,6 @@ def _apply_filters(
Objects etc.
"""
fields = params.fields

if fields:
model_dict = self.pydantic_model_optional(**fields).dict()
for field_name in fields.keys():
Expand Down Expand Up @@ -728,10 +734,11 @@ async def get_all(

rows = await query.run()
headers = {}
if self.add_range_headers:
if split_params.range_header is True:
plural_name = (
self.range_header_plural_name or self.table._meta.tablename
split_params.range_header_name or self.table._meta.tablename
)

row_length = len(rows)
if row_length == 0:
curr_page_len = 0
Expand Down
33 changes: 33 additions & 0 deletions piccolo_api/fastapi/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,39 @@ def modify_signature(
]
)

parameters.extend(
[
Parameter(
name="__range_header",
kind=Parameter.POSITIONAL_OR_KEYWORD,
annotation=bool,
default=Query(
default=False,
description=(
"Set to 'true' to add the "
"Content-Range response header"
),
),
)
]
)
parameters.extend(
[
Parameter(
name="__range_header_name",
kind=Parameter.POSITIONAL_OR_KEYWORD,
annotation=str,
default=Query(
default=None,
description=(
"Specify the object name in the Content-Range "
"response header (defaults to the table name)."
),
),
)
]
)

endpoint.__signature__ = Signature( # type: ignore
parameters=parameters
)
30 changes: 12 additions & 18 deletions tests/crud/test_crud_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1250,12 +1250,12 @@ def test_plural_name(self):
PiccoloCRUD(
table=Movie,
read_only=False,
add_range_headers=True,
range_header_plural_name="movies",
)
)

response = client.get("/")
response = client.get(
"/?__range_header=true&__range_header_name=movies"
)
self.assertTrue(response.status_code == 200)
# Make sure the content is correct:
response_json = response.json()
Expand All @@ -1266,11 +1266,9 @@ def test_empty_list(self):
"""
Make sure the content-range header responds correctly for empty rows
"""
client = TestClient(
PiccoloCRUD(table=Movie, read_only=False, add_range_headers=True)
)
client = TestClient(PiccoloCRUD(table=Movie, read_only=False))

response = client.get("/")
response = client.get("/?__range_header=true")
self.assertTrue(response.status_code == 200)
# Make sure the content is correct:
response_json = response.json()
Expand All @@ -1282,16 +1280,14 @@ def test_unpaged_ranges(self):
Make sure the content-range header responds
correctly for unpaged results
"""
client = TestClient(
PiccoloCRUD(table=Movie, read_only=False, add_range_headers=True)
)
client = TestClient(PiccoloCRUD(table=Movie, read_only=False))

movie = Movie(name="Star Wars", rating=93)
movie.save().run_sync()
movie2 = Movie(name="Blade Runner", rating=94)
movie2.save().run_sync()

response = client.get("/")
response = client.get("/?__range_header=true")
self.assertTrue(response.status_code == 200)
# Make sure the content is correct:
response_json = response.json()
Expand All @@ -1304,9 +1300,7 @@ def test_page_sized_results(self):
Make sure the content-range header responds
correctly requests with page_size
"""
client = TestClient(
PiccoloCRUD(table=Movie, read_only=False, add_range_headers=True)
)
client = TestClient(PiccoloCRUD(table=Movie, read_only=False))

movie = Movie(name="Star Wars", rating=93)
movie.save().run_sync()
Expand All @@ -1315,14 +1309,14 @@ def test_page_sized_results(self):
movie3 = Movie(name="The Godfather", rating=95)
movie3.save().run_sync()

response = client.get("/?__page_size=1")
response = client.get("/?__page_size=1&__range_header=true")
self.assertEqual(response.headers.get("Content-Range"), "movie 0-0/3")

response = client.get("/?__page_size=1&__page=2")
response = client.get("/?__page_size=1&__page=2&__range_header=true")
self.assertEqual(response.headers.get("Content-Range"), "movie 1-1/3")

response = client.get("/?__page_size=1&__page=2")
response = client.get("/?__page_size=1&__page=2&__range_header=true")
self.assertEqual(response.headers.get("Content-Range"), "movie 1-1/3")

response = client.get("/?__page_size=99&__page=1")
response = client.get("/?__page_size=99&__page=1&__range_header=true")
self.assertEqual(response.headers.get("Content-Range"), "movie 0-2/3")

0 comments on commit 70b4671

Please sign in to comment.