Skip to content

Commit

Permalink
Update examples for Connexion 3.0 (#1615)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
RobbeSneyders committed Dec 30, 2022
1 parent e5784c5 commit 073f0d4
Show file tree
Hide file tree
Showing 99 changed files with 679 additions and 998 deletions.
14 changes: 3 additions & 11 deletions connexion/apis/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,16 @@ def __init__(
*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()
Expand Down Expand Up @@ -171,12 +168,9 @@ def _handle_add_operation_error(self, path: str, method: str, exc_info: tuple):
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):
Expand All @@ -190,7 +184,6 @@ def __init__(
base_path=None,
arguments=None,
resolver=None,
debug=False,
resolver_error_handler=None,
options=None,
**kwargs,
Expand All @@ -206,7 +199,6 @@ def __init__(
arguments=arguments,
resolver=resolver,
resolver_error_handler=resolver_error_handler,
debug=debug,
options=options,
**kwargs,
)
Expand Down
51 changes: 36 additions & 15 deletions connexion/apps/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ def __init__(
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,
Expand All @@ -34,30 +31,22 @@ def __init__(
"""
: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
Expand Down Expand Up @@ -169,7 +158,9 @@ def add_api(
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)

Expand All @@ -182,7 +173,6 @@ def add_api(
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(),
Expand All @@ -197,7 +187,6 @@ def add_api(
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(),
)
Expand Down Expand Up @@ -267,6 +256,38 @@ def index():
`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):
"""
Expand Down
6 changes: 5 additions & 1 deletion connexion/apps/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ async def asgi_app(self, scope: Scope, receive: Receive, send: Send) -> None:
)

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)

Expand Down
90 changes: 3 additions & 87 deletions connexion/apps/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@


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
Expand All @@ -34,9 +32,7 @@ def __init__(
"""
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()

Expand Down Expand Up @@ -100,8 +96,6 @@ def common_error_handler(self, exception):
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):
Expand All @@ -124,89 +118,11 @@ def index():
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):
Expand Down
2 changes: 1 addition & 1 deletion connexion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion connexion/decorators/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def wrapper(*args, **kwargs):

@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)

Expand Down
3 changes: 2 additions & 1 deletion connexion/decorators/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -72,7 +73,7 @@ async def wrapper(
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
Expand Down
2 changes: 1 addition & 1 deletion connexion/middleware/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"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:
Expand Down
2 changes: 1 addition & 1 deletion connexion/middleware/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _apply_middlewares(
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,
Expand Down

0 comments on commit 073f0d4

Please sign in to comment.