-
-
Notifications
You must be signed in to change notification settings - Fork 771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update examples for Connexion 3.0 #1615
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle case when base path is "", which is the case both when endpoint is not part of an API, or when endpoint is part of the API registered on the root path. |
||
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) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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() | ||
|
||
|
@@ -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): | ||
|
@@ -124,89 +118,11 @@ def index(): | |
logger.debug("Adding %s with decorator", rule, extra=kwargs) | ||
return self.app.route(rule, **kwargs) | ||
|
||
def run( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lifted to AbstractApp. |
||
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): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs some work. Will submit a separate PR for this. |
||
if asyncio.iscoroutinefunction(function): | ||
|
||
@functools.wraps(function) | ||
|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sure iterator doesn't run out when registering multiple apis. |
||
|
||
def add_api( | ||
self, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
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 @@ def __init__(self, *args, default: ASGIApp, **kwargs): | |
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed for proper usage of root_path behind reverse proxy. |
||
""" | ||
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 @@ def add_openapi_json(self): | |
(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 @@ def add_swagger_ui(self): | |
# 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) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ openapi: 3.0.0 | |
info: | ||
title: API Key Example | ||
version: '1.0' | ||
servers: | ||
- url: /openapi | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need a base path now that we're registering two apis on the same app. |
||
paths: | ||
/secret: | ||
get: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see no reason to pass these to the app instead of to
app.run
directly.