Support sorting and pagination in API
This commit is contained in:
parent
c3941bd70c
commit
03d2ac8b80
@ -7,6 +7,7 @@ from __future__ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -34,6 +35,53 @@ def JSONError(error_code, error_str):
|
|||||||
return json.dumps(dict(error=error_str, status_code=error_code))
|
return json.dumps(dict(error=error_str, status_code=error_code))
|
||||||
|
|
||||||
|
|
||||||
|
def _JSONApiSpec(query):
|
||||||
|
"""
|
||||||
|
Implementing JSON API spec for filtering, sorting and paginating results.
|
||||||
|
|
||||||
|
:param query: A Bottle query dict.
|
||||||
|
:return: A tuple of filters, page number, page size (items per page) and
|
||||||
|
sorting to apply.
|
||||||
|
"""
|
||||||
|
# Handle filtering according to JSON API spec
|
||||||
|
filters = {}
|
||||||
|
for param in query:
|
||||||
|
filter_match = FILTER_RE.match(param)
|
||||||
|
if not filter_match:
|
||||||
|
continue
|
||||||
|
field = filter_match.group(1)
|
||||||
|
value = query[filter_match.group(0)]
|
||||||
|
filters[field] = value
|
||||||
|
|
||||||
|
# Handle pagination according to JSON API spec
|
||||||
|
page_number, page_size = 0, None
|
||||||
|
try:
|
||||||
|
if 'page[size]' in query:
|
||||||
|
page_size = int(query['page[size]'])
|
||||||
|
assert page_size > 0
|
||||||
|
if 'page[number]' in query:
|
||||||
|
page_number = int(query['page[number]'])
|
||||||
|
assert page_number >= 0
|
||||||
|
except (AssertionError, ValueError):
|
||||||
|
raise ValueError("Invalid pagination provided.")
|
||||||
|
|
||||||
|
# Handle sorting according to JSON API spec
|
||||||
|
sorting = []
|
||||||
|
if 'sort' in query:
|
||||||
|
for index in query['sort'].split(','):
|
||||||
|
try:
|
||||||
|
sort_field = getattr(flat_model.Flat, index.lstrip('-'))
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid sorting key provided: {}.".format(index)
|
||||||
|
)
|
||||||
|
if index.startswith('-'):
|
||||||
|
sort_field = sort_field.desc()
|
||||||
|
sorting.append(sort_field)
|
||||||
|
|
||||||
|
return filters, page_number, page_size, sorting
|
||||||
|
|
||||||
|
|
||||||
def _serialize_flat(flat, config):
|
def _serialize_flat(flat, config):
|
||||||
"""
|
"""
|
||||||
Serialize a flat for JSON API.
|
Serialize a flat for JSON API.
|
||||||
@ -101,6 +149,18 @@ def flats_v1(config, db):
|
|||||||
Filtering can be done through the ``filter`` GET param, according
|
Filtering can be done through the ``filter`` GET param, according
|
||||||
to JSON API spec (http://jsonapi.org/recommendations/#filtering).
|
to JSON API spec (http://jsonapi.org/recommendations/#filtering).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
By default no pagination is done. Pagination can be forced using
|
||||||
|
``page[size]`` to specify a number of items per page and
|
||||||
|
``page[number]`` to specify which page to return. Pages are numbered
|
||||||
|
starting from 0.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Sorting can be handled through the ``sort`` GET param, according to
|
||||||
|
JSON API spec (http://jsonapi.org/format/#fetching-sorting).
|
||||||
|
|
||||||
:return: The available flats objects in a JSON ``data`` dict.
|
:return: The available flats objects in a JSON ``data`` dict.
|
||||||
"""
|
"""
|
||||||
if bottle.request.method == 'OPTIONS':
|
if bottle.request.method == 'OPTIONS':
|
||||||
@ -108,26 +168,29 @@ def flats_v1(config, db):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_query = db.query(flat_model.Flat)
|
try:
|
||||||
|
filters, page_number, page_size, sorting = _JSONApiSpec(
|
||||||
# Handle filtering according to JSON API spec
|
bottle.request.query
|
||||||
filters = {}
|
)
|
||||||
for param in bottle.request.query:
|
except ValueError as exc:
|
||||||
filter_match = FILTER_RE.match(param)
|
return JSONError(400, str(exc))
|
||||||
if not filter:
|
|
||||||
continue
|
|
||||||
field = filter_match.group(1)
|
|
||||||
value = bottle.request.query[filter_match.group(0)]
|
|
||||||
filters[field] = value
|
|
||||||
db_query = db_query.filter_by(**filters)
|
|
||||||
|
|
||||||
# Build flat list
|
# Build flat list
|
||||||
|
db_query = (
|
||||||
|
db.query(flat_model.Flat).filter_by(**filters).order_by(*sorting)
|
||||||
|
)
|
||||||
flats = [
|
flats = [
|
||||||
_serialize_flat(flat, config)
|
_serialize_flat(flat, config)
|
||||||
for flat in db_query
|
for flat in itertools.islice(
|
||||||
|
db_query,
|
||||||
|
page_number * page_size if page_size else None,
|
||||||
|
page_number * page_size + page_size if page_size else None
|
||||||
|
)
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
"data": flats
|
"data": flats,
|
||||||
|
"page": page_number,
|
||||||
|
"items_per_page": page_size if page_size else len(flats)
|
||||||
}
|
}
|
||||||
except Exception as exc: # pylint: disable= broad-except
|
except Exception as exc: # pylint: disable= broad-except
|
||||||
return JSONError(500, str(exc))
|
return JSONError(500, str(exc))
|
||||||
@ -245,6 +308,23 @@ def search_v1(db, config):
|
|||||||
"query": "SOME_QUERY"
|
"query": "SOME_QUERY"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Filtering can be done through the ``filter`` GET param, according
|
||||||
|
to JSON API spec (http://jsonapi.org/recommendations/#filtering).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
By default no pagination is done. Pagination can be forced using
|
||||||
|
``page[size]`` to specify a number of items per page and
|
||||||
|
``page[number]`` to specify which page to return. Pages are numbered
|
||||||
|
starting from 0.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Sorting can be handled through the ``sort`` GET param, according to
|
||||||
|
JSON API spec (http://jsonapi.org/format/#fetching-sorting).
|
||||||
|
|
||||||
:return: The matching flat objects in a JSON ``data`` dict.
|
:return: The matching flat objects in a JSON ``data`` dict.
|
||||||
"""
|
"""
|
||||||
if bottle.request.method == 'OPTIONS':
|
if bottle.request.method == 'OPTIONS':
|
||||||
@ -257,14 +337,30 @@ def search_v1(db, config):
|
|||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
return JSONError(400, "Invalid query provided.")
|
return JSONError(400, "Invalid query provided.")
|
||||||
|
|
||||||
flats_db_query = flat_model.Flat.search_query(db, query)
|
try:
|
||||||
|
filters, page_number, page_size, sorting = _JSONApiSpec(
|
||||||
|
bottle.request.query
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
return JSONError(400, str(exc))
|
||||||
|
|
||||||
|
flats_db_query = (flat_model.Flat
|
||||||
|
.search_query(db, query)
|
||||||
|
.filter_by(**filters)
|
||||||
|
.order_by(*sorting))
|
||||||
flats = [
|
flats = [
|
||||||
_serialize_flat(flat, config)
|
_serialize_flat(flat, config)
|
||||||
for flat in flats_db_query
|
for flat in itertools.islice(
|
||||||
|
flats_db_query,
|
||||||
|
page_number * page_size if page_size else None,
|
||||||
|
page_number * page_size + page_size if page_size else None
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"data": flats
|
"data": flats,
|
||||||
|
"page": page_number,
|
||||||
|
"items_per_page": page_size if page_size else len(flats)
|
||||||
}
|
}
|
||||||
except Exception as exc: # pylint: disable= broad-except
|
except Exception as exc: # pylint: disable= broad-except
|
||||||
return JSONError(500, str(exc))
|
return JSONError(500, str(exc))
|
||||||
|
Loading…
Reference in New Issue
Block a user