Add an opendata endpoint in API

This commit is contained in:
Lucas Verney 2017-12-13 14:38:05 +01:00
parent f29204265c
commit 49ebfe9111
3 changed files with 111 additions and 6 deletions

View File

@ -35,3 +35,14 @@ class PostalCode(BASE):
def __repr__(self):
return "<PostalCode(id=%s)>" % self.id
def json_api_repr(self):
"""
Return a dict representation of this postal code object that is JSON
serializable.
"""
return {
k: v
for k, v in self.__dict__.items()
if not k.startswith("_")
}

View File

@ -82,7 +82,7 @@ def get_app(config):
)
# API v1 routes
app.route("/api/v1/", ["GET", "OPTIONS"], api_routes.index_v1)
app.route("/api/v1", ["GET", "OPTIONS"], api_routes.index_v1)
app.route("/api/v1/time_to_places", ["GET", "OPTIONS"],
api_routes.time_to_places_v1)
@ -97,6 +97,10 @@ def get_app(config):
app.route("/api/v1/search", "POST", api_routes.search_v1)
app.route("/api/v1/opendata", "GET", api_routes.opendata_index_v1)
app.route("/api/v1/opendata/postal_codes", "GET",
api_routes.opendata_postal_codes_v1)
# Index
app.route("/", "GET", lambda: _serve_static_file("index.html"))

View File

@ -35,13 +35,16 @@ def JSONError(error_code, error_str):
return json.dumps(dict(error=error_str, status_code=error_code))
def _JSONApiSpec(query):
def _JSONApiSpec(query, model, default_sorting=None):
"""
Implementing JSON API spec for filtering, sorting and paginating results.
:param query: A Bottle query dict.
:param model: Database model used in this query.
:param default_sorting: Optional field to sort on if no sort options are
passed through parameters.
:return: A tuple of filters, page number, page size (items per page) and
sorting to apply.
sorting to apply.
"""
# Handle filtering according to JSON API spec
filters = {}
@ -70,7 +73,7 @@ def _JSONApiSpec(query):
if 'sort' in query:
for index in query['sort'].split(','):
try:
sort_field = getattr(flat_model.Flat, index.lstrip('-'))
sort_field = getattr(model, index.lstrip('-'))
except AttributeError:
raise ValueError(
"Invalid sorting key provided: {}.".format(index)
@ -78,6 +81,16 @@ def _JSONApiSpec(query):
if index.startswith('-'):
sort_field = sort_field.desc()
sorting.append(sort_field)
# Default sorting options
if not sorting and default_sorting:
try:
sorting.append(getattr(model, default_sorting))
except AttributeError:
raise ValueError(
"Invalid default sorting key provided: {}.".format(
default_sorting
)
)
return filters, page_number, page_size, sorting
@ -128,6 +141,7 @@ def index_v1():
GET /api/v1/
"""
return {
"opendata": "/api/v1/opendata",
"flats": "/api/v1/flats",
"flat": "/api/v1/flat/:id",
"search": "/api/v1/search",
@ -170,7 +184,9 @@ def flats_v1(config, db):
try:
try:
filters, page_number, page_size, sorting = _JSONApiSpec(
bottle.request.query
bottle.request.query,
flat_model.Flat,
default_sorting='cost'
)
except ValueError as exc:
return JSONError(400, str(exc))
@ -339,7 +355,9 @@ def search_v1(db, config):
try:
filters, page_number, page_size, sorting = _JSONApiSpec(
bottle.request.query
bottle.request.query,
flat_model.Flat,
default_sorting='cost'
)
except ValueError as exc:
return JSONError(400, str(exc))
@ -409,3 +427,75 @@ def ics_feed_v1(config, db):
pass
return cal.serialize()
def opendata_index_v1():
"""
API v1 data index route.
Example::
GET /api/v1/opendata
"""
return {
"postal_codes": "/api/v1/opendata/postal_codes"
}
def opendata_postal_codes_v1(db):
"""
API v1 data postal codes route.
Example::
GET /api/v1/opendata/postal_codes
.. 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 postal codes data from opendata.
"""
if bottle.request.method == 'OPTIONS':
# CORS
return {}
try:
try:
filters, page_number, page_size, sorting = _JSONApiSpec(
bottle.request.query,
PostalCode,
default_sorting='postal_code'
)
except ValueError as exc:
return JSONError(400, str(exc))
db_query = db.query(PostalCode).filter_by(**filters).order_by(*sorting)
postal_codes = [
x.json_api_repr() for x 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 {
"data": postal_codes,
"page": page_number,
"items_per_page": page_size if page_size else len(postal_codes)
}
except Exception as exc: # pylint: disable= broad-except
return JSONError(500, str(exc))