Continue server side code
This commit is contained in:
parent
2d27e72b33
commit
d59d84af43
@ -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
102
server/jsonapi.py
Normal 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
41
server/models.py
Normal 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
106
server/routes.py
Normal 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"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user