mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 04:19:26 +00:00
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:
@@ -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,12 +168,9 @@ 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)
|
||||
logger.error(error_msg)
|
||||
_type, value, traceback = exc_info
|
||||
raise value.with_traceback(traceback)
|
||||
|
||||
|
||||
class AbstractAPI(AbstractRoutingAPI, metaclass=AbstractAPIMeta):
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
22
examples/apikey/README.rst
Normal file
22
examples/apikey/README.rst
Normal 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
|
||||
@@ -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)
|
||||
@@ -2,6 +2,8 @@ openapi: 3.0.0
|
||||
info:
|
||||
title: API Key Example
|
||||
version: '1.0'
|
||||
servers:
|
||||
- url: /openapi
|
||||
paths:
|
||||
/secret:
|
||||
get:
|
||||
23
examples/apikey/spec/swagger.yaml
Normal file
23
examples/apikey/spec/swagger.yaml
Normal 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
|
||||
15
examples/basicauth/README.rst
Normal file
15
examples/basicauth/README.rst
Normal 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``.
|
||||
@@ -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)
|
||||
@@ -2,6 +2,8 @@ openapi: 3.0.0
|
||||
info:
|
||||
title: Basic Auth Example
|
||||
version: '1.0'
|
||||
servers:
|
||||
- url: /openapi
|
||||
paths:
|
||||
/secret:
|
||||
get:
|
||||
@@ -1,9 +1,8 @@
|
||||
swagger: "2.0"
|
||||
|
||||
info:
|
||||
title: Basic Auth Example
|
||||
version: "1.0"
|
||||
|
||||
basePath: /swagger
|
||||
paths:
|
||||
/secret:
|
||||
get:
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -11,7 +11,7 @@ paths:
|
||||
/echo:
|
||||
post:
|
||||
description: Echo passed data
|
||||
operationId: enforcedefaults.echo
|
||||
operationId: app.echo
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
12
examples/helloworld/README.rst
Normal file
12
examples/helloworld/README.rst
Normal 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
16
examples/helloworld/hello.py
Executable 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)
|
||||
@@ -4,7 +4,7 @@ info:
|
||||
title: Hello World
|
||||
version: "1.0"
|
||||
servers:
|
||||
- url: http://localhost:9090/v1.0
|
||||
- url: /openapi
|
||||
|
||||
paths:
|
||||
/greeting/{name}:
|
||||
@@ -4,7 +4,7 @@ info:
|
||||
title: "{{title}}"
|
||||
version: "1.0"
|
||||
|
||||
basePath: /v1.0
|
||||
basePath: /swagger
|
||||
|
||||
paths:
|
||||
/greeting/{name}:
|
||||
12
examples/helloworld_async/README.rst
Normal file
12
examples/helloworld_async/README.rst
Normal 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.
|
||||
21
examples/helloworld_async/hello.py
Executable file
21
examples/helloworld_async/hello.py
Executable 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)
|
||||
@@ -4,7 +4,7 @@ info:
|
||||
title: Hello World
|
||||
version: "1.0"
|
||||
servers:
|
||||
- url: http://localhost:9090/v1.0
|
||||
- url: /openapi
|
||||
|
||||
paths:
|
||||
/greeting/{name}:
|
||||
25
examples/helloworld_async/spec/swagger.yaml
Normal file
25
examples/helloworld_async/spec/swagger.yaml
Normal 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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
3
examples/jwt/requirements.txt
Normal file
3
examples/jwt/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
..[swagger-ui]
|
||||
python-jose[cryptography]
|
||||
Flask>=0.10.1
|
||||
@@ -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
43
examples/methodresolver/app.py
Executable 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)
|
||||
@@ -5,7 +5,7 @@ info:
|
||||
license:
|
||||
name: MIT
|
||||
servers:
|
||||
- url: http://localhost:9090/v1.0
|
||||
- url: /openapi
|
||||
paths:
|
||||
/pets:
|
||||
get:
|
||||
@@ -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
19
examples/oauth2/app.py
Executable 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)
|
||||
@@ -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)
|
||||
37
examples/oauth2/spec/openapi.yaml
Normal file
37
examples/oauth2/spec/openapi.yaml
Normal 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.
|
||||
@@ -4,6 +4,8 @@ info:
|
||||
title: OAuth Example
|
||||
version: "1.0"
|
||||
|
||||
basePath: /swagger
|
||||
|
||||
paths:
|
||||
/secret:
|
||||
get:
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
35
examples/oauth2_local_tokeninfo/spec/openapi.yaml
Normal file
35
examples/oauth2_local_tokeninfo/spec/openapi.yaml
Normal 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.
|
||||
@@ -4,6 +4,8 @@ info:
|
||||
title: OAuth Example
|
||||
version: "1.0"
|
||||
|
||||
basePath: /swagger
|
||||
|
||||
paths:
|
||||
/secret:
|
||||
get:
|
||||
@@ -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
|
||||
@@ -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``.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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"})
|
||||
@@ -1,6 +0,0 @@
|
||||
# Install swagger-ui before connexion.
|
||||
connexion[swagger-ui]
|
||||
|
||||
connexion>=2.2.0
|
||||
python-jose[cryptography]
|
||||
Flask>=0.10.1
|
||||
@@ -1,14 +0,0 @@
|
||||
#
|
||||
# Run all project with:
|
||||
#
|
||||
# tox
|
||||
#
|
||||
[tox]
|
||||
envlist = py3
|
||||
skipsdist = true
|
||||
|
||||
[testenv]
|
||||
deps = -rrequirements.txt
|
||||
|
||||
commands =
|
||||
python app.py
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
12
examples/restyresolver/README.rst
Normal file
12
examples/restyresolver/README.rst
Normal 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
23
examples/restyresolver/resty.py
Executable 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)
|
||||
@@ -5,7 +5,7 @@ info:
|
||||
license:
|
||||
name: MIT
|
||||
servers:
|
||||
- url: http://localhost:9090/v1.0
|
||||
- url: /openapi
|
||||
paths:
|
||||
/pets:
|
||||
get:
|
||||
@@ -4,7 +4,7 @@ info:
|
||||
title: "{{title}}"
|
||||
version: "1.0"
|
||||
|
||||
basePath: /v1.0
|
||||
basePath: /swagger
|
||||
|
||||
paths:
|
||||
/pets:
|
||||
88
examples/reverseproxy/README.rst
Normal file
88
examples/reverseproxy/README.rst
Normal 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
83
examples/reverseproxy/app.py
Executable 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
|
||||
)
|
||||
@@ -2,6 +2,8 @@ openapi: 3.0.0
|
||||
info:
|
||||
title: Path-Altering Reverse Proxy Example
|
||||
version: '1.0'
|
||||
servers:
|
||||
- url: /openapi
|
||||
paths:
|
||||
/hello:
|
||||
get:
|
||||
15
examples/reverseproxy/spec/swagger.yaml
Normal file
15
examples/reverseproxy/spec/swagger.yaml
Normal 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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.0
|
||||
servers:
|
||||
- url: http://localhost:8081/
|
||||
- url: /openapi
|
||||
info:
|
||||
title: Pet Shop Example API
|
||||
version: '0.1'
|
||||
@@ -2,6 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: Pet Shop Example API
|
||||
version: "0.1"
|
||||
basePath: /swagger
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
@@ -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``.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -1 +0,0 @@
|
||||
import api.pets # noqa
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
connexion>=1.0.97
|
||||
Flask>=0.10.1
|
||||
SQLAlchemy>=1.0.13
|
||||
7
setup.py
7
setup.py
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user