#!/usr/bin/env python # coding: utf-8 """ Helpers to implement a JSON API with Bottle. """ import arrow import datetime import json import re import bottle import peewee FILTER_RE = re.compile(r"filter\[([A-z0-9_]+?)\](\[([A-z0-9_]+\??)\])?") class DateAwareJSONEncoder(json.JSONEncoder): """ Extend the default JSON encoder to serialize datetimes to iso strings. """ def default(self, o): # pylint: disable=locally-disabled,E0202 if isinstance(o, (datetime.date, datetime.datetime)): return arrow.get(o).isoformat() return json.JSONEncoder.default(self, o) @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, ' 'Authorization' ) 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)) for value in query.getall(param): if isinstance(field, peewee.DateTimeField): value = arrow.get(value).naive elif isinstance(field, peewee.DoubleField): value = float(value) elif isinstance(field, peewee.IntegerField): value = int(value) # Handle operation operation = filter_match.group(3) if operation is None: # Default operation is 'eq' operation = 'eq' # Handle '?' modifier modifier_filter = None if operation.endswith('?'): modifier_filter = (field == None) operation = operation.rstrip('?') if operation == 'eq': operation_filter = (field == value) elif operation == 'ne': operation_filter = (field != value) elif operation == 'gt': operation_filter = (field > value) elif operation == 'ge': operation_filter = (field >= value) elif operation == 'lt': operation_filter = (field < value) elif operation == 'le': operation_filter = (field <= value) else: raise ValueError("Invalid filtering operator provided.") if modifier_filter: filters.append(modifier_filter | operation_filter) else: filters.append(operation_filter) # 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