Continue server side code

This commit is contained in:
Lucas Verney 2018-06-25 17:12:17 +02:00
parent 2d27e72b33
commit d59d84af43
4 changed files with 251 additions and 61 deletions

View File

@ -1,68 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding: utf-8 # coding: utf-8
import json
import arrow
import bottle import bottle
import peewee
db = peewee.SqliteDatabase('reports.db') from server import routes
from server.models import db, Report
class Report(peewee.Model):
type = peewee.CharField(max_length=255)
lat = peewee.DoubleField()
lng = peewee.DoubleField()
datetime = peewee.DateTimeField(
default=arrow.utcnow().replace(microsecond=0)
)
class Meta:
database = db
@bottle.hook('after_request')
def enable_cors():
"""
Add CORS headers at each request.
"""
# The str() call is required as we import unicode_literal and WSGI
# headers list should have plain str type.
bottle.response.headers[str('Access-Control-Allow-Origin')] = str('*')
bottle.response.headers[str('Access-Control-Allow-Methods')] = str(
'PUT, GET, POST, DELETE, OPTIONS, PATCH'
)
bottle.response.headers[str('Access-Control-Allow-Headers')] = str(
'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
)
@bottle.route('/api/v1/reports', ["POST", "OPTIONS"])
def postReport():
"""
Add a new report in database.
"""
if bottle.request.method == 'OPTIONS':
return {}
try:
payload = json.load(bottle.request.body)
except ValueError:
bottle.abort(400, "Invalid JSON payload.")
try:
Report.create(
type=payload['type'],
lat=payload['lat'],
lng=payload['lng']
)
except KeyError:
bottle.abort(400, "Invalid JSON payload.")
return {
"status": "ok"
}
if __name__ == "__main__": if __name__ == "__main__":

102
server/jsonapi.py Normal file
View File

@ -0,0 +1,102 @@
#!/usr/bin/env python
# coding: utf-8
"""
Helpers to implement a JSON API with Bottle.
"""
import json
import re
import bottle
FILTER_RE = re.compile(r"filter\[([A-z0-9_]+)\]")
@bottle.hook('after_request')
def enable_cors():
"""
Add CORS headers at each request.
"""
# The str() call is required as we import unicode_literal and WSGI
# headers list should have plain str type.
bottle.response.headers[str('Access-Control-Allow-Origin')] = str('*')
bottle.response.headers[str('Access-Control-Allow-Methods')] = str(
'PUT, GET, POST, DELETE, OPTIONS, PATCH'
)
bottle.response.headers[str('Access-Control-Allow-Headers')] = str(
'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
)
def JsonApiError(error_code, error_str):
"""
Return an HTTP error with a JSON payload.
:param error_code: HTTP error code to return.
:param error_str: Error as a string.
:returns: Set correct response parameters and returns JSON-serialized error
content.
"""
bottle.response.status = error_code
bottle.response.content_type = "application/json"
return json.dumps(dict(detail=error_str, status=error_code))
def JsonApiParseQuery(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.
"""
# Handle filtering according to JSON API spec
filters = []
for param in query:
filter_match = FILTER_RE.match(param)
if not filter_match:
continue
field = getattr(model, filter_match.group(1))
value = query[filter_match.group(0)]
filters.append(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(model, index.lstrip('-'))
except AttributeError:
raise ValueError(
"Invalid sorting key provided: {}.".format(index)
)
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

41
server/models.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
# coding: utf-8
"""
Models and database definition
"""
import arrow
import peewee
from playhouse.shortcuts import model_to_dict
db = peewee.SqliteDatabase('reports.db')
class BaseModel(peewee.Model):
"""
Common base class for all models
"""
class Meta:
database = db
class Report(BaseModel):
"""
A report object
"""
type = peewee.CharField(max_length=255)
lat = peewee.DoubleField()
lng = peewee.DoubleField()
datetime = peewee.DateTimeField(
default=lambda: arrow.utcnow().replace(microsecond=0).datetime
)
is_open = peewee.BooleanField(default=True)
def to_json(self):
return {
"type": "reports",
"id": self.id,
"attributes": {
k: v for k, v in model_to_dict(self).items()
if k != "id"
}
}

106
server/routes.py Normal file
View File

@ -0,0 +1,106 @@
#!/usr/bin/env python
# coding: utf-8
"""
Routes definitions
"""
import json
import bottle
from server.models import Report
from server import jsonapi
@bottle.route('/api/v1/reports', ["GET", "OPTIONS"])
def get_reports():
"""
API v1 GET reports route.
Example::
GET /api/v1/reports
.. 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 available reports objects in a JSON ``data`` dict.
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
# Handle filtering, pagination and sorting
try:
filters, page_number, page_size, sorting = jsonapi.JsonApiParseQuery(
bottle.request.query,
Report,
default_sorting='id'
)
except ValueError as exc:
return jsonapi.JsonApiError(400, "Invalid parameters: " + str(exc))
# Query
query = Report.select()
if filters:
query = query.where(*filters)
query = query.order_by(*sorting)
if page_number and page_size:
query = query.paginate(page_number, page_size)
return {
"data": [
r.to_json()
for r in query
]
}
@bottle.route('/api/v1/reports', ["POST", "OPTIONS"])
def post_report():
"""
API v1 POST reports route.
Example::
POST /api/v1/reports
{
"type": "toto",
"lat": 32,
"lng": 27
}
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
try:
payload = json.load(bottle.request.body)
except ValueError as exc:
return jsonapi.JsonApiError(400, "Invalid JSON payload: " + str(exc))
try:
Report.create(
type=payload['type'],
lat=payload['lat'],
lng=payload['lng']
)
except KeyError as exc:
return jsonapi.JsonApiError(400, "Invalid report payload: " + str(exc))
return {
"status": "ok"
}