"""Flask-RAML (REST API Markup Language) API server with parameter conversion, response encoding, and examples.""" __all__ = 'MimeEncoders API Loader Converter Content ApiError RequestError ParameterError AuthError'.split() __version__ = '0.2.2' from sys import exc_info from operator import itemgetter from functools import wraps from flask import abort, request, has_request_context, Response from flask.app import HTTPException from werkzeug.http import HTTP_STATUS_CODES from werkzeug.datastructures import MultiDict import flask.ext.mime_encoders import flask.ext.mime_encoders.json import raml from raml import Content, ApiError, RequestError, ParameterError, AuthError # Export raml module properties. class MimeEncoders(flask.ext.mime_encoders.MimeEncoders): default = flask.ext.mime_encoders.MimeEncoders.json class Converter(raml.Converter): log = True def convert_params(self, specification, params): if isinstance(params, MultiDict): params, multidict = {}, params for key, values in multidict.iteritems(): params[key] = values[0] if len(values) == 1 else values return super(Converter, self).convert_params(specification, params) class Loader(raml.Loader): log = True spec_param_template = '{{{name}}}' flask_param_template = '<{flask_type}:{name}>' flask_types = { 'integer': 'int', 'number': 'float', 'string': 'string', 'boolean': 'bool', 'date': 'date', } def get_resource_uri(self, resource): uri = resource['relativeUri'] if 'uriParameters' in resource: spec_format, flask_format = self.spec_param_template.format, self.flask_param_template.format for name, param in resource['uriParameters'].items(): param['name'] = name param['flask_type'] = self.flask_types[param['type']] uri = uri.replace(spec_format(**param), flask_format(**param)) resource['allUriParameters'].update(resource['uriParameters']) return uri class API(raml.API): """Flask API. """ plugins = dict(raml.API.plugins, loader=Loader, encoders=MimeEncoders, converter=Converter) auth = None logger_name = '{app}:api' decode_request = True encode_response = True convert_query_params = True convert_uri_params = True endpoint_template = '{api}{resource}_{methods}' requested_response_status_header = 'X-Test-Response-Status' default_error_status = 500 default_error_message = 'internal server error' config_exclude = raml.API.config_exclude.union('unhandled_uris unhandled_methods'.split()) def __init__(self, app, path, uri=None, id=None, log=None, **options): self.app = app self.views = {} if log is None or isinstance(log, basestring): log = app.logger.manager.getLogger(log or options.get('logger_name', self.logger_name).format(app=app.name)) super(API, self).__init__(path, uri, id, log, **options) self.default_mimetype = self.encoders.default.mimetype if self.auth and getattr(self.auth, 'log', None) is True: self.auth.log = log if log: log.debug(repr(self)) @property def unhandled_uris(self): return [uri for uri in self.api if uri not in self.views] @property def unhandled_methods(self): result = [] for uri, resource in self.api.iteritems(): methods = self.views.get(uri, ()) result.extend((uri, method) for method in resource['methodsByName'] if method.upper() not in methods) return result def abort(self, status, error=None, encoder=True): (self.log.exception if self.app.debug and exc_info()[0] else self.log.error)( '%r %s %s >> %s', status, request.method, request.path, error or HTTP_STATUS_CODES.get(status, 'Unknown Error')) if error: return abort(status, description=error, response = self.encoders[encoder].make_response( dict(status=status, error=error), status=status)) else: return abort(status) def add_route(self, resource, view, methods=None, endpoint=None, **options): return self.route(resource, methods, endpoint, **options)(view) def route(self, resource, methods=None, endpoint=None, **options): resource = self.get_resource(resource) uri = resource['uri'] config = dict(self.config, **options) if options else self.config methods = self.get_resource_methods(resource, methods) if endpoint is None: endpoint = self.get_endpoint(resource, methods, self.endpoint_template) auth = config['auth'] decorate = config.get('decorate', None) decode_request = self.encoders[config['decode_request']] encode_response = self.encoders[config['encode_response']] convert_uri_params = config['convert_uri_params'] convert_query_params = config['convert_query_params'] def decorator(view): self.log.debug('map %s %s %s', self.id, '/'.join(sorted(methods)), uri) @wraps(view) def decorated_view(**uri_params): try: url = request.path self.log.info('%s %s << %s [%s|%s|%s]', request.method, url, uri_params if self.app.debug or not uri_params else '{...}', len(uri_params) or '-', len(request.args) or '-', len(request.data) or '-') if auth: auth.authorize(uri_params, request) method = self.get_method_spec(resource, request.method) if convert_uri_params: uri_params = self.converter.convert_params(resource['allUriParameters'], uri_params) if convert_query_params: if 'queryParameters' in method: uri_params.update(self.converter.convert_params(method['queryParameters'], request.args)) elif request.args: self.abort(400, 'resource does not accept query parameters') if uri_params: self.log.debug('%s %s << args: %s [%s]', request.method, url, uri_params, len(uri_params) or '-') if decode_request: self.log.debug('%s %s << data: %s [%s]', request.method, url, decode_request.name, len(request.data)) uri_params.update(decode_request.get_request_data()) response = view(**uri_params) if encode_response and not isinstance(response, (Response, basestring)): response = encode_response.make_response(response) self.log.debug('%s %s >> %s [%s:%s] (%s)', request.method, url, encode_response.name, type(response.response), len(response.response), response.status) return response except HTTPException as error: if error.response: # Use exception response if it was already created, either by API.abort(), or custom way. raise else: # Otherwise, create a custom response via API.abort(). self.abort(error.code, error.description) except ApiError as error: self.abort(error.status, error.message) except Exception as error: msg = str(error) if self.app.debug else self.default_error_message self.abort(self.default_error_status, msg) if decorate: decorated_view = decorate(decorated_view) self.app.add_url_rule(uri, endpoint, decorated_view, methods=methods) for method in methods: self.views.setdefault(uri, {})[method] = decorated_view return decorated_view return decorator def serve(self, view, *args, **kwargs): try: return view(*args, **kwargs) except ApiError as error: self.abort(error.status, error.message) def get_endpoint(self, resource, methods=None, template=None): return (template or self.endpoint_template).format( api=self.id, resource=resource['uniqueId'], methods='+'.join(methods) if methods else 'any', ) def get_response_mimetype(self, response, accept=None, request=request): if accept is None: if request and has_request_context(): accept = map(itemgetter(0), request.accept_mimetypes) return super(API, self).get_response_mimetype(response, accept) def get_default_status(self, status=None, request=request): try: return request.headers[self.requested_response_status_header] except (KeyError, RuntimeError): return super(API, self).get_default_status() def serve_examples(self, **options): for uri, method in self.unhandled_methods: self.serve_example(uri, method) def serve_example(self, resource, methods=None, **options): resource = self.get_resource(resource) for method in self.get_resource_methods(resource, methods): method_spec = self.get_method_spec(resource, method) self.route(resource, method, **options)(self.create_example_view(method_spec)) def create_example_view(self, method_spec): def view(**params): return self.serve(self.get_example, method_spec) return view def get_example(self, method_spec, status=None, mimetype=None): response = self.get_response(method_spec, status) body = self.get_example_body(response, mimetype) headers = self.get_example_headers(response) self.log.info('%s %s: %s %s (%d bytes, %d headers)', method_spec['method'].upper(), method_spec['uri'], response['status'], body.mimetype, len(body), len(headers)) return Response(body.content, status=response['status'], headers=headers, mimetype=body.mimetype)