Skip to content

Commit

Permalink
Merge pull request #44 from zalando/release/v0.8
Browse files Browse the repository at this point in the history
Release/v0.8
  • Loading branch information
jmcs committed Jul 31, 2015
2 parents 1d536f7 + 9889f4c commit 1a18044
Show file tree
Hide file tree
Showing 15 changed files with 309 additions and 99 deletions.
2 changes: 2 additions & 0 deletions .checkignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tests
docs
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
language: python
python:
- "pypy"
- "2.7"
- "3.4"
install:
- pip install -e .
Expand Down
30 changes: 24 additions & 6 deletions connexion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ class Api:
Single API that corresponds to a flask blueprint
"""

def __init__(self, swagger_yaml_path: pathlib.Path, base_url: str=None, arguments: dict=None,
swagger_ui: bool=None):
def __init__(self, swagger_yaml_path, base_url=None, arguments=None, swagger_ui=None):
"""
:type swagger_yaml_path: pathlib.Path
:type base_url: str | None
:type arguments: dict | None
:type swagger_ui: bool
"""
self.swagger_yaml_path = pathlib.Path(swagger_yaml_path)
logger.debug('Loading specification: %s', swagger_yaml_path, extra={'swagger_yaml': swagger_yaml_path,
'base_url': base_url,
Expand Down Expand Up @@ -74,7 +79,7 @@ def __init__(self, swagger_yaml_path: pathlib.Path, base_url: str=None, argument
self.add_swagger_ui()
self.add_paths()

def add_operation(self, method: str, path: str, swagger_operation: dict):
def add_operation(self, method, path, swagger_operation):
"""
Adds one operation to the api.
Expand All @@ -86,6 +91,10 @@ def add_operation(self, method: str, path: str, swagger_operation: dict):
A friendly name for the operation. The id MUST be unique among all operations described in the API.
Tools and libraries MAY use the operation id to uniquely identify an operation.
:type method: str
:type path: str
:type swagger_operation: dict
"""
operation = Operation(method=method, path=path, operation=swagger_operation,
app_produces=self.produces, app_security=self.security,
Expand All @@ -95,9 +104,11 @@ def add_operation(self, method: str, path: str, swagger_operation: dict):

self.blueprint.add_url_rule(path, operation.endpoint_name, operation.function, methods=[method])

def add_paths(self, paths: list=None):
def add_paths(self, paths=None):
"""
Adds the paths defined in the specification as endpoints
:type paths: list
"""
paths = paths or self.specification.get('paths', dict())
for path, methods in paths.items():
Expand Down Expand Up @@ -128,7 +139,11 @@ def add_swagger_ui(self):
index_endpoint_name = "{name}_swagger_ui_index".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/ui/', index_endpoint_name, self.swagger_ui_index)

def create_blueprint(self, base_url: str=None) -> flask.Blueprint:
def create_blueprint(self, base_url=None):
"""
:type base_url: str | None
:rtype: flask.Blueprint
"""
base_url = base_url or self.base_url
logger.debug('Creating API blueprint: %s', base_url)
endpoint = utils.flaskify_endpoint(base_url)
Expand All @@ -139,5 +154,8 @@ def swagger_ui_index(self):
return flask.render_template('index.html', api_url=self.base_url)

@staticmethod
def swagger_ui_static(filename: str):
def swagger_ui_static(filename):
"""
:type filename: str
"""
return flask.send_from_directory(str(SWAGGER_UI_PATH), filename)
107 changes: 94 additions & 13 deletions connexion/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
language governing permissions and limitations under the License.
"""


import logging
import pathlib
import types

import flask
import tornado.wsgi
Expand All @@ -29,17 +27,23 @@


class App:

def __init__(self, import_name: str, port: int=5000, specification_dir: pathlib.Path='', server: str=None,
arguments: dict=None, debug: bool=False, swagger_ui: bool=True):
def __init__(self, import_name, port=5000, specification_dir='', server=None, arguments=None, debug=False,
swagger_ui=True):
"""
:param import_name: the name of the application package
:type import_name: str
:param port: port to listen to
:type port: int
:param specification_dir: directory where to look for specifications
:type specification_dir: pathlib.Path | str
:param server: which wsgi server to use
:type server: str | None
:param arguments: arguments to replace on the specification
:type arguments: dict | None
:param debug: include debugging information
:type debug: bool
:param swagger_ui: whether to include swagger ui or not
:type swagger_ui: bool
"""
self.app = flask.Flask(import_name)

Expand All @@ -65,12 +69,25 @@ def __init__(self, import_name: str, port: int=5000, specification_dir: pathlib.
self.arguments = arguments or {}
self.swagger_ui = swagger_ui

def add_api(self, swagger_file: pathlib.Path, base_path: str=None, arguments: dict=None, swagger_ui: bool=None):
@staticmethod
def common_error_handler(e):
"""
:type e: Exception
"""
if not isinstance(e, werkzeug.exceptions.HTTPException):
e = werkzeug.exceptions.InternalServerError()
return problem(title=e.name, detail=e.description, status=e.code)

def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None):
"""
:param swagger_file: swagger file with the specification
:type swagger_file: pathlib.Path
:param base_path: base path where to add this api
:type base_path: str | None
:param arguments: api version specific arguments to replace on the specification
:type arguments: dict | None
:param swagger_ui: whether to include swagger ui or not
:type swagger_ui: bool
"""
swagger_ui = swagger_ui if swagger_ui is not None else self.swagger_ui
logger.debug('Adding API: %s', swagger_file)
Expand All @@ -81,16 +98,80 @@ def add_api(self, swagger_file: pathlib.Path, base_path: str=None, arguments: di
api = connexion.api.Api(yaml_path, base_path, arguments, swagger_ui)
self.app.register_blueprint(api.blueprint)

def add_error_handler(self, error_code: int, function: types.FunctionType):
def add_error_handler(self, error_code, function):
"""
:type error_code: int
:type function: types.FunctionType
"""
self.app.error_handler_spec[None][error_code] = function

@staticmethod
def common_error_handler(e: werkzeug.exceptions.HTTPException):
if not isinstance(e, werkzeug.exceptions.HTTPException):
e = werkzeug.exceptions.InternalServerError()
return problem(title=e.name, detail=e.description, status=e.code)
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
"""
Connects a URL rule. Works exactly like the `route` decorator. If a view_func is provided it will be
registered with the endpoint.
Basically this example::
@app.route('/')
def index():
pass
Is equivalent to the following::
def index():
pass
app.add_url_rule('/', 'index', index)
If the view_func is not provided you will need to connect the endpoint to a view function like so::
app.view_functions['index'] = index
Internally`route` invokes `add_url_rule` so if you want to customize the behavior via subclassing you only need
to change this method.
:param rule: the URL rule as string
:type rule: str
:param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as
endpoint
:type endpoint: str
:param view_func: the function to call when serving a request to the provided endpoint
:type view_func: types.FunctionType
:param options: the options to be forwarded to the underlying `werkzeug.routing.Rule` object. A change
to Werkzeug is handling of method options. methods is a list of methods this rule should be
limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly
`HEAD`).
"""
log_details = {'endpoint': endpoint, 'view_func': view_func.__name__}
log_details.update(options)
logger.debug('Adding %s', rule, extra=log_details)
self.app.add_url_rule(rule, endpoint, view_func, **options)

def route(self, rule, **options):
"""
A decorator that is used to register a view function for a
given URL rule. This does the same thing as `add_url_rule`
but is intended for decorator usage::
@app.route('/')
def index():
return 'Hello World'
:param rule: the URL rule as string
:type rule: str
:param endpoint: the endpoint for the registered URL rule. Flask
itself assumes the name of the view function as
endpoint
:param options: the options to be forwarded to the underlying `werkzeug.routing.Rule` object. A change
to Werkzeug is handling of method options. methods is a list of methods this rule should be
limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly
`HEAD`).
"""
logger.debug('Adding %s with decorator', rule, extra=options)
return self.app.route(rule, **options)

def run(self):
def run(self): # pragma: no cover
# this functions is not covered in unit tests because we would effectively testing the mocks
logger.debug('Starting {} HTTP server..'.format(self.server), extra=vars(self))
if self.server == 'flask':
self.app.run('0.0.0.0', port=self.port, debug=self.debug)
Expand Down
46 changes: 37 additions & 9 deletions connexion/decorators/produces.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@
import functools
import json
import logging
import types


logger = logging.getLogger('connexion.decorators.produces')


class BaseSerializer:
def __init__(self, mimetype='text/plain'):
"""
:type mimetype: str
"""
self.mimetype = mimetype

@staticmethod
def get_data_status_code(data) -> ('Any', int):
def get_data_status_code(data):
"""
:type data: flask.Response | (object, int) | object
:rtype: (object, int)
"""
url = flask.request.url
logger.debug('Getting data and status code', extra={'data': data, 'data_type': type(data), 'url': url})
if isinstance(data, flask.Response):
Expand All @@ -42,15 +47,27 @@ def get_data_status_code(data) -> ('Any', int):
'url': url})
return data, status_code

def __call__(self, function: types.FunctionType) -> types.FunctionType:
def __call__(self, function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""
return function

def __repr__(self) -> str:
def __repr__(self):
"""
:rtype: str
"""
return '<BaseSerializer: {}>'.format(self.mimetype)


class Produces(BaseSerializer):
def __call__(self, function: types.FunctionType) -> types.FunctionType:
def __call__(self, function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""

@functools.wraps(function)
def wrapper(*args, **kwargs):
url = flask.request.url
Expand All @@ -65,12 +82,20 @@ def wrapper(*args, **kwargs):

return wrapper

def __repr__(self) -> str:
def __repr__(self):
"""
:rtype: str
"""
return '<Produces: {}>'.format(self.mimetype)


class Jsonifier(BaseSerializer):
def __call__(self, function: types.FunctionType) -> types.FunctionType:
def __call__(self, function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""

@functools.wraps(function)
def wrapper(*args, **kwargs):
url = flask.request.url
Expand All @@ -89,5 +114,8 @@ def wrapper(*args, **kwargs):

return wrapper

def __repr__(self) -> str:
def __repr__(self):
"""
:rtype: str
"""
return '<Jsonifier: {}>'.format(self.mimetype)
16 changes: 13 additions & 3 deletions connexion/decorators/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,31 @@
import functools
import logging
import requests
import types

from connexion.problem import problem


logger = logging.getLogger('connexion.api.security')


def security_passthrough(function: types.FunctionType) -> types.FunctionType:
def security_passthrough(function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""
return function


def verify_oauth(token_info_url: str, allowed_scopes: set, function: types.FunctionType) -> types.FunctionType:
def verify_oauth(token_info_url, allowed_scopes, function):
"""
Decorator to verify oauth
:param token_info_url: Url to get information about the token
:type token_info_url: str
:param allowed_scopes: Set with scopes that are allowed to access the endpoint
:type allowed_scopes: set
:type function: types.FunctionType
:rtype: types.FunctionType
"""

@functools.wraps(function)
Expand Down

0 comments on commit 1a18044

Please sign in to comment.