Cleaner API

This commit is contained in:
Lucas Verney 2017-12-05 12:20:40 +01:00
parent 9424f81959
commit b285c270aa
4 changed files with 132 additions and 210 deletions

View File

@ -13,6 +13,7 @@ import arrow
from sqlalchemy import ( from sqlalchemy import (
Column, DateTime, Enum, Float, SmallInteger, String, Text Column, DateTime, Enum, Float, SmallInteger, String, Text
) )
from sqlalchemy.orm import validates
from flatisfy.database.base import BASE from flatisfy.database.base import BASE
from flatisfy.database.types import MagicJSON from flatisfy.database.types import MagicJSON
@ -96,6 +97,62 @@ class Flat(BASE):
# Date for visit # Date for visit
visit_date = Column(DateTime) visit_date = Column(DateTime)
@validates('utilities')
def validate_utilities(self, _, utilities):
"""
Utilities validation method
"""
if isinstance(utilities, FlatUtilities):
return utilities
if utilities == "C.C.":
return FlatUtilities.included
elif utilities == "H.C.":
return FlatUtilities.excluded
else:
return FlatUtilities.unknown
@validates("status")
def validate_status(self, _, status):
"""
Status validation method
"""
if isinstance(status, FlatStatus):
return status
try:
return getattr(FlatStatus, status)
except (AttributeError, TypeError):
LOGGER.warn("Unkown flat status %s, ignoring it.",
status)
return self.status.default.arg
@validates("notation")
def validate_status(self, _, notation):
"""
Notation validation method
"""
try:
notation = int(notation)
assert notation >= 0 and notation <= 5
except (ValueError, AssertionError):
raise ValueError('notation should be an integer between 0 and 5')
return notation
@validates("date")
def validate_date(self, _, date):
"""
Date validation method
"""
return arrow.get(date).naive
@validates("visit_date")
def validate_visit_date(self, _, visit_date):
"""
Visit date validation method
"""
return arrow.get(visit_date).naive
@staticmethod @staticmethod
def from_dict(flat_dict): def from_dict(flat_dict):
""" """
@ -119,29 +176,6 @@ class Flat(BASE):
) )
del flat_dict["flatisfy"] del flat_dict["flatisfy"]
# Handle utilities field
if not isinstance(flat_dict["utilities"], FlatUtilities):
if flat_dict["utilities"] == "C.C.":
flat_dict["utilities"] = FlatUtilities.included
elif flat_dict["utilities"] == "H.C.":
flat_dict["utilities"] = FlatUtilities.excluded
else:
flat_dict["utilities"] = FlatUtilities.unknown
# Handle status field
flat_status = flat_dict.get("status", "new")
if not isinstance(flat_status, FlatStatus):
try:
flat_dict["status"] = getattr(FlatStatus, flat_status)
except AttributeError:
if "status" in flat_dict:
del flat_dict["status"]
LOGGER.warn("Unkown flat status %s, ignoring it.",
flat_status)
# Handle date field
flat_dict["date"] = arrow.get(flat_dict["date"]).naive
flat_object = Flat() flat_object = Flat()
# Using a __dict__.update() call to make it work even if there are # Using a __dict__.update() call to make it work even if there are
# extra keys in flat_dict which are not valid kwargs for Flat model. # extra keys in flat_dict which are not valid kwargs for Flat model.
@ -151,7 +185,6 @@ class Flat(BASE):
def __repr__(self): def __repr__(self):
return "<Flat(id=%s, urls=%s)>" % (self.id, self.urls) return "<Flat(id=%s, urls=%s)>" % (self.id, self.urls)
def json_api_repr(self): def json_api_repr(self):
""" """
Return a dict representation of this flat object that is JSON Return a dict representation of this flat object that is JSON

View File

@ -73,20 +73,11 @@ def get_app(config):
api_routes.time_to_places_v1) api_routes.time_to_places_v1)
app.route("/api/v1/flats", "GET", api_routes.flats_v1) app.route("/api/v1/flats", "GET", api_routes.flats_v1)
app.route("/api/v1/flats/status/:status", "GET", app.route("/api/v1/flats/:flat_id", "GET", api_routes.flat_v1)
api_routes.flats_by_status_v1) app.route("/api/v1/flats/:flat_id", "PATCH",
api_routes.update_flat_v1)
app.route("/api/v1/flat/:flat_id", "GET", api_routes.flat_v1) app.route("/api/v1/ics/visits.ics", "GET",
app.route("/api/v1/flat/:flat_id/status", "POST",
api_routes.update_flat_status_v1)
app.route("/api/v1/flat/:flat_id/notes", "POST",
api_routes.update_flat_notes_v1)
app.route("/api/v1/flat/:flat_id/notation", "POST",
api_routes.update_flat_notation_v1)
app.route("/api/v1/flat/:flat_id/visit_date", "POST",
api_routes.update_flat_visit_date_v1)
app.route("/api/v1/visits.ics", "GET",
api_routes.ics_feed_v1) api_routes.ics_feed_v1)
app.route("/api/v1/search", "POST", api_routes.search_v1) app.route("/api/v1/search", "POST", api_routes.search_v1)

View File

@ -46,7 +46,7 @@ export const getFlats = function (callback) {
export const getFlat = function (flatId, callback) { export const getFlat = function (flatId, callback) {
fetch( fetch(
'/api/v1/flat/' + encodeURIComponent(flatId), '/api/v1/flats/' + encodeURIComponent(flatId),
{ credentials: 'same-origin' } { credentials: 'same-origin' }
) )
.then(function (response) { .then(function (response) {
@ -61,10 +61,10 @@ export const getFlat = function (flatId, callback) {
export const updateFlatStatus = function (flatId, newStatus, callback) { export const updateFlatStatus = function (flatId, newStatus, callback) {
fetch( fetch(
'/api/v1/flat/' + encodeURIComponent(flatId) + '/status', '/api/v1/flats/' + encodeURIComponent(flatId),
{ {
credentials: 'same-origin', credentials: 'same-origin',
method: 'POST', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
@ -79,10 +79,10 @@ export const updateFlatStatus = function (flatId, newStatus, callback) {
export const updateFlatNotes = function (flatId, newNotes, callback) { export const updateFlatNotes = function (flatId, newNotes, callback) {
fetch( fetch(
'/api/v1/flat/' + encodeURIComponent(flatId) + '/notes', '/api/v1/flats/' + encodeURIComponent(flatId),
{ {
credentials: 'same-origin', credentials: 'same-origin',
method: 'POST', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
@ -97,10 +97,10 @@ export const updateFlatNotes = function (flatId, newNotes, callback) {
export const updateFlatNotation = function (flatId, newNotation, callback) { export const updateFlatNotation = function (flatId, newNotation, callback) {
fetch( fetch(
'/api/v1/flat/' + encodeURIComponent(flatId) + '/notation', '/api/v1/flats/' + encodeURIComponent(flatId),
{ {
credentials: 'same-origin', credentials: 'same-origin',
method: 'POST', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
@ -115,10 +115,10 @@ export const updateFlatNotation = function (flatId, newNotation, callback) {
export const updateFlatVisitDate = function (flatId, newVisitDate, callback) { export const updateFlatVisitDate = function (flatId, newVisitDate, callback) {
fetch( fetch(
'/api/v1/flat/' + encodeURIComponent(flatId) + '/visit_date', '/api/v1/flats/' + encodeURIComponent(flatId),
{ {
credentials: 'same-origin', credentials: 'same-origin',
method: 'POST', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },

View File

@ -8,8 +8,8 @@ from __future__ import (
import datetime import datetime
import json import json
import re
import arrow
import bottle import bottle
import vobject import vobject
@ -18,6 +18,12 @@ from flatisfy.models import flat as flat_model
from flatisfy.models.postal_code import PostalCode from flatisfy.models.postal_code import PostalCode
def JSONError(error_code, error_str):
bottle.response.status = error_code
bottle.response.content_type = "application/json"
return json.dumps(dict(error=error_str, status_code=error_code))
def _serialize_flat(flat, config): def _serialize_flat(flat, config):
""" """
Serialize a flat for JSON API. Serialize a flat for JSON API.
@ -65,7 +71,8 @@ def index_v1():
"flats": "/api/v1/flats", "flats": "/api/v1/flats",
"flat": "/api/v1/flat/:id", "flat": "/api/v1/flat/:id",
"search": "/api/v1/search", "search": "/api/v1/search",
"time_to_places": "/api/v1/time_to/places" "ics": "/api/v1/ics/visits.ics",
"time_to_places": "/api/v1/time_to_places"
} }
@ -75,57 +82,43 @@ def flats_v1(config, db):
GET /api/v1/flats GET /api/v1/flats
.. note:: Filtering can be done through the ``filter`` GET param, according
to JSON API spec (http://jsonapi.org/recommendations/#filtering).
:return: The available flats objects in a JSON ``data`` dict. :return: The available flats objects in a JSON ``data`` dict.
""" """
try: try:
db_query = db.query(flat_model.Flat)
# Handle filtering according to JSON API spec
FILTER_RE = re.compile(r"filter\[([A-z0-9_]+)\]")
filters = {}
for param in bottle.request.query:
filter_match = FILTER_RE.match(param)
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
flats = [ flats = [
_serialize_flat(flat, config) _serialize_flat(flat, config)
for flat in db.query(flat_model.Flat).all() for flat in db_query
] ]
return { return {
"data": flats "data": flats
} }
except Exception as e: except Exception as exc:
return { return JSONError(500, str(exc))
"error": str(e)
}
def flats_by_status_v1(status, config, db):
"""
API v1 flats route with a specific status:
GET /api/v1/flats/status/:status
:return: The matching flats objects in a JSON ``data`` dict.
"""
try:
flats = [
_serialize_flat(flat, config)
for flat in (
db.query(flat_model.Flat)
.filter_by(status=getattr(flat_model.FlatStatus, status))
.all()
)
]
return {
"data": flats
}
except AttributeError:
return bottle.HTTPError(400, "Invalid status provided.")
except Exception as e:
return {
"error": str(e)
}
def flat_v1(flat_id, config, db): def flat_v1(flat_id, config, db):
""" """
API v1 flat route: API v1 flat route:
GET /api/v1/flat/:flat_id GET /api/v1/flats/:flat_id
:return: The flat object in a JSON ``data`` dict. :return: The flat object in a JSON ``data`` dict.
""" """
@ -133,141 +126,50 @@ def flat_v1(flat_id, config, db):
flat = db.query(flat_model.Flat).filter_by(id=flat_id).first() flat = db.query(flat_model.Flat).filter_by(id=flat_id).first()
if not flat: if not flat:
return bottle.HTTPError(404, "No flat with id {}.".format(flat_id)) return JSONError(404, "No flat with id {}.".format(flat_id))
return { return {
"data": _serialize_flat(flat, config) "data": _serialize_flat(flat, config)
} }
except Exception as e: except Exception as exc:
return { return JSONError(500, str(exc))
"error": str(e)
}
def update_flat_status_v1(flat_id, config, db): def update_flat_v1(flat_id, config, db):
""" """
API v1 route to update flat status: API v1 route to update flat status:
POST /api/v1/flat/:flat_id/status PATCH /api/v1/flat/:flat_id
Data: {
"status": "NEW_STATUS"
}
:return: The new flat object in a JSON ``data`` dict.
"""
try:
flat = db.query(flat_model.Flat).filter_by(id=flat_id).first()
if not flat:
return bottle.HTTPError(404, "No flat with id {}.".format(flat_id))
try:
flat.status = getattr(
flat_model.FlatStatus, json.load(bottle.request.body)["status"]
)
except (AttributeError, ValueError, KeyError):
return bottle.HTTPError(400, "Invalid status provided.")
return {
"data": _serialize_flat(flat, config)
}
except Exception as e:
return {
"error": str(e)
}
def update_flat_notes_v1(flat_id, config, db):
"""
API v1 route to update flat notes:
POST /api/v1/flat/:flat_id/notes
Data: {
"notes": "NEW_NOTES"
}
:return: The new flat object in a JSON ``data`` dict.
"""
try:
flat = db.query(flat_model.Flat).filter_by(id=flat_id).first()
if not flat:
return bottle.HTTPError(404, "No flat with id {}.".format(flat_id))
try:
flat.notes = json.load(bottle.request.body)["notes"]
except (ValueError, KeyError):
return bottle.HTTPError(400, "Invalid notes provided.")
return {
"data": _serialize_flat(flat, config)
}
except Exception as e:
return {
"error": str(e)
}
def update_flat_notation_v1(flat_id, config, db):
"""
API v1 route to update flat notation:
POST /api/v1/flat/:flat_id/notation
Data: {
"notation": "NEW_NOTATION"
}
:return: The new flat object in a JSON ``data`` dict.
"""
try:
flat = db.query(flat_model.Flat).filter_by(id=flat_id).first()
if not flat:
return bottle.HTTPError(404, "No flat with id {}.".format(flat_id))
try:
flat.notation = json.load(bottle.request.body)["notation"]
assert flat.notation >= 0 and flat.notation <= 5
except (AssertionError, ValueError, KeyError):
return bottle.HTTPError(400, "Invalid notation provided.")
return {
"data": _serialize_flat(flat, config)
}
except Exception as e:
return {
"error": str(e)
}
def update_flat_visit_date_v1(flat_id, config, db):
"""
API v1 route to update flat date of visit:
POST /api/v1/flat/:flat_id/visit_date
Data: { Data: {
"status": "NEW_STATUS",
"visit_date": "ISO8601 DATETIME" "visit_date": "ISO8601 DATETIME"
} }
.. note:: The keys in the data sent are same keys as in ``Flat`` model. You
can provide any subset of them to update part of the flat infos.
:return: The new flat object in a JSON ``data`` dict. :return: The new flat object in a JSON ``data`` dict.
""" """
try: try:
flat = db.query(flat_model.Flat).filter_by(id=flat_id).first() flat = db.query(flat_model.Flat).filter_by(id=flat_id).first()
if not flat: if not flat:
return bottle.HTTPError(404, "No flat with id {}.".format(flat_id)) return JSONError(404, "No flat with id {}.".format(flat_id))
try: try:
visit_date = json.load(bottle.request.body)["visit_date"] json_body = json.load(bottle.request.body)
if visit_date: for k, v in json_body.items():
visit_date = arrow.get(visit_date).naive setattr(flat, k, v)
flat.visit_date = visit_date except ValueError as exc:
except (arrow.parser.ParserError, ValueError, KeyError): return JSONError(
return bottle.HTTPError(400, "Invalid visit date provided.") 400,
"Invalid payload provided: {}.".format(str(exc))
)
return { return {
"data": _serialize_flat(flat, config) "data": _serialize_flat(flat, config)
} }
except Exception as e: except Exception as exc:
return { return JSONError(500, str(exc))
"error": str(e)
}
def time_to_places_v1(config): def time_to_places_v1(config):
@ -289,10 +191,8 @@ def time_to_places_v1(config):
return { return {
"data": places "data": places
} }
except Exception as e: except Exception as exc:
return { return JSONError(500, str(exc))
"error": str(e)
}
def search_v1(db, config): def search_v1(db, config):
@ -310,7 +210,7 @@ def search_v1(db, config):
try: try:
query = json.load(bottle.request.body)["query"] query = json.load(bottle.request.body)["query"]
except (ValueError, KeyError): except (ValueError, KeyError):
return bottle.HTTPError(400, "Invalid query provided.") return JSONError(400, "Invalid query provided.")
flats_db_query = flat_model.Flat.search_query(db, query) flats_db_query = flat_model.Flat.search_query(db, query)
flats = [ flats = [
@ -321,26 +221,24 @@ def search_v1(db, config):
return { return {
"data": flats "data": flats
} }
except Exception as e: except Exception as exc:
return { return JSONError(500, str(exc))
"error": str(e)
}
def ics_feed_v1(config, db): def ics_feed_v1(config, db):
""" """
API v1 ICS feed of visits route: API v1 ICS feed of visits route:
GET /api/v1/visits.ics GET /api/v1/ics/visits.ics
:return: The ICS feed for the visits. :return: The ICS feed for the visits.
""" """
cal = vobject.iCalendar()
try: try:
flats_with_visits = db.query(flat_model.Flat).filter( flats_with_visits = db.query(flat_model.Flat).filter(
flat_model.Flat.visit_date.isnot(None) flat_model.Flat.visit_date.isnot(None)
).all() )
cal = vobject.iCalendar()
for flat in flats_with_visits: for flat in flats_with_visits:
vevent = cal.add('vevent') vevent = cal.add('vevent')
vevent.add('dtstart').value = flat.visit_date vevent.add('dtstart').value = flat.visit_date
@ -360,7 +258,7 @@ def ics_feed_v1(config, db):
description += '\n{}\n'.format(flat.notes) description += '\n{}\n'.format(flat.notes)
vevent.add('description').value = description vevent.add('description').value = description
return cal.serialize()
except: except:
return '' pass
return cal.serialize()