Skip to content

Commit

Permalink
Merge branch 'master' into return_tuple_for_aiohttp
Browse files Browse the repository at this point in the history
  • Loading branch information
cognifloyd committed May 3, 2019
2 parents da810b7 + 1e6aead commit b4b72e7
Show file tree
Hide file tree
Showing 36 changed files with 170 additions and 58 deletions.
13 changes: 7 additions & 6 deletions connexion/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import sys

import werkzeug.exceptions as exceptions # NOQA
from .apps import AbstractApp # NOQA

from .apis import AbstractAPI # NOQA
from .apps import AbstractApp # NOQA
from .decorators.produces import NoContent # NOQA
from .exceptions import ProblemException # NOQA
# add operation for backwards compatability
from .operations import compat
from .problem import problem # NOQA
from .decorators.produces import NoContent # NOQA
from .resolver import Resolution, Resolver, RestyResolver # NOQA

import sys

# add operation for backwards compatability
from .operations import compat
full_name = '{}.operation'.format(__package__)
sys.modules[full_name] = sys.modules[compat.__name__]

Expand Down
2 changes: 1 addition & 1 deletion connexion/apis/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def __init__(self, specification, base_path=None, arguments=None,
logger.debug('Validate Responses: %s', str(validate_responses))
self.validate_responses = validate_responses

logger.debug('Strict Request Validation: %s', str(validate_responses))
logger.debug('Strict Request Validation: %s', str(strict_validation))
self.strict_validation = strict_validation

logger.debug('Pythonic params: %s', str(pythonic_params))
Expand Down
41 changes: 34 additions & 7 deletions connexion/apis/aiohttp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import re
from urllib.parse import parse_qs

import jinja2

import aiohttp_jinja2
import jinja2
from aiohttp import web
from aiohttp.web_exceptions import HTTPNotFound
from aiohttp.web_exceptions import HTTPNotFound, HTTPPermanentRedirect
from aiohttp.web_middlewares import normalize_path_middleware
from connexion.apis.abstract import AbstractAPI
from connexion.exceptions import OAuthProblem, OAuthScopeProblem
from connexion.handlers import AuthErrorHandler
Expand Down Expand Up @@ -41,9 +41,19 @@ def oauth_problem_middleware(request, handler):

class AioHttpApi(AbstractAPI):
def __init__(self, *args, **kwargs):
# NOTE we use HTTPPermanentRedirect (308) because
# clients sometimes turn POST requests into GET requests
# on 301, 302, or 303
# see https://tools.ietf.org/html/rfc7538
trailing_slash_redirect = normalize_path_middleware(
append_slash=True,
redirect_class=HTTPPermanentRedirect
)
self.subapp = web.Application(
debug=kwargs.get('debug', False),
middlewares=[oauth_problem_middleware]
middlewares=[
oauth_problem_middleware,
trailing_slash_redirect
]
)
AbstractAPI.__init__(self, *args, **kwargs)

Expand Down Expand Up @@ -120,7 +130,6 @@ def add_swagger_ui(self):
console_ui_path)

for path in (
console_ui_path,
console_ui_path + '/',
console_ui_path + '/index.html',
):
Expand All @@ -130,8 +139,26 @@ def add_swagger_ui(self):
self._get_swagger_ui_home
)

# we have to add an explicit redirect instead of relying on the
# normalize_path_middleware because we also serve static files
# from this dir (below)

@asyncio.coroutine
def redirect(request):
raise web.HTTPMovedPermanently(
location=self.base_path + console_ui_path + '/'
)

self.subapp.router.add_route(
'GET',
console_ui_path,
redirect
)

# this route will match and get a permission error when trying to
# serve index.html, so we add the redirect above.
self.subapp.router.add_static(
console_ui_path + '/',
console_ui_path,
path=str(self.options.openapi_console_ui_from_dir),
name='swagger_ui_static'
)
Expand Down
3 changes: 1 addition & 2 deletions connexion/apis/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
import flask
import six
import werkzeug.exceptions
from werkzeug.local import LocalProxy

from connexion.apis import flask_utils
from connexion.apis.abstract import AbstractAPI
from connexion.handlers import AuthErrorHandler
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
from connexion.utils import Jsonifier, is_json_mimetype, yamldumper
from werkzeug.local import LocalProxy

logger = logging.getLogger('connexion.apis.flask_api')

Expand Down
5 changes: 1 addition & 4 deletions connexion/apps/aiohttp_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, import_name, only_one_api=False, **kwargs):
self._api_added = False

def create_app(self):
return web.Application(debug=self.debug)
return web.Application()

def get_root_path(self):
mod = sys.modules.get(self.import_name)
Expand Down Expand Up @@ -79,9 +79,6 @@ def run(self, port=None, server=None, debug=None, host=None, **options):

if debug is not None:
self.debug = debug
self.app._debug = debug
for subapp in self.app._subapps:
subapp._debug = debug

logger.debug('Starting %s HTTP server..', self.server, extra=vars(self))

Expand Down
3 changes: 1 addition & 2 deletions connexion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from os import path

import click
from clickclick import AliasedGroup, fatal_error

import connexion
from clickclick import AliasedGroup, fatal_error
from connexion.mock import MockResolver

logger = logging.getLogger('connexion.cli')
Expand Down
3 changes: 1 addition & 2 deletions connexion/decorators/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import textwrap

import requests
from six.moves import http_cookies

from connexion.utils import get_function_from_name
from six.moves import http_cookies

from ..exceptions import (ConnexionException, OAuthProblem,
OAuthResponseProblem, OAuthScopeProblem)
Expand Down
2 changes: 1 addition & 1 deletion connexion/operations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .abstract import AbstractOperation # noqa
from .openapi import OpenAPIOperation # noqa
from .swagger2 import Swagger2Operation # noqa
from .secure import SecureOperation # noqa
from .swagger2 import Swagger2Operation # noqa


def make_operation(spec, *args, **kwargs):
Expand Down
1 change: 0 additions & 1 deletion connexion/operations/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import logging

import six

from connexion.operations.secure import SecureOperation

from ..decorators.metrics import UWSGIMetricsCollector
Expand Down
2 changes: 1 addition & 1 deletion connexion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,4 @@ def ignore_aliases(self, *args):
# Dump long lines as "|".
yaml.representer.SafeRepresenter.represent_scalar = my_represent_scalar

return yaml.dump(openapi, default_flow_style=False, allow_unicode=True, Dumper=NoAnchorDumper)
return yaml.dump(openapi, allow_unicode=True, Dumper=NoAnchorDumper)
5 changes: 4 additions & 1 deletion docs/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ taken from there, otherwise it's None. This allows authorizing individual
operations with oauth scope while using basic authentication for
authentication.

You can find a `minimal Basic Auth example application`_ in Connexion's "examples" folder.

ApiKey Authentication
---------------------

Expand All @@ -66,7 +68,7 @@ With Connexion, the API security definition **must** include a
semantics as for ``x-basicInfoFunc``, but the function accepts two
parameters: apikey and required_scopes.

You can find a `minimal Basic Auth example application`_ in Connexion's "examples" folder.
You can find a `minimal API Key example application`_ in Connexion's "examples" folder.

Bearer Authentication (JWT)
---------------------------
Expand All @@ -91,4 +93,5 @@ way to start a HTTPS server when using Connexion?
.. _rfc7662: https://tools.ietf.org/html/rfc7662
.. _minimal OAuth example application: https://github.com/zalando/connexion/tree/master/examples/swagger2/oauth2
.. _minimal Basic Auth example application: https://github.com/zalando/connexion/tree/master/examples/swagger2/basicauth
.. _minimal API Key example application: https://github.com/zalando/connexion/tree/master/examples/oauth2/apikey
.. _minimal JWT example application: https://github.com/zalando/connexion/tree/master/examples/openapi3/jwt
20 changes: 20 additions & 0 deletions examples/openapi3/apikey/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
=======================
API Key Example
=======================

Running:

.. code-block:: bash
$ sudo pip3 install --upgrade connexion[swagger-ui] # install Connexion from PyPI
$ ./app.py
Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI.

The hardcoded apikey is `asdf1234567890`.

Test it out (in another terminal):

.. code-block:: bash
$ curl -H 'X-Auth: asdf1234567890' http://localhost:8080/secret
32 changes: 32 additions & 0 deletions examples/openapi3/apikey/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
'''
Basic example of a resource server
'''

import connexion
from connexion.exceptions import OAuthProblem

TOKEN_DB = {
'asdf1234567890': {
'uid': 100
}
}


def apikey_auth(token, required_scopes):
info = TOKEN_DB.get(token, None)

if not info:
raise OAuthProblem('Invalid token')

return info


def get_secret(user) -> str:
return "You are {user} and the secret is 'wbevuec'".format(user=user)


if __name__ == '__main__':
app = connexion.FlaskApp(__name__)
app.add_api('openapi.yaml')
app.run(port=8080)
25 changes: 25 additions & 0 deletions examples/openapi3/apikey/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.0.0
info:
title: API Key Example
version: '1.0'
paths:
/secret:
get:
summary: Return secret string
operationId: app.get_secret
responses:
'200':
description: secret response
content:
'*/*':
schema:
type: string
security:
- api_key: []
components:
securitySchemes:
api_key:
type: apiKey
name: X-Auth
in: header
x-apikeyInfoFunc: app.apikey_auth
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def read_version(package):
install_requires = [
'clickclick>=1.2',
'jsonschema>=2.5.1,<3.0.0',
'PyYAML>=3.13',
'PyYAML>=5.1',
'requests>=2.9.1',
'six>=1.9',
'inflection>=0.3.1',
Expand Down
1 change: 1 addition & 0 deletions tests/aiohttp/test_aiohttp_api_secure.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import base64


from connexion import AioHttpApp


Expand Down
1 change: 1 addition & 0 deletions tests/aiohttp/test_aiohttp_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unittest import mock

import pytest

from conftest import TEST_FOLDER
from connexion import AioHttpApp
from connexion.exceptions import ConnexionException
Expand Down
10 changes: 6 additions & 4 deletions tests/aiohttp/test_aiohttp_simple_api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import asyncio
import sys

import pytest
import yaml

import pytest
from conftest import TEST_FOLDER
from connexion import AioHttpApp

Expand Down Expand Up @@ -86,7 +86,7 @@ def test_no_swagger_json(aiohttp_api_spec_dir, aiohttp_client):
specification_dir=aiohttp_api_spec_dir,
options=options,
debug=True)
api = app.add_api('swagger_simple.yaml')
app.add_api('swagger_simple.yaml')

app_client = yield from aiohttp_client(app.app)
swagger_json = yield from app_client.get('/v1.0/swagger.json') # type: flask.Response
Expand All @@ -101,7 +101,7 @@ def test_no_swagger_yaml(aiohttp_api_spec_dir, aiohttp_client):
specification_dir=aiohttp_api_spec_dir,
options=options,
debug=True)
api = app.add_api('swagger_simple.yaml')
app.add_api('swagger_simple.yaml')

app_client = yield from aiohttp_client(app.app)
spec_response = yield from app_client.get('/v1.0/swagger.yaml') # type: flask.Response
Expand All @@ -118,6 +118,7 @@ def test_swagger_ui(aiohttp_api_spec_dir, aiohttp_client):
app_client = yield from aiohttp_client(app.app)
swagger_ui = yield from app_client.get('/v1.0/ui')
assert swagger_ui.status == 200
assert swagger_ui.url.path == '/v1.0/ui/'
assert b'url = "/v1.0/swagger.json"' in (yield from swagger_ui.read())

swagger_ui = yield from app_client.get('/v1.0/ui/')
Expand Down Expand Up @@ -239,6 +240,7 @@ def test_validate_responses(aiohttp_app, aiohttp_client):
def test_get_users(aiohttp_client, aiohttp_app):
app_client = yield from aiohttp_client(aiohttp_app.app)
resp = yield from app_client.get('/v1.0/users')
assert resp.url.path == '/v1.0/users/' # followed redirect
assert resp.status == 200

json_data = yield from resp.json()
Expand All @@ -257,7 +259,7 @@ def test_create_user(aiohttp_client, aiohttp_app):
@asyncio.coroutine
def test_access_request_context(aiohttp_client, aiohttp_app):
app_client = yield from aiohttp_client(aiohttp_app.app)
resp = yield from app_client.post('/v1.0/aiohttp_access_request_context')
resp = yield from app_client.post('/v1.0/aiohttp_access_request_context/')
assert resp.status == 204


Expand Down
4 changes: 2 additions & 2 deletions tests/api/test_bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import json

import jinja2
import mock
import pytest
import yaml
from openapi_spec_validator.loaders import ExtendedSafeLoader

import mock
import pytest
from conftest import TEST_FOLDER, build_app_from_fixture
from connexion import App
from connexion.exceptions import InvalidSpecification
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys

import pytest

from connexion import App

logging.basicConfig(level=logging.DEBUG)
Expand Down
3 changes: 2 additions & 1 deletion tests/decorators/test_parameter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from connexion.decorators.parameter import parameter_to_arg
# we are using "mock" module here for Py 2.7 support
from mock import MagicMock

from connexion.decorators.parameter import parameter_to_arg


def test_injection():
request = MagicMock(name='request', path_params={'p1': '123'})
Expand Down
Loading

0 comments on commit b4b72e7

Please sign in to comment.