Update examples for Connexion 3.0 (#1615)

This PR updates the examples for Connexion 3.0 and merges them for
OpenAPI and Swagger.

2 examples required some changes to make them work:
- The reverse proxy example required some fixes to the
SwaggerUIMiddleware to leverage the `root_path` correctly. This is
included in the PR.
- The enforced defaults example requires the json validator to adapt the
body and pass it on. We currently pass on the original body after
validation, and I'm not sure if we should change this. I'll submit a
separate PR to discuss this.
This commit is contained in:
Robbe Sneyders
2022-12-30 20:34:19 +01:00
committed by GitHub
parent e5784c5741
commit 073f0d446e
99 changed files with 679 additions and 998 deletions

View File

@@ -105,19 +105,16 @@ class AbstractRoutingAPI(AbstractSpecAPI):
*args,
resolver_error_handler: t.Optional[t.Callable] = None,
pythonic_params=False,
debug: bool = False,
**kwargs,
) -> None:
"""Minimal interface of an API, with only functionality related to routing.
:param pythonic_params: When True CamelCase parameters are converted to snake_case and an underscore is appended
to any shadowed built-ins
:param debug: Flag to run in debug mode
"""
super().__init__(*args, **kwargs)
logger.debug("Pythonic params: %s", str(pythonic_params))
self.pythonic_params = pythonic_params
self.debug = debug
self.resolver_error_handler = resolver_error_handler
self.add_paths()
@@ -171,9 +168,6 @@ class AbstractRoutingAPI(AbstractSpecAPI):
error_msg = "Failed to add operation for {method} {url}".format(
method=method.upper(), url=url
)
if self.debug:
logger.exception(error_msg)
else:
logger.error(error_msg)
_type, value, traceback = exc_info
raise value.with_traceback(traceback)
@@ -190,7 +184,6 @@ class AbstractAPI(AbstractRoutingAPI, metaclass=AbstractAPIMeta):
base_path=None,
arguments=None,
resolver=None,
debug=False,
resolver_error_handler=None,
options=None,
**kwargs,
@@ -206,7 +199,6 @@ class AbstractAPI(AbstractRoutingAPI, metaclass=AbstractAPIMeta):
arguments=arguments,
resolver=resolver,
resolver_error_handler=resolver_error_handler,
debug=debug,
options=options,
**kwargs,
)

View File

@@ -20,12 +20,9 @@ class AbstractApp(metaclass=abc.ABCMeta):
self,
import_name,
api_cls,
port=None,
specification_dir="",
host=None,
arguments=None,
auth_all_paths=False,
debug=None,
resolver=None,
options=None,
skip_error_handlers=False,
@@ -34,30 +31,22 @@ class AbstractApp(metaclass=abc.ABCMeta):
"""
:param import_name: the name of the application package
:type import_name: str
:param host: the host interface to bind on.
:type host: 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 arguments: arguments to replace on the specification
:type arguments: dict | None
:param auth_all_paths: whether to authenticate not defined paths
:type auth_all_paths: bool
:param debug: include debugging information
:type debug: bool
:param resolver: Callable that maps operationID to a function
:param middlewares: Callable that maps operationID to a function
:type middlewares: list | None
"""
self.port = port
self.host = host
self.debug = debug
self.resolver = resolver
self.import_name = import_name
self.arguments = arguments or {}
self.api_cls = api_cls
self.resolver_error = None
self.extra_files = []
# Options
self.auth_all_paths = auth_all_paths
@@ -169,7 +158,9 @@ class AbstractApp(metaclass=abc.ABCMeta):
if isinstance(specification, dict):
specification = specification
else:
specification = self.specification_dir / specification
specification = t.cast(pathlib.Path, self.specification_dir / specification)
# Add specification as file to watch for reloading
self.extra_files.append(str(specification.relative_to(pathlib.Path.cwd())))
api_options = self.options.extend(options)
@@ -182,7 +173,6 @@ class AbstractApp(metaclass=abc.ABCMeta):
validate_responses=validate_responses,
strict_validation=strict_validation,
auth_all_paths=auth_all_paths,
debug=self.debug,
validator_map=validator_map,
pythonic_params=pythonic_params,
options=api_options.as_dict(),
@@ -197,7 +187,6 @@ class AbstractApp(metaclass=abc.ABCMeta):
validate_responses=validate_responses,
strict_validation=strict_validation,
auth_all_paths=auth_all_paths,
debug=self.debug,
pythonic_params=pythonic_params,
options=api_options.as_dict(),
)
@@ -267,6 +256,38 @@ class AbstractApp(metaclass=abc.ABCMeta):
`HEAD`).
"""
def run(self, import_string: str = None, **kwargs):
"""Run the application using uvicorn.
:param import_string: application as import string (eg. "main:app"). This is needed to run
using reload.
:param kwargs: kwargs to pass to `uvicorn.run`.
"""
try:
import uvicorn
except ImportError:
raise RuntimeError(
"uvicorn is not installed. Please install connexion using the uvicorn extra "
"(connexion[uvicorn])"
)
logger.warning(
f"`{self.__class__.__name__}.run` is optimized for development. "
"For production, run using a dedicated ASGI server."
)
app: t.Union[str, AbstractApp]
if import_string is not None:
app = import_string
kwargs.setdefault("reload", True)
kwargs["reload_includes"] = self.extra_files + kwargs.get(
"reload_includes", []
)
else:
app = self
uvicorn.run(app, **kwargs)
@abc.abstractmethod
def __call__(self, scope, receive, send):
"""

View File

@@ -83,7 +83,11 @@ class AsyncAsgiApp:
)
api_base_path = connexion_context.get("api_base_path")
if api_base_path and not api_base_path == self.base_path:
if (
api_base_path is not None
and api_base_path in self.apis
and not api_base_path == self.base_path
):
api = self.apis[api_base_path]
return await api(scope, receive, send)

View File

@@ -23,9 +23,7 @@ logger = logging.getLogger("connexion.app")
class FlaskApp(AbstractApp):
def __init__(
self, import_name, server="flask", server_args=None, extra_files=None, **kwargs
):
def __init__(self, import_name, server_args=None, **kwargs):
"""
:param extra_files: additional files to be watched by the reloader, defaults to the swagger specs of added apis
:type extra_files: list[str | pathlib.Path], optional
@@ -34,9 +32,7 @@ class FlaskApp(AbstractApp):
"""
self.import_name = import_name
self.server = server
self.server_args = dict() if server_args is None else server_args
self.extra_files = extra_files or []
self.app = self.create_app()
@@ -100,8 +96,6 @@ class FlaskApp(AbstractApp):
def add_api(self, specification, **kwargs):
api = super().add_api(specification, **kwargs)
self.app.register_blueprint(api.blueprint)
if isinstance(specification, (str, pathlib.Path)):
self.extra_files.append(self.specification_dir / specification)
return api
def add_error_handler(self, error_code, function):
@@ -124,89 +118,11 @@ class FlaskApp(AbstractApp):
logger.debug("Adding %s with decorator", rule, extra=kwargs)
return self.app.route(rule, **kwargs)
def run(
self, port=None, server=None, debug=None, host=None, extra_files=None, **options
): # pragma: no cover
"""
Runs the application on a local development server.
:param host: the host interface to bind on.
:type host: str
:param port: port to listen to
:type port: int
:param server: which wsgi server to use
:type server: str | None
:param debug: include debugging information
:type debug: bool
:param extra_files: additional files to be watched by the reloader.
:type extra_files: Iterable[str | pathlib.Path]
:param options: options to be forwarded to the underlying server
"""
# this functions is not covered in unit tests because we would effectively testing the mocks
# overwrite constructor parameter
if port is not None:
self.port = port
elif self.port is None:
self.port = 5000
self.host = host or self.host or "0.0.0.0"
if server is not None:
self.server = server
if debug is not None:
self.debug = debug
if extra_files is not None:
self.extra_files.extend(extra_files)
logger.debug("Starting %s HTTP server..", self.server, extra=vars(self))
if self.server == "flask":
self.app.run(
self.host,
port=self.port,
debug=self.debug,
extra_files=self.extra_files,
**options,
)
elif self.server == "tornado":
try:
import tornado.autoreload
import tornado.httpserver
import tornado.ioloop
import tornado.wsgi
except ImportError:
raise Exception("tornado library not installed")
wsgi_container = tornado.wsgi.WSGIContainer(self.app)
http_server = tornado.httpserver.HTTPServer(wsgi_container, **options)
http_server.listen(self.port, address=self.host)
if self.debug:
tornado.autoreload.start()
logger.info("Listening on %s:%s..", self.host, self.port)
tornado.ioloop.IOLoop.instance().start()
elif self.server == "gevent":
try:
import gevent.pywsgi
except ImportError:
raise Exception("gevent library not installed")
if self.debug:
logger.warning(
"gevent server doesn't support debug mode. Please switch to flask/tornado server."
)
http_server = gevent.pywsgi.WSGIServer(
(self.host, self.port), self.app, **options
)
logger.info("Listening on %s:%s..", self.host, self.port)
http_server.serve_forever()
else:
raise Exception(f"Server {self.server} not recognized")
def __call__(self, scope, receive, send):
async def __call__(self, scope, receive, send):
"""
ASGI interface. Calls the middleware wrapped around the wsgi app.
"""
return self.middleware(scope, receive, send)
return await self.middleware(scope, receive, send)
class FlaskJSONProvider(flask.json.provider.DefaultJSONProvider):

View File

@@ -230,7 +230,7 @@ def run(
"swagger_url": console_ui_url or None,
}
app = app_cls(__name__, debug=debug, auth_all_paths=auth_all_paths, options=options)
app = app_cls(__name__, auth_all_paths=auth_all_paths, options=options)
app.add_api(
spec_file_full_path,

View File

@@ -53,7 +53,7 @@ class RequestResponseDecorator:
@functools.wraps(function)
def wrapper(*args, **kwargs):
request = self.api.get_request()
request = self.api.get_request(*args, uri_parser=uri_parser, **kwargs)
response = function(request)
return self.api.get_response(response, self.mimetype)

View File

@@ -38,6 +38,7 @@ def parameter_to_arg(
sanitize = pythonic if pythonic_params else sanitized
arguments, has_kwargs = inspect_function_arguments(function)
# TODO: should always be used for AsyncApp
if asyncio.iscoroutinefunction(function):
@functools.wraps(function)
@@ -72,7 +73,7 @@ def parameter_to_arg(
else:
@functools.wraps(function)
async def wrapper(request: ConnexionRequest) -> t.Any:
def wrapper(request: ConnexionRequest) -> t.Any:
body_name = sanitize(operation.body_name(request.content_type))
# Pass form contents separately for Swagger2 for backward compatibility with
# Connexion 2 Checking for body_name is not enough

View File

@@ -120,7 +120,7 @@ class RoutedMiddleware(AppMiddleware, t.Generic[API]):
"you have a routing middleware registered upstream. "
)
api_base_path = connexion_context.get("api_base_path")
if api_base_path:
if api_base_path is not None and api_base_path in self.apis:
api = self.apis[api_base_path]
operation_id = connexion_context.get("operation_id")
try:

View File

@@ -55,7 +55,7 @@ class ConnexionMiddleware:
for middleware in reversed(middlewares):
app = middleware(app) # type: ignore
apps.append(app)
return app, reversed(apps)
return app, list(reversed(apps))
def add_api(
self,

View File

@@ -4,6 +4,7 @@ import re
import typing as t
from contextvars import ContextVar
from starlette.requests import Request as StarletteRequest
from starlette.responses import RedirectResponse
from starlette.responses import Response as StarletteResponse
from starlette.routing import Router
@@ -42,16 +43,11 @@ class SwaggerUIAPI(AbstractSpecAPI):
def normalize_string(string):
return re.sub(r"[^a-zA-Z0-9]", "_", string.strip("/"))
def _base_path_for_prefix(self, request):
def _base_path_for_prefix(self, request: StarletteRequest) -> str:
"""
returns a modified basePath which includes the incoming request's
path prefix.
returns a modified basePath which includes the incoming root_path.
"""
base_path = self.base_path
if not request.url.path.startswith(self.base_path):
prefix = request.url.path.split(self.base_path)[0]
base_path = prefix + base_path
return base_path
return request.scope.get("root_path", "").rstrip("/")
def _spec_for_prefix(self, request):
"""
@@ -68,7 +64,7 @@ class SwaggerUIAPI(AbstractSpecAPI):
(or {base_path}/swagger.json for swagger2)
"""
logger.info(
"Adding spec json: %s/%s", self.base_path, self.options.openapi_spec_path
"Adding spec json: %s%s", self.base_path, self.options.openapi_spec_path
)
self.router.add_route(
methods=["GET"],
@@ -132,8 +128,11 @@ class SwaggerUIAPI(AbstractSpecAPI):
# normalize_path_middleware because we also serve static files
# from this dir (below)
async def redirect(_request):
return RedirectResponse(url=self.base_path + console_ui_path + "/")
async def redirect(request):
url = request.scope.get("root_path", "").rstrip("/")
url += console_ui_path
url += "/"
return RedirectResponse(url=url)
self.router.add_route(methods=["GET"], path=console_ui_path, endpoint=redirect)

View File

@@ -0,0 +1,22 @@
=======================
API Key Example
=======================
Running:
.. code-block:: bash
$ pip install --upgrade connexion[swagger-ui] # install Connexion from PyPI
$ python app.py
Now open your browser and go to http://localhost:8080/openapi/ui/ or
http://localhost:8080/swagger/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/openapi/secret
$ curl -H 'X-Auth: asdf1234567890' http://localhost:8080/swagger/secret

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Basic example of a resource server
"""
from pathlib import Path
import connexion
from connexion.exceptions import OAuthProblem
@@ -22,7 +22,10 @@ def get_secret(user) -> str:
return f"You are {user} and the secret is 'wbevuec'"
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml")
app.add_api("swagger.yaml")
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api("openapi.yaml")
app.run(port=8080)
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -2,6 +2,8 @@ openapi: 3.0.0
info:
title: API Key Example
version: '1.0'
servers:
- url: /openapi
paths:
/secret:
get:

View File

@@ -0,0 +1,23 @@
swagger: "2.0"
info:
title: API Key Example
version: '1.0'
basePath: /swagger
paths:
/secret:
get:
summary: Return secret string
operationId: app.get_secret
responses:
'200':
description: secret response
schema:
type: string
security:
- api_key: []
securityDefinitions:
api_key:
type: apiKey
name: X-Auth
in: header
x-apikeyInfoFunc: app.apikey_auth

View File

@@ -0,0 +1,15 @@
=======================
HTTP Basic Auth Example
=======================
Running:
.. code-block:: bash
$ pip install --upgrade connexion[swagger-ui] # install Connexion from PyPI
$ python app.py
Now open your browser and go to http://localhost:8080/openapi/ui/ or
http://localhost:8080/swagger/ui/ to see the Swagger UI.
The hardcoded credentials are ``admin:secret`` and ``foo:bar``.

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Basic example of a resource server
"""
from pathlib import Path
import connexion
@@ -19,7 +19,10 @@ def get_secret(user) -> str:
return f"You are {user} and the secret is 'wbevuec'"
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml")
app.add_api("swagger.yaml")
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api("openapi.yaml")
app.run(port=8080)
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -2,6 +2,8 @@ openapi: 3.0.0
info:
title: Basic Auth Example
version: '1.0'
servers:
- url: /openapi
paths:
/secret:
get:

View File

@@ -1,9 +1,8 @@
swagger: "2.0"
info:
title: Basic Auth Example
version: "1.0"
basePath: /swagger
paths:
/secret:
get:

View File

@@ -2,6 +2,11 @@
Custom Validator Example
========================
.. warning::
This example is outdated. Currently validation no longer adapts the body.
TODO: decide if validation should adapt body or how we want to enable defaults otherwise.
In this example we fill-in non-provided properties with their defaults.
Validator code is based on example from `python-jsonschema docs`_.
@@ -9,7 +14,7 @@ Running:
.. code-block:: bash
$ ./enforcedefaults.py
$ python app.py
Now open your browser and go to http://localhost:8080/v1/ui/ to see the Swagger
UI. If you send a ``POST`` request with empty body ``{}``, you should receive

View File

@@ -1,12 +1,13 @@
#!/usr/bin/env python3
from pathlib import Path
import connexion
import jsonschema
from connexion.decorators.validation import RequestBodyValidator
from connexion.json_schema import Draft4RequestValidator
from connexion.validators import JSONRequestBodyValidator
def echo(data):
# TODO: should work as sync endpoint when parameter decorator is fixed
async def echo(data):
return data
@@ -27,15 +28,17 @@ def extend_with_set_default(validator_class):
DefaultsEnforcingDraft4Validator = extend_with_set_default(Draft4RequestValidator)
class DefaultsEnforcingRequestBodyValidator(RequestBodyValidator):
class DefaultsEnforcingRequestBodyValidator(JSONRequestBodyValidator):
def __init__(self, *args, **kwargs):
super().__init__(*args, validator=DefaultsEnforcingDraft4Validator, **kwargs)
validator_map = {"body": DefaultsEnforcingRequestBodyValidator}
validator_map = {"body": {"application/json": DefaultsEnforcingRequestBodyValidator}}
app = connexion.AsyncApp(__name__, specification_dir="spec")
app.add_api("swagger.yaml", validator_map=validator_map)
if __name__ == "__main__":
app = connexion.FlaskApp(__name__, port=8080, specification_dir=".")
app.add_api("enforcedefaults-api.yaml", validator_map=validator_map)
app.run()
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -11,7 +11,7 @@ paths:
/echo:
post:
description: Echo passed data
operationId: enforcedefaults.echo
operationId: app.echo
parameters:
- name: data
in: body

View File

@@ -0,0 +1,12 @@
===================
Hello World Example
===================
Running:
.. code-block:: bash
$ python hello.py
Now open your browser and go to http://localhost:8080/openapi/ui/ or
http://localhost:8080/swagger/ui/ to see the Swagger UI.

16
examples/helloworld/hello.py Executable file
View File

@@ -0,0 +1,16 @@
from pathlib import Path
import connexion
def post_greeting(name: str) -> str:
return f"Hello {name}"
app = connexion.FlaskApp(__name__, specification_dir="spec/")
app.add_api("openapi.yaml", arguments={"title": "Hello World Example"})
app.add_api("swagger.yaml", arguments={"title": "Hello World Example"})
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -4,7 +4,7 @@ info:
title: Hello World
version: "1.0"
servers:
- url: http://localhost:9090/v1.0
- url: /openapi
paths:
/greeting/{name}:

View File

@@ -4,7 +4,7 @@ info:
title: "{{title}}"
version: "1.0"
basePath: /v1.0
basePath: /swagger
paths:
/greeting/{name}:

View File

@@ -0,0 +1,12 @@
===================================
Hello World Example using async App
===================================
Running:
.. code-block:: bash
$ python hello.py
Now open your browser and go to http://localhost:8000/openapi/ui/ or
http://localhost:8000/swagger/ui/ to see the Swagger UI.

View File

@@ -0,0 +1,21 @@
from pathlib import Path
import connexion
async def test():
pass
async def post_greeting(name: str):
await test()
return f"Hello {name}", 201
app = connexion.AsyncApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml", arguments={"title": "Hello World Example"})
app.add_api("swagger.yaml", arguments={"title": "Hello World Example"})
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -4,7 +4,7 @@ info:
title: Hello World
version: "1.0"
servers:
- url: http://localhost:9090/v1.0
- url: /openapi
paths:
/greeting/{name}:

View File

@@ -0,0 +1,25 @@
swagger: "2.0"
info:
title: Hello World
version: "1.0"
basePath: /swagger
paths:
/greeting/{name}:
post:
summary: Generate greeting
description: Generates a greeting message.
operationId: hello.post_greeting
responses:
200:
description: greeting response
schema:
type: string
example: "hello dave!"
parameters:
- name: name
in: path
description: Name of the person to greet.
required: true
type: string

View File

@@ -2,12 +2,16 @@
JWT Auth Example
=======================
.. note::
jwt is not supported by swagger 2.0: https://swagger.io/docs/specification/2-0/authentication/
Running:
.. code-block:: bash
$ sudo pip3 install -r requirements.txt
$ ./app.py
$ pip install -r requirements.txt
$ python app.py
Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI.
Use endpoint **/auth** to generate JWT token, copy it, then click **Authorize** button and paste the token.

View File

@@ -1,9 +1,8 @@
#!/usr/bin/env python3
"""
Basic example of a resource server
"""
import time
from pathlib import Path
import connexion
from jose import JWTError, jwt
@@ -47,7 +46,9 @@ def _current_timestamp() -> int:
return int(time.time())
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml")
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api("openapi.yaml")
app.run(port=8080)
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -0,0 +1,3 @@
..[swagger-ui]
python-jose[cryptography]
Flask>=0.10.1

View File

@@ -6,6 +6,6 @@ Running:
.. code-block:: bash
$ ./app.py
$ python app.py
Now open your browser and go to http://localhost:9090/v1.0/ui/ to see the Swagger UI.

43
examples/methodresolver/app.py Executable file
View File

@@ -0,0 +1,43 @@
import logging
from pathlib import Path
import connexion
from connexion.resolver import MethodViewResolver
logging.basicConfig(level=logging.INFO)
zoo = {
1: {
"id": 1,
"name": "giraffe",
"tags": ["africa", "yellow", "hoofs", "herbivore", "long neck"],
},
2: {
"id": 2,
"name": "lion",
"tags": ["africa", "yellow", "paws", "carnivore", "mane"],
},
}
app = connexion.FlaskApp(__name__, specification_dir="spec/", debug=True)
options = {"swagger_ui": True}
app.add_api(
"openapi.yaml",
options=options,
arguments={"title": "MethodViewResolver Example"},
resolver=MethodViewResolver(
"api",
# class params are entirely optional
# they allow to inject dependencies top down
# so that the app can be wired, in the entrypoint
class_arguments={"PetsView": {"kwargs": {"pets": zoo}}},
),
strict_validation=True,
validate_responses=True,
)
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -5,7 +5,7 @@ info:
license:
name: MIT
servers:
- url: http://localhost:9090/v1.0
- url: /openapi
paths:
/pets:
get:

View File

@@ -9,17 +9,17 @@ Running:
.. code-block:: bash
$ sudo pip3 install --upgrade connexion # install Connexion from PyPI
$ ./mock_tokeninfo.py & # start mock in background
$ ./app.py
$ pip install --upgrade connexion # install Connexion from PyPI
$ python mock_tokeninfo.py & # start mock in background
$ python app.py
Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI.
Now open your browser and go to http://localhost:8080/openapi/ui/ to see the Swagger UI.
You can use the hardcoded tokens to request the endpoint:
.. code-block:: bash
$ curl http://localhost:8080/secret # missing authentication
$ curl -H 'Authorization: Bearer 123' http://localhost:8080/secret
$ curl -H 'Authorization: Bearer 456' http://localhost:8080/secret
$ curl http://localhost:8080/openapi/secret # missing authentication
$ curl -H 'Authorization: Bearer 123' http://localhost:8080/openapi/secret
$ curl -H 'Authorization: Bearer 456' http://localhost:8080/swagger/secret

19
examples/oauth2/app.py Executable file
View File

@@ -0,0 +1,19 @@
"""
Basic example of a resource server
"""
from pathlib import Path
import connexion
def get_secret(user) -> str:
return f"You are: {user}"
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml")
app.add_api("swagger.yaml")
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""
Mock OAuth2 token info
"""
import connexion
import uvicorn
from connexion import request
# our hardcoded mock "Bearer" access tokens
@@ -25,6 +25,6 @@ def get_tokeninfo() -> dict:
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("mock_tokeninfo.yaml")
app.run(port=7979)
uvicorn.run(app, port=7979)

View File

@@ -0,0 +1,37 @@
openapi: 3.0.0
info:
title: OAuth Example
version: "1.0"
servers:
- url: /openapi
paths:
/secret:
get:
summary: Return secret string
operationId: app.get_secret
responses:
200:
description: secret response
content:
'text/plain':
schema:
type: string
security:
# enable authentication and require the "uid" scope for this endpoint
- oauth2: ['uid']
components:
securitySchemes:
oauth2:
type: oauth2
x-tokenInfoUrl: http://localhost:7979/tokeninfo
flows:
implicit:
authorizationUrl: https://example.com/oauth2/dialog
# the token info URL is hardcoded for our mock_tokeninfo.py script
# you can also pass it as an environment variable TOKENINFO_URL
scopes:
uid: Unique identifier of the user accessing the service.

View File

@@ -4,6 +4,8 @@ info:
title: OAuth Example
version: "1.0"
basePath: /swagger
paths:
/secret:
get:

View File

@@ -9,16 +9,16 @@ Running:
.. code-block:: bash
$ sudo pip3 install --upgrade connexion # install Connexion from PyPI
$ ./app.py
$ pip install --upgrade connexion # install Connexion from PyPI
$ python app.py
Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI.
Now open your browser and go to http://localhost:8080/openapi/ui/ to see the Swagger UI.
You can use the hardcoded tokens to request the endpoint:
.. code-block:: bash
$ curl http://localhost:8080/secret # missing authentication
$ curl -H 'Authorization: Bearer 123' http://localhost:8080/secret
$ curl -H 'Authorization: Bearer 456' http://localhost:8080/secret
$ curl http://localhost:8080/openapi/secret # missing authentication
$ curl -H 'Authorization: Bearer 123' http://localhost:8080/openapi/secret
$ curl -H 'Authorization: Bearer 456' http://localhost:8080/swagger/secret

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Basic example of a resource server
"""
from pathlib import Path
import connexion
@@ -20,7 +20,10 @@ def token_info(access_token) -> dict:
return {"uid": uid, "scope": ["uid"]}
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml")
app.add_api("swagger.yaml")
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api("app.yaml")
app.run(port=8080)
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -0,0 +1,35 @@
openapi: 3.0.0
info:
title: OAuth Example
version: "1.0"
servers:
- url: /openapi
paths:
/secret:
get:
summary: Return secret string
operationId: app.get_secret
responses:
200:
description: secret response
content:
text/plain:
schema:
type: string
security:
# enable authentication and require the "uid" scope for this endpoint
- oauth2: ['uid']
components:
securitySchemes:
oauth2:
type: oauth2
x-tokenInfoFunc: app.token_info
flows:
implicit:
authorizationUrl: https://example.com/oauth2/dialog
scopes:
uid: Unique identifier of the user accessing the service.

View File

@@ -4,6 +4,8 @@ info:
title: OAuth Example
version: "1.0"
basePath: /swagger
paths:
/secret:
get:

View File

@@ -1,20 +0,0 @@
=======================
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

View File

@@ -1,14 +0,0 @@
=======================
HTTP Basic Auth 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 credentials are ``admin:secret`` and ``foo:bar``.

View File

@@ -1,11 +0,0 @@
===================
Hello World Example
===================
Running:
.. code-block:: bash
$ ./hello.py
Now open your browser and go to http://localhost:9090/v1.0/ui/ to see the Swagger UI.

View File

@@ -1,11 +0,0 @@
import connexion
def post_greeting(name: str) -> str:
return f"Hello {name}"
if __name__ == "__main__":
app = connexion.FlaskApp(__name__, port=9090, specification_dir="openapi/")
app.add_api("helloworld-api.yaml", arguments={"title": "Hello World Example"})
app.run()

View File

@@ -1,11 +0,0 @@
===================
Hello World Example
===================
Running:
.. code-block:: bash
$ uvicorn hello:app
Now open your browser and go to http://localhost:9090/v1.0/ui/ to see the Swagger UI.

View File

@@ -1,15 +0,0 @@
import connexion
from starlette.responses import PlainTextResponse
async def test():
pass
async def post_greeting(name: str) -> PlainTextResponse:
await test()
return f"Hello {name}", 201
app = connexion.AsyncApp(__name__, port=9090, specification_dir="openapi/")
app.add_api("helloworld-api.yaml", arguments={"title": "Hello World Example"})

View File

@@ -1,6 +0,0 @@
# Install swagger-ui before connexion.
connexion[swagger-ui]
connexion>=2.2.0
python-jose[cryptography]
Flask>=0.10.1

View File

@@ -1,14 +0,0 @@
#
# Run all project with:
#
# tox
#
[tox]
envlist = py3
skipsdist = true
[testenv]
deps = -rrequirements.txt
commands =
python app.py

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env python
import logging
import connexion
from connexion.resolver import MethodViewResolver
logging.basicConfig(level=logging.INFO)
zoo = {
1: {
"id": 1,
"name": "giraffe",
"tags": ["africa", "yellow", "hoofs", "herbivore", "long neck"],
},
2: {
"id": 2,
"name": "lion",
"tags": ["africa", "yellow", "paws", "carnivore", "mane"],
},
}
if __name__ == "__main__":
app = connexion.FlaskApp(__name__, specification_dir="openapi/", debug=True)
options = {"swagger_ui": True}
app.add_api(
"pets-api.yaml",
options=options,
arguments={"title": "MethodViewResolver Example"},
resolver=MethodViewResolver(
"api",
# class params are entirely optional
# they allow to inject dependencies top down
# so that the app can be wired, in the entrypoint
class_arguments={"PetsView": {"kwargs": {"pets": zoo}}},
),
strict_validation=True,
validate_responses=True,
)
app.run(port=9090, debug=True)

View File

@@ -1,11 +0,0 @@
=====================
RestyResolver Example
=====================
Running:
.. code-block:: bash
$ ./resty.py
Now open your browser and go to http://localhost:9090/v1.0/ui/ to see the Swagger UI.

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env python
import logging
import connexion
from connexion.resolver import RestyResolver
logging.basicConfig(level=logging.INFO)
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api(
"resty-api.yaml",
arguments={"title": "RestyResolver Example"},
resolver=RestyResolver("api"),
)
app.run(port=9090)

View File

@@ -1,58 +0,0 @@
=====================
Reverse Proxy Example
=====================
This example demonstrates how to run a connexion application behind a path-altering reverse proxy.
You can either set the path in your app, or set the ``X-Forwarded-Path`` header.
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/reverse_proxied/ui/ to see the Swagger UI.
You can also use the ``X-Forwarded-Path`` header to modify the reverse proxy path.
For example:
.. code-block:: bash
curl -H "X-Forwarded-Path: /banana/" http://localhost:8080/openapi.json
{
"servers" : [
{
"url" : "banana/"
}
],
"paths" : {
"/hello" : {
"get" : {
"responses" : {
"200" : {
"description" : "hello",
"content" : {
"text/plain" : {
"schema" : {
"type" : "string"
}
}
}
}
},
"operationId" : "app.hello",
"summary" : "say hi"
}
}
},
"openapi" : "3.0.0",
"info" : {
"version" : "1.0",
"title" : "Path-Altering Reverse Proxy Example"
}
}

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env python3
"""
example of connexion running behind a path-altering reverse-proxy
NOTE this demo is not secure by default!!
You'll want to make sure these headers are coming from your proxy, and not
directly from users on the web!
"""
import logging
import connexion
# adapted from http://flask.pocoo.org/snippets/35/
class ReverseProxied:
"""Wrap the application in this middleware and configure the
reverse proxy to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.
In nginx:
location /proxied {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Path /proxied;
}
:param app: the WSGI application
:param script_name: override the default script name (path)
:param scheme: override the default scheme
:param server: override the default server
"""
def __init__(self, app, script_name=None, scheme=None, server=None):
self.app = app
self.script_name = script_name
self.scheme = scheme
self.server = server
def __call__(self, environ, start_response):
logging.warning(
"this demo is not secure by default!! "
"You'll want to make sure these headers are coming from your proxy, "
"and not directly from users on the web!"
)
script_name = environ.get("HTTP_X_FORWARDED_PATH", "") or self.script_name
if script_name:
environ["SCRIPT_NAME"] = "/" + script_name.lstrip("/")
path_info = environ["PATH_INFO"]
if path_info.startswith(script_name):
environ["PATH_INFO_OLD"] = path_info
environ["PATH_INFO"] = path_info[len(script_name) :]
scheme = environ.get("HTTP_X_SCHEME", "") or self.scheme
if scheme:
environ["wsgi.url_scheme"] = scheme
server = environ.get("HTTP_X_FORWARDED_SERVER", "") or self.server
if server:
environ["HTTP_HOST"] = server
return self.app(environ, start_response)
def hello():
return "hello"
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api("openapi.yaml")
flask_app = app.app
proxied = ReverseProxied(flask_app.wsgi_app, script_name="/reverse_proxied/")
flask_app.wsgi_app = proxied
flask_app.run(port=8080)

View File

@@ -1,15 +0,0 @@
==================
SQLAlchemy Example
==================
A simple example of how one might use SQLAlchemy as a backing store for a
Connexion based application.
Running:
.. code-block:: bash
$ sudo pip3 install -r requirements.txt
$ ./app.py
Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI.

View File

@@ -0,0 +1,12 @@
=====================
RestyResolver Example
=====================
Running:
.. code-block:: bash
$ python resty.py
Now open your browser and go to http://localhost:8080/openapi/ui/ or
http://localhost:8080/swagger/ui/ to see the Swagger UI.

23
examples/restyresolver/resty.py Executable file
View File

@@ -0,0 +1,23 @@
import logging
from pathlib import Path
import connexion
from connexion.resolver import RestyResolver
logging.basicConfig(level=logging.INFO)
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api(
"openapi.yaml",
arguments={"title": "RestyResolver Example"},
resolver=RestyResolver("api"),
)
app.add_api(
"swagger.yaml",
arguments={"title": "RestyResolver Example"},
resolver=RestyResolver("api"),
)
if __name__ == "__main__":
app.run(f"{Path(__file__).stem}:app", port=8080)

View File

@@ -5,7 +5,7 @@ info:
license:
name: MIT
servers:
- url: http://localhost:9090/v1.0
- url: /openapi
paths:
/pets:
get:

View File

@@ -4,7 +4,7 @@ info:
title: "{{title}}"
version: "1.0"
basePath: /v1.0
basePath: /swagger
paths:
/pets:

View File

@@ -0,0 +1,88 @@
=====================
Reverse Proxy Example
=====================
This example demonstrates how to run a connexion application behind a path-altering reverse proxy.
You can set the path in three ways:
- Via the Middleware
.. code-block::
app = ReverseProxied(app, root_path="/reverse_proxied/")
- Via the ASGI server
.. code-block::
uvicorn ... --root_path="/reverse_proxied/"
- By using the ``X-Forwarded-Path`` header in your proxy server. Eg in nginx:
.. code-block::
location /proxied {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Path /proxied;
}
To run this example, install Connexion from PyPI:
.. code-block::
$ pip install --upgrade connexion[swagger-ui]
and then run it either directly
.. code-block::
$ python app.py
or using uvicorn (or another async server):
.. code-block::
$ uvicorn --factory app:create_app --port 8080
If your proxy server is running at http://localhost:8080/revers_proxied/, you can go to
http://localhost:8080/reverse_proxied/openapi/ui/ to see the Swagger UI.
Or you can test this using the ``X-Forwarded-Path`` header to modify the reverse proxy path.
For example, note the servers block:
.. code-block:: bash
curl -H "X-Forwarded-Path: /banana/" http://localhost:8080/openapi/openapi.json
{
"servers" : [
{
"url" : "/banana/openapi"
}
],
"paths" : {
"/hello" : {
"get" : {
"responses" : {
"200" : {
"description" : "hello",
"content" : {
"text/plain" : {
"schema" : {
"type" : "string"
}
}
}
}
},
"operationId" : "app.hello",
"summary" : "say hi"
}
}
},
"openapi" : "3.0.0",
"info" : {
"version" : "1.0",
"title" : "Path-Altering Reverse Proxy Example"
}
}

83
examples/reverseproxy/app.py Executable file
View File

@@ -0,0 +1,83 @@
"""
example of connexion running behind a path-altering reverse-proxy
NOTE this demo is not secure by default!!
You'll want to make sure these headers are coming from your proxy, and not
directly from users on the web!
"""
import logging
from pathlib import Path
import connexion
import uvicorn
from starlette.types import Receive, Scope, Send
class ReverseProxied:
"""Wrap the application in this middleware and configure the
reverse proxy to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.
In nginx:
location /proxied {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Path /proxied;
}
:param app: the WSGI application
:param root_path: override the default script name (path)
:param scheme: override the default scheme
:param server: override the default server
"""
def __init__(self, app, root_path=None, scheme=None, server=None):
self.app = app
self.root_path = root_path
self.scheme = scheme
self.server = server
async def __call__(self, scope: Scope, receive: Receive, send: Send):
logging.warning(
"this demo is not secure by default!! "
"You'll want to make sure these headers are coming from your proxy, "
"and not directly from users on the web!"
)
root_path = scope.get("root_path") or self.root_path
for header, value in scope.get("headers", []):
if header == b"x-forwarded-path":
root_path = value.decode()
break
if root_path:
scope["root_path"] = "/" + root_path.strip("/")
path_info = scope.get("PATH_INFO", scope.get("path"))
if path_info.startswith(root_path):
scope["PATH_INFO"] = path_info[len(root_path) :]
scope["scheme"] = scope.get("scheme") or self.scheme
scope["server"] = scope.get("server") or (self.server, None)
return await self.app(scope, receive, send)
def hello():
return "hello"
def create_app():
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml")
app.add_api("swagger.yaml")
app = ReverseProxied(app, root_path="/reverse_proxied/")
return app
if __name__ == "__main__":
uvicorn.run(
f"{Path(__file__).stem}:create_app", factory=True, port=8080, proxy_headers=True
)

View File

@@ -2,6 +2,8 @@ openapi: 3.0.0
info:
title: Path-Altering Reverse Proxy Example
version: '1.0'
servers:
- url: /openapi
paths:
/hello:
get:

View File

@@ -0,0 +1,15 @@
swagger: "2.0"
info:
title: Path-Altering Reverse Proxy Example
version: '1.0'
basePath: /swagger
paths:
/hello:
get:
summary: say hi
operationId: app.hello
responses:
'200':
description: hello
schema:
type: string

View File

@@ -9,7 +9,7 @@ Running:
.. code-block:: bash
$ sudo pip3 install -r requirements.txt
$ ./app.py
$ pip install -r requirements.txt
$ python app.py
Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI.

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
import datetime
import logging
@@ -48,8 +47,9 @@ def delete_pet(pet_id):
logging.basicConfig(level=logging.INFO)
db_session = orm.init_db("sqlite:///:memory:")
app = connexion.FlaskApp(__name__)
app = connexion.FlaskApp(__name__, specification_dir="spec")
app.add_api("openapi.yaml")
app.add_api("swagger.yaml")
application = app.app
@@ -60,4 +60,4 @@ def shutdown_session(exception=None):
if __name__ == "__main__":
app.run(port=8081, use_reloader=False, threaded=False)
app.run(port=8080, reload=False)

View File

@@ -1,6 +1,6 @@
openapi: 3.0.0
servers:
- url: http://localhost:8081/
- url: /openapi
info:
title: Pet Shop Example API
version: '0.1'

View File

@@ -2,6 +2,7 @@ swagger: '2.0'
info:
title: Pet Shop Example API
version: "0.1"
basePath: /swagger
consumes:
- application/json
produces:

View File

@@ -1,14 +0,0 @@
=======================
HTTP Basic Auth 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 credentials are ``admin:secret`` and ``foo:bar``.

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env python3
"""
Basic example of a resource server
"""
import connexion
PASSWD = {"admin": "secret", "foo": "bar"}
def basic_auth(username, password):
if PASSWD.get(username) == password:
return {"sub": username}
# optional: raise exception for custom error response
return None
def get_secret(user) -> str:
return f"You are {user} and the secret is 'wbevuec'"
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api("swagger.yaml")
app.run(port=8080)

View File

@@ -1,11 +0,0 @@
===================
Hello World Example
===================
Running:
.. code-block:: bash
$ ./hello.py
Now open your browser and go to http://localhost:9090/v1.0/ui/ to see the Swagger UI.

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env python3
import connexion
def post_greeting(name: str) -> str:
return f"Hello {name}"
if __name__ == "__main__":
app = connexion.FlaskApp(__name__, port=9090, specification_dir="swagger/")
app.add_api("helloworld-api.yaml", arguments={"title": "Hello World Example"})
app.run()

View File

@@ -1,116 +0,0 @@
swagger: '2.0'
info:
title: Pet Shop Example API
version: "0.1"
consumes:
- application/json
produces:
- application/json
paths:
/pets:
get:
tags: [Pets]
summary: Get all pets
parameters:
- name: animal_type
in: query
type: string
pattern: "^[a-zA-Z0-9]*$"
- name: limit
in: query
type: integer
minimum: 0
default: 100
responses:
200:
description: Return pets
schema:
type: array
items:
$ref: '#/definitions/Pet'
examples:
application/json:
- id: 1
name: Susie
animal_type: cat
/pets/{pet_id}:
get:
tags: [Pets]
summary: Get a single pet
parameters:
- $ref: '#/parameters/pet_id'
responses:
200:
description: Return pet
schema:
$ref: '#/definitions/Pet'
404:
description: Pet does not exist
put:
tags: [Pets]
summary: Create or update a pet
parameters:
- $ref: '#/parameters/pet_id'
- name: pet
in: body
schema:
$ref: '#/definitions/Pet'
responses:
200:
description: Pet updated
201:
description: New pet created
delete:
tags: [Pets]
summary: Remove a pet
parameters:
- $ref: '#/parameters/pet_id'
responses:
204:
description: Pet was deleted
404:
description: Pet does not exist
parameters:
pet_id:
name: pet_id
description: Pet's Unique identifier
in: path
type: string
required: true
pattern: "^[a-zA-Z0-9-]+$"
definitions:
Pet:
type: object
required:
- name
- animal_type
properties:
id:
type: string
description: Unique identifier
example: "123"
readOnly: true
name:
type: string
description: Pet's name
example: "Susie"
minLength: 1
maxLength: 100
animal_type:
type: string
description: Kind of animal
example: "cat"
minLength: 1
tags:
type: object
description: Custom tags
created:
type: string
format: date-time
description: Creation time
example: "2015-07-07T15:49:51.230+02:00"
readOnly: true

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env python3
"""
Basic example of a resource server
"""
import connexion
def get_secret(user) -> str:
return f"You are: {user}"
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api("app.yaml")
app.run(port=8080)

View File

@@ -1,11 +0,0 @@
=====================
RestyResolver Example
=====================
Running:
.. code-block:: bash
$ ./resty.py
Now open your browser and go to http://localhost:9090/v1.0/ui/ to see the Swagger UI.

View File

@@ -1 +0,0 @@
import api.pets # noqa

View File

@@ -1,43 +0,0 @@
import datetime
from connexion import NoContent
pets = {}
def post(pet):
count = len(pets)
pet["id"] = count + 1
pet["registered"] = datetime.datetime.now()
pets[pet["id"]] = pet
return pet, 201
def put(id, pet):
id = int(id)
if pets.get(id) is None:
return NoContent, 404
pets[id] = pet
return pets[id]
def delete(id):
id = int(id)
if pets.get(id) is None:
return NoContent, 404
del pets[id]
return NoContent, 204
def get(id):
id = int(id)
if pets.get(id) is None:
return NoContent, 404
return pets[id]
def search():
# NOTE: we need to wrap it with list for Python 3 as dict_values is not JSON serializable
return list(pets.values())

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env python
import logging
import connexion
from connexion.resolver import RestyResolver
logging.basicConfig(level=logging.INFO)
if __name__ == "__main__":
app = connexion.FlaskApp(__name__)
app.add_api(
"resty-api.yaml",
arguments={"title": "RestyResolver Example"},
resolver=RestyResolver("api"),
)
app.run(port=9090)

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env python3
import datetime
import logging
import connexion
import orm
from connexion import NoContent
db_session = None
def get_pets(limit, animal_type=None):
q = db_session.query(orm.Pet)
if animal_type:
q = q.filter(orm.Pet.animal_type == animal_type)
return [p.dump() for p in q][:limit]
def get_pet(pet_id):
pet = db_session.query(orm.Pet).filter(orm.Pet.id == pet_id).one_or_none()
return pet.dump() if pet is not None else ("Not found", 404)
def put_pet(pet_id, pet):
p = db_session.query(orm.Pet).filter(orm.Pet.id == pet_id).one_or_none()
pet["id"] = pet_id
if p is not None:
logging.info("Updating pet %s..", pet_id)
p.update(**pet)
else:
logging.info("Creating pet %s..", pet_id)
pet["created"] = datetime.datetime.utcnow()
db_session.add(orm.Pet(**pet))
db_session.commit()
return NoContent, (200 if p is not None else 201)
def delete_pet(pet_id):
pet = db_session.query(orm.Pet).filter(orm.Pet.id == pet_id).one_or_none()
if pet is not None:
logging.info("Deleting pet %s..", pet_id)
db_session.query(orm.Pet).filter(orm.Pet.id == pet_id).delete()
db_session.commit()
return NoContent, 204
else:
return NoContent, 404
logging.basicConfig(level=logging.INFO)
db_session = orm.init_db("sqlite:///:memory:")
app = connexion.FlaskApp(__name__)
app.add_api("swagger.yaml")
application = app.app
@application.teardown_appcontext
def shutdown_session(exception=None):
db_session.remove()
if __name__ == "__main__":
app.run(port=8080, threaded=False) # in-memory database isn't shared across threads

View File

@@ -1,34 +0,0 @@
from sqlalchemy import Column, DateTime, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
Base = declarative_base()
class Pet(Base):
__tablename__ = "pets"
id = Column(String(20), primary_key=True)
name = Column(String(100))
animal_type = Column(String(20))
created = Column(DateTime())
def update(self, id=None, name=None, animal_type=None, tags=None, created=None):
if name is not None:
self.name = name
if animal_type is not None:
self.animal_type = animal_type
if created is not None:
self.created = created
def dump(self):
return {k: v for k, v in vars(self).items() if not k.startswith("_")}
def init_db(uri):
engine = create_engine(uri, convert_unicode=True)
db_session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine)
)
Base.query = db_session.query_property()
Base.metadata.create_all(bind=engine)
return db_session

View File

@@ -1,3 +0,0 @@
connexion>=1.0.97
Flask>=0.10.1
SQLAlchemy>=1.0.13

View File

@@ -52,6 +52,10 @@ docs_require = [
'sphinx-autoapi==1.8.1'
]
uvicorn_requires = [
'uvicorn[standard]>=0.17.6'
]
class PyTest(TestCommand):
@@ -100,7 +104,8 @@ setup(
'tests': tests_require,
'flask': flask_require,
'swagger-ui': swagger_ui_require,
'docs': docs_require
'docs': docs_require,
'uvicorn': uvicorn_requires,
},
cmdclass={'test': PyTest},
test_suite='tests',

View File

@@ -19,9 +19,7 @@ def test_app_with_relative_path(simple_api_spec_dir, spec):
# Create the app with a relative path and run the test_app testcase below.
app = App(
__name__,
port=5001,
specification_dir=".." / simple_api_spec_dir.relative_to(TEST_FOLDER),
debug=True,
)
app.add_api(spec)
@@ -38,7 +36,6 @@ def test_app_with_resolver(simple_api_spec_dir, spec):
resolver = Resolver()
app = App(
__name__,
port=5001,
specification_dir=".." / simple_api_spec_dir.relative_to(TEST_FOLDER),
resolver=resolver,
)
@@ -46,33 +43,13 @@ def test_app_with_resolver(simple_api_spec_dir, spec):
assert api.resolver is resolver
@pytest.mark.parametrize("spec", SPECS)
def test_app_with_different_server_option(simple_api_spec_dir, spec):
# Create the app with a relative path and run the test_app testcase below.
app = App(
__name__,
port=5001,
server="gevent",
specification_dir=".." / simple_api_spec_dir.relative_to(TEST_FOLDER),
debug=True,
)
app.add_api(spec)
app_client = app.app.test_client()
get_bye = app_client.get("/v1.0/bye/jsantos") # type: flask.Response
assert get_bye.status_code == 200
assert get_bye.data == b"Goodbye jsantos"
def test_app_with_different_uri_parser(simple_api_spec_dir):
from connexion.uri_parsing import FirstValueURIParser
app = App(
__name__,
port=5001,
specification_dir=".." / simple_api_spec_dir.relative_to(TEST_FOLDER),
options={"uri_parser_class": FirstValueURIParser},
debug=True,
)
app.add_api("swagger.yaml")
@@ -87,7 +64,7 @@ def test_app_with_different_uri_parser(simple_api_spec_dir):
@pytest.mark.parametrize("spec", SPECS)
def test_swagger_ui(simple_api_spec_dir, spec):
app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(spec)
app_client = app.app.test_client()
swagger_ui = app_client.get("/v1.0/ui/") # type: flask.Response
@@ -104,10 +81,8 @@ def test_swagger_ui_with_config(simple_api_spec_dir, spec):
options = {"swagger_ui_config": swagger_ui_config}
app = App(
__name__,
port=5001,
specification_dir=simple_api_spec_dir,
options=options,
debug=True,
)
app.add_api(spec)
app_client = app.app.test_client()
@@ -122,10 +97,8 @@ def test_no_swagger_ui(simple_api_spec_dir, spec):
options = {"swagger_ui": False}
app = App(
__name__,
port=5001,
specification_dir=simple_api_spec_dir,
options=options,
debug=True,
)
app.add_api(spec)
@@ -133,7 +106,7 @@ def test_no_swagger_ui(simple_api_spec_dir, spec):
swagger_ui = app_client.get("/v1.0/ui/") # type: flask.Response
assert swagger_ui.status_code == 404
app2 = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app2 = App(__name__, specification_dir=simple_api_spec_dir)
app2.add_api(spec, options={"swagger_ui": False})
app2_client = app2.app.test_client()
swagger_ui2 = app2_client.get("/v1.0/ui/") # type: flask.Response
@@ -147,10 +120,8 @@ def test_swagger_ui_config_json(simple_api_spec_dir, spec):
options = {"swagger_ui_config": swagger_ui_config}
app = App(
__name__,
port=5001,
specification_dir=simple_api_spec_dir,
options=options,
debug=True,
)
app.add_api(spec)
app_client = app.app.test_client()
@@ -165,7 +136,7 @@ def test_swagger_ui_config_json(simple_api_spec_dir, spec):
@pytest.mark.parametrize("spec", SPECS)
def test_no_swagger_ui_config_json(simple_api_spec_dir, spec):
"""Verify the swagger-ui-config.json file is not returned when the swagger_ui_config option not passed to app."""
app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(spec)
app_client = app.app.test_client()
url = "/v1.0/ui/swagger-ui-config.json"
@@ -176,7 +147,7 @@ def test_no_swagger_ui_config_json(simple_api_spec_dir, spec):
@pytest.mark.parametrize("spec", SPECS)
def test_swagger_json_app(simple_api_spec_dir, spec):
"""Verify the spec json file is returned for default setting passed to app."""
app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(spec)
app_client = app.app.test_client()
url = "/v1.0/{spec}"
@@ -188,7 +159,7 @@ def test_swagger_json_app(simple_api_spec_dir, spec):
@pytest.mark.parametrize("spec", SPECS)
def test_swagger_yaml_app(simple_api_spec_dir, spec):
"""Verify the spec yaml file is returned for default setting passed to app."""
app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(spec)
app_client = app.app.test_client()
url = "/v1.0/{spec}"
@@ -203,10 +174,8 @@ def test_no_swagger_json_app(simple_api_spec_dir, spec):
options = {"serve_spec": False}
app = App(
__name__,
port=5001,
specification_dir=simple_api_spec_dir,
options=options,
debug=True,
)
app.add_api(spec)
@@ -231,7 +200,7 @@ def test_dict_as_yaml_path(simple_api_spec_dir, spec):
openapi_string = jinja2.Template(openapi_template).render({})
specification = yaml.load(openapi_string, ExtendedSafeLoader) # type: dict
app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(specification)
app_client = app.app.test_client()
@@ -243,7 +212,7 @@ def test_dict_as_yaml_path(simple_api_spec_dir, spec):
@pytest.mark.parametrize("spec", SPECS)
def test_swagger_json_api(simple_api_spec_dir, spec):
"""Verify the spec json file is returned for default setting passed to api."""
app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(spec)
app_client = app.app.test_client()
@@ -255,7 +224,7 @@ def test_swagger_json_api(simple_api_spec_dir, spec):
@pytest.mark.parametrize("spec", SPECS)
def test_no_swagger_json_api(simple_api_spec_dir, spec):
"""Verify the spec json file is not returned when set to False when adding api."""
app = App(__name__, port=5001, specification_dir=simple_api_spec_dir, debug=True)
app = App(__name__, specification_dir=simple_api_spec_dir)
app.add_api(spec, options={"serve_spec": False})
app_client = app.app.test_client()
@@ -321,20 +290,7 @@ def test_add_api_with_function_resolver_function_is_wrapped(simple_api_spec_dir,
def test_default_query_param_does_not_match_defined_type(default_param_error_spec_dir):
with pytest.raises(InvalidSpecification):
build_app_from_fixture(
default_param_error_spec_dir, validate_responses=True, debug=False
)
def test_handle_add_operation_error_debug(simple_api_spec_dir):
app = App(__name__, specification_dir=simple_api_spec_dir, debug=True)
app.api_cls = type("AppTest", (app.api_cls,), {})
app.api_cls.add_operation = mock.MagicMock(
side_effect=Exception("operation error!")
)
api = app.add_api("swagger.yaml", resolver=lambda oid: (lambda foo: "bar"))
assert app.api_cls.add_operation.called
assert api.resolver.resolve_function_from_operation_id("faux")("bah") == "bar"
build_app_from_fixture(default_param_error_spec_dir, validate_responses=True)
def test_handle_add_operation_error(simple_api_spec_dir):

View File

@@ -7,8 +7,6 @@ from werkzeug.test import Client, EnvironBuilder
def test_app(simple_app):
assert simple_app.port == 5001
app_client = simple_app.app.test_client()
# by default the Swagger UI is enabled

View File

@@ -112,7 +112,7 @@ def security_handler_factory():
@pytest.fixture
def app():
cnx_app = App(__name__, port=5001, specification_dir=SPEC_FOLDER, debug=True)
cnx_app = App(__name__, specification_dir=SPEC_FOLDER)
cnx_app.add_api("api.yaml", validate_responses=True)
return cnx_app
@@ -157,10 +157,8 @@ def build_app_from_fixture(
cnx_app = App(
__name__,
port=5001,
specification_dir=FIXTURES_FOLDER / api_spec_folder,
middlewares=middlewares,
debug=debug,
)
cnx_app.add_api(spec_file, **kwargs)

View File

@@ -31,7 +31,7 @@ async def test_injection():
return "body"
parameter_decorator = parameter_to_arg(Op(), handler)
await parameter_decorator(request)
parameter_decorator(request)
func.assert_called_with(p1="123")
@@ -61,7 +61,7 @@ async def test_injection_with_context():
return "body"
parameter_decorator = parameter_to_arg(Op2(), handler)
await parameter_decorator(request)
parameter_decorator(request)
func.assert_called_with(request.context, p1="123")

View File

@@ -89,40 +89,6 @@ def test_invalid_operation_does_stop_application_to_setup():
)
def test_invalid_operation_does_not_stop_application_in_debug_mode():
api = FlaskApi(
TEST_FOLDER / "fixtures/op_error_api/swagger.yaml",
base_path="/api/v1.0",
arguments={"title": "OK"},
debug=True,
)
assert api.specification["info"]["title"] == "OK"
api = FlaskApi(
TEST_FOLDER / "fixtures/missing_op_id/swagger.yaml",
base_path="/api/v1.0",
arguments={"title": "OK"},
debug=True,
)
assert api.specification["info"]["title"] == "OK"
api = FlaskApi(
TEST_FOLDER / "fixtures/module_not_implemented/swagger.yaml",
base_path="/api/v1.0",
arguments={"title": "OK"},
debug=True,
)
assert api.specification["info"]["title"] == "OK"
api = FlaskApi(
TEST_FOLDER / "fixtures/user_module_loading_error/swagger.yaml",
base_path="/api/v1.0",
arguments={"title": "OK"},
debug=True,
)
assert api.specification["info"]["title"] == "OK"
def test_other_errors_stop_application_to_setup():
# Errors should still result exceptions!
with pytest.raises(InvalidSpecification):
@@ -139,7 +105,6 @@ def test_invalid_schema_file_structure():
TEST_FOLDER / "fixtures/invalid_schema/swagger.yaml",
base_path="/api/v1.0",
arguments={"title": "OK"},
debug=True,
)

View File

@@ -6,13 +6,9 @@ from flask import Flask
def test_flask_app_default_params():
app = FlaskApp("MyApp")
assert app.import_name == "MyApp"
assert app.server == "flask"
assert app.api_cls == FlaskApi
assert app.arguments == {}
# debug should be None so that user can use Flask environment variables to set it
assert app.debug is None
assert app.host is None
assert app.port is None
assert app.resolver is None
assert app.resolver_error is None
assert not app.auth_all_paths

View File

@@ -40,7 +40,6 @@ def expected_arguments():
"swagger_url": None,
},
"auth_all_paths": False,
"debug": False,
}
@@ -62,27 +61,21 @@ def test_run_missing_spec():
def test_run_simple_spec(mock_app_run, spec_file):
default_port = 5000
runner = CliRunner()
runner.invoke(main, ["run", spec_file], catch_exceptions=False)
app_instance = mock_app_run()
app_instance.run.assert_called_with(
port=default_port, host=None, server="flask", debug=False
)
app_instance.run.assert_called()
def test_run_spec_with_host(mock_app_run, spec_file):
default_port = 5000
runner = CliRunner()
runner.invoke(
main, ["run", spec_file, "--host", "custom.host"], catch_exceptions=False
)
app_instance = mock_app_run()
app_instance.run.assert_called_with(
port=default_port, host="custom.host", server="flask", debug=False
)
app_instance.run.assert_called()
def test_run_no_options_all_default(mock_app_run, expected_arguments, spec_file):
@@ -137,19 +130,6 @@ def test_run_using_option_auth_all_paths(mock_app_run, expected_arguments, spec_
mock_app_run.assert_called_with("connexion.cli", **expected_arguments)
def test_run_in_debug_mode(mock_app_run, expected_arguments, spec_file, monkeypatch):
logging_config = MagicMock(name="connexion.cli.logging.basicConfig")
monkeypatch.setattr("connexion.cli.logging.basicConfig", logging_config)
runner = CliRunner()
runner.invoke(main, ["run", spec_file, "-d"], catch_exceptions=False)
logging_config.assert_called_with(level=logging.DEBUG)
expected_arguments["debug"] = True
mock_app_run.assert_called_with("connexion.cli", **expected_arguments)
def test_run_in_very_verbose_mode(
mock_app_run, expected_arguments, spec_file, monkeypatch
):
@@ -161,7 +141,6 @@ def test_run_in_very_verbose_mode(
logging_config.assert_called_with(level=logging.DEBUG)
expected_arguments["debug"] = True
mock_app_run.assert_called_with("connexion.cli", **expected_arguments)
@@ -174,7 +153,6 @@ def test_run_in_verbose_mode(mock_app_run, expected_arguments, spec_file, monkey
logging_config.assert_called_with(level=logging.INFO)
expected_arguments["debug"] = False
mock_app_run.assert_called_with("connexion.cli", **expected_arguments)