Continue server side code
This commit is contained in:
parent
2d27e72b33
commit
d59d84af43
@ -1,68 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
import json
|
||||
|
||||
import arrow
|
||||
import bottle
|
||||
import peewee
|
||||
|
||||
db = peewee.SqliteDatabase('reports.db')
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
from server import routes
|
||||
from server.models import db, Report
|
||||
|
||||
|
||||
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