Skip to content
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

Merged
merged 3 commits into from
Dec 30, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions connexion/apps/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ def __init__(
self,
import_name,
api_cls,
port=None,
Copy link
Member Author

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.

specification_dir="",
host=None,
arguments=None,
auth_all_paths=False,
debug=None,
Expand All @@ -34,10 +32,6 @@ 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
Expand All @@ -50,14 +44,13 @@ def __init__(
: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 +162,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 Down Expand Up @@ -267,6 +262,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 (
Copy link
Member Author

Choose a reason for hiding this comment

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

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(
Copy link
Member Author

Choose a reason for hiding this comment

The 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):
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
Copy link
Member Author

Choose a reason for hiding this comment

The 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)
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))
Copy link
Member Author

Choose a reason for hiding this comment

The 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,
Expand Down
21 changes: 10 additions & 11 deletions connexion/middleware/swagger_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Copy link
Member Author

Choose a reason for hiding this comment

The 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):
"""
Expand All @@ -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"],
Expand Down Expand Up @@ -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)

Expand Down
22 changes: 22 additions & 0 deletions examples/apikey/README.rst
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
11 changes: 7 additions & 4 deletions examples/openapi3/apikey/app.py → examples/apikey/app.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ openapi: 3.0.0
info:
title: API Key Example
version: '1.0'
servers:
- url: /openapi
Copy link
Member Author

Choose a reason for hiding this comment

The 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:
Expand Down
23 changes: 23 additions & 0 deletions examples/apikey/spec/swagger.yaml
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
Loading