Add CORS headers in the API response, add a debug option to get verbose debugging from Bottle webserver

This commit is contained in:
Lucas Verney 2017-12-05 17:33:19 +01:00
parent 8a74a79ac2
commit 03d2ad6f7f
4 changed files with 54 additions and 11 deletions

View File

@ -233,4 +233,5 @@ def serve(config):
# standard logging # standard logging
server = web_app.QuietWSGIRefServer server = web_app.QuietWSGIRefServer
app.run(host=config["host"], port=config["port"], server=server) app.run(host=config["host"], port=config["port"], server=server,
debug=config["debug"])

View File

@ -63,6 +63,8 @@ DEFAULT_CONFIG = {
"search_index": None, "search_index": None,
# Web app port # Web app port
"port": 8080, "port": 8080,
# Debug mode for webserver
"debug": False,
# Web app host to listen on # Web app host to listen on
"host": "127.0.0.1", "host": "127.0.0.1",
# Web server to use to serve the webapp (see Bottle deployment doc) # Web server to use to serve the webapp (see Bottle deployment doc)
@ -126,6 +128,7 @@ def validate_config(config, check_with_data):
assert config["database"] is None or isinstance(config["database"], str) # noqa: E501 assert config["database"] is None or isinstance(config["database"], str) # noqa: E501
assert isinstance(config["debug"], bool)
assert isinstance(config["port"], int) assert isinstance(config["port"], int)
assert isinstance(config["host"], str) assert isinstance(config["host"], str)
assert config["webserver"] is None or isinstance(config["webserver"], str) # noqa: E501 assert config["webserver"] is None or isinstance(config["webserver"], str) # noqa: E501

View File

@ -42,7 +42,6 @@ def _serve_static_file(filename):
) )
) )
def get_app(config): def get_app(config):
""" """
Get a Bottle app instance with all the routes set-up. Get a Bottle app instance with all the routes set-up.
@ -51,7 +50,7 @@ def get_app(config):
""" """
get_session = database.init_db(config["database"], config["search_index"]) get_session = database.init_db(config["database"], config["search_index"])
app = bottle.default_app() app = bottle.Bottle()
app.install(DatabasePlugin(get_session)) app.install(DatabasePlugin(get_session))
app.install(ConfigPlugin(config)) app.install(ConfigPlugin(config))
app.config.setdefault("canister.log_level", logging.root.level) app.config.setdefault("canister.log_level", logging.root.level)
@ -60,24 +59,40 @@ def get_app(config):
app.install(canister.Canister()) app.install(canister.Canister())
# Use DateAwareJSONEncoder to dump JSON strings # Use DateAwareJSONEncoder to dump JSON strings
# From http://stackoverflow.com/questions/21282040/bottle-framework-how-to-return-datetime-in-json-response#comment55718456_21282666. pylint: disable=locally-disabled,line-too-long # From http://stackoverflow.com/questions/21282040/bottle-framework-how-to-return-datetime-in-json-response#comment55718456_21282666. pylint: disable=locally-disabled,line-too-long
bottle.install( app.install(
bottle.JSONPlugin( bottle.JSONPlugin(
json_dumps=functools.partial(json.dumps, cls=DateAwareJSONEncoder) json_dumps=functools.partial(json.dumps, cls=DateAwareJSONEncoder)
) )
) )
# API v1 routes # Enable CORS
app.route("/api/v1/", "GET", api_routes.index_v1) @app.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')] = '*'
bottle.response.headers[str('Access-Control-Allow-Methods')] = (
'PUT, GET, POST, DELETE, OPTIONS'
)
bottle.response.headers[str('Access-Control-Allow-Headers')] = (
'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
)
app.route("/api/v1/time_to_places", "GET", # API v1 routes
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) api_routes.time_to_places_v1)
app.route("/api/v1/flats", "GET", api_routes.flats_v1) app.route("/api/v1/flats", ["GET", "OPTIONS"], api_routes.flats_v1)
app.route("/api/v1/flats/:flat_id", "GET", api_routes.flat_v1) app.route("/api/v1/flats/:flat_id", ["GET", "OPTIONS"], api_routes.flat_v1)
app.route("/api/v1/flats/:flat_id", "PATCH", app.route("/api/v1/flats/:flat_id", ["PATCH", "OPTIONS"],
api_routes.update_flat_v1) api_routes.update_flat_v1)
app.route("/api/v1/ics/visits.ics", "GET", app.route("/api/v1/ics/visits.ics", ["GET", "OPTIONS"],
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

@ -103,6 +103,10 @@ def flats_v1(config, db):
:return: The available flats objects in a JSON ``data`` dict. :return: The available flats objects in a JSON ``data`` dict.
""" """
if bottle.request.method == 'OPTIONS':
# CORS
return ''
try: try:
db_query = db.query(flat_model.Flat) db_query = db.query(flat_model.Flat)
@ -139,6 +143,10 @@ def flat_v1(flat_id, config, db):
:return: The flat object in a JSON ``data`` dict. :return: The flat object in a JSON ``data`` dict.
""" """
if bottle.request.method == 'OPTIONS':
# CORS
return {}
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()
@ -171,6 +179,10 @@ def update_flat_v1(flat_id, config, db):
:return: The new flat object in a JSON ``data`` dict. :return: The new flat object in a JSON ``data`` dict.
""" """
if bottle.request.method == 'OPTIONS':
# CORS
return {}
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:
@ -204,6 +216,10 @@ def time_to_places_v1(config):
:return: The JSON dump of the places to compute time to (dict of places :return: The JSON dump of the places to compute time to (dict of places
names mapped to GPS coordinates). names mapped to GPS coordinates).
""" """
if bottle.request.method == 'OPTIONS':
# CORS
return {}
try: try:
places = {} places = {}
for constraint_name, constraint in config["constraints"].items(): for constraint_name, constraint in config["constraints"].items():
@ -231,6 +247,10 @@ def search_v1(db, config):
:return: The matching flat objects in a JSON ``data`` dict. :return: The matching flat objects in a JSON ``data`` dict.
""" """
if bottle.request.method == 'OPTIONS':
# CORS
return {}
try: try:
try: try:
query = json.load(bottle.request.body)["query"] query = json.load(bottle.request.body)["query"]
@ -260,6 +280,10 @@ def ics_feed_v1(config, db):
:return: The ICS feed for the visits. :return: The ICS feed for the visits.
""" """
if bottle.request.method == 'OPTIONS':
# CORS
return {}
cal = vobject.iCalendar() cal = vobject.iCalendar()
try: try:
flats_with_visits = db.query(flat_model.Flat).filter( flats_with_visits = db.query(flat_model.Flat).filter(