This repository has been archived by the owner on Oct 27, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
161 additions
and
333 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
|
||
def cors(api, router): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,166 @@ | ||
import re | ||
from urllib.parse import urljoin | ||
|
||
from pulsar.apps.wsgi import Router | ||
from apispec import APISpec | ||
from apispec.utils import load_yaml_from_docstring | ||
|
||
from apispec import Path, APISpec | ||
from apispec import utils | ||
from marshmallow import Schema, fields | ||
|
||
from .schema import api_schema | ||
from lux.core import JsonRouter | ||
|
||
from pulsar.apps import wsgi | ||
|
||
|
||
default_plugins = ['apispec.ext.marshmallow'] | ||
METHODS = ['head', 'get', 'post', 'put', 'patch', 'delete'] | ||
|
||
RE_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>') | ||
|
||
class APISchema(Schema): | ||
BASE_URL = fields.String(required=True) | ||
TITLE = fields.String(required=True) | ||
VERSION = fields.String(default='0.1.0') | ||
SPEC_PLUGINS = fields.List(fields.String(), default=default_plugins) | ||
PRODUCES = fields.List(fields.String(), default=['application/json']) | ||
SPEC_PATH = fields.String(default='spec') | ||
MODEL = fields.String(default='*') | ||
CORS = fields.Boolean(default=True) | ||
|
||
def register_api_spec(app, cfg): | ||
schema = api_schema.load(cfg) | ||
title = cfg.get('TITLE') | ||
if title: | ||
spec = APISpec(title, | ||
version=cfg.get('VERSION'), | ||
plugins=cfg.get('SPEC_PLUGINS')) | ||
spec = APISpec(cfg.get('TITLE'), | ||
version=cfg.get('VERSION'), | ||
plugins=cfg.get('SPEC_PLUGINS')) | ||
spec.app = app | ||
|
||
app.api_spec = spec | ||
api_schema = APISchema() | ||
RE_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>') | ||
|
||
|
||
def api_operations(api, router): | ||
"""Get all API operations""" | ||
operations = {} | ||
for method in METHODS: | ||
handle = getattr(router, method, None) | ||
if not hasattr(handle, '__call__'): | ||
continue | ||
|
||
def setup(spec): | ||
"""Setup for the plugin.""" | ||
spec.register_path_helper(path_from_view) | ||
doc = load_yaml_from_docstring(handle.__doc__) | ||
parameters = getattr(handle, 'parameters', None) | ||
if parameters: | ||
doc = parameters.add_to(api, doc) | ||
if doc: | ||
operations[method] = doc | ||
|
||
return operations | ||
|
||
def pulsarpath2openapi(path): | ||
|
||
def rule2openapi(path): | ||
"""Convert a Flask URL rule to an OpenAPI-compliant path. | ||
:param str path: Flask path template. | ||
""" | ||
return RE_URL.sub(r'{\1}', path) | ||
|
||
|
||
def path_from_view(spec, view, **kwargs): | ||
"""Path helper that allows passing a pulsar Routers""" | ||
if not isinstance(view, Router): | ||
return | ||
path = pulsarpath2openapi(view.rule) | ||
app = spec.app | ||
app_root = app.config['API_URL'] or '/' | ||
path = urljoin(app_root.rstrip('/') + '/', path.lstrip('/')) | ||
operations = utils.load_operations_from_docstring(view.__doc__) | ||
path = Path(path=path, operations=operations) | ||
return path | ||
|
||
class route(wsgi.route): | ||
|
||
class Response: | ||
def __init__(self, *args, responses=None, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.api = api_parameters(responses=responses) | ||
|
||
def __init__(self, status_code, doc): | ||
self.status_code = status_code | ||
self.doc = doc | ||
def __call__(self, method): | ||
method = super().__call__(method) | ||
return self.api(method) | ||
|
||
|
||
class apidoc: | ||
response = Response | ||
|
||
def __init__(self, doc=None, responses=None): | ||
self.doc = doc or '' | ||
class api_parameters: | ||
"""Inject api parameters to an endpoint handler | ||
""" | ||
def __init__(self, form=None, path=None, query=None, body=None, | ||
responses=None): | ||
self.form = form | ||
self.path = path | ||
self.query = query | ||
self.body = body | ||
self.responses = responses | ||
|
||
def __call__(self, handler): | ||
handler.__openapi__ = self | ||
return handler | ||
def __call__(self, f): | ||
f.parameters = self | ||
return f | ||
|
||
def add_to(self, api, doc): | ||
doc = doc if doc is not None else {} | ||
parameters = doc.get('parameters', []) | ||
processed = set() | ||
if self.path: | ||
self._extend(api, self.path, parameters, 'path', processed) | ||
if self.query: | ||
self._extend(api, self.query, parameters, 'query', processed) | ||
if self.form: | ||
self._extend(api, self.form, parameters, 'formData', processed) | ||
if isinstance(self.body, as_body): | ||
obj = self.body(api.spec) | ||
processed.add(obj['name']) | ||
parameters.append(obj) | ||
if parameters: | ||
doc['parameters'] = parameters | ||
return doc | ||
|
||
def _extend(self, api, schema, parameters, loc, processed): | ||
spec = self._spec(api) | ||
spec.definition('param', schema=schema) | ||
params = spec.to_dict()['definitions']['param'] | ||
properties = params['properties'] | ||
required = set(params.get('required') or ()) | ||
for name, obj in properties.items(): | ||
if name in processed: | ||
LOG.error('Parameter "%s" already in api path parameter list', | ||
name) | ||
continue | ||
processed.add(name) | ||
obj['name'] = name | ||
obj['in'] = loc | ||
if name in required: | ||
obj['required'] = True | ||
parameters.append(obj) | ||
|
||
def _spec(self, api): | ||
return APISpec('', '', plugins=list(api.spec.plugins)) | ||
|
||
def _as_body(self, definition, parameters): | ||
if not isinstance(definition, dict): | ||
body = dict(schema={"$ref": "#/definitions/%s" % definition}) | ||
if 'name' not in body: | ||
body['name'] = 'body' | ||
body['in'] = 'body' | ||
body['required'] = True | ||
parameters.add(body['name']) | ||
return body | ||
|
||
|
||
class as_body: | ||
|
||
def __init__(self, schema, **obj): | ||
self.obj = obj | ||
self.schema_cls = schema if isinstance(schema, type) else type(schema) | ||
|
||
def __call__(self, spec): | ||
definition = None | ||
plg = spec.plugins.get('apispec.ext.marshmallow') | ||
if plg and 'refs' in plg: | ||
definition = plg['refs'].get(self.schema_cls) | ||
if not definition: | ||
LOG.warning('Could not add body parameter %s' % self.schema_cls) | ||
else: | ||
obj = self.obj.copy() | ||
obj['schema'] = {"$ref": "#/definitions/%s" % definition} | ||
obj['in'] = 'body' | ||
if 'name' not in obj: | ||
obj['name'] = definition.lower() | ||
return obj | ||
|
||
|
||
class Specification(JsonRouter): | ||
api = None | ||
|
||
def get(self, request): | ||
if self.api: | ||
pass | ||
spec = self.api.spec.to_dict() | ||
if 'host' not in spec: | ||
spec['host'] = request.get_host() | ||
if 'schemes' not in spec: | ||
spec['schemes'] = [request.scheme] | ||
return request.json_response(spec) |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.