From 7d23a1e1ed51d6aa3f36ea38b0bfd368e2a48548 Mon Sep 17 00:00:00 2001 From: Robbe Sneyders Date: Fri, 13 May 2022 15:04:36 +0200 Subject: [PATCH] Refactor routing into middleware-api-operation model (#1533) * Rename MiddlewareAPI to RoutingAPI * Refactor routing into middleware-api-operation model --- connexion/apis/__init__.py | 2 +- connexion/apis/abstract.py | 4 +- connexion/handlers.py | 7 ++ connexion/middleware/routing.py | 120 ++++++++++---------------------- 4 files changed, 47 insertions(+), 86 deletions(-) diff --git a/connexion/apis/__init__.py b/connexion/apis/__init__.py index da5a4bfdc..4bb486f47 100644 --- a/connexion/apis/__init__.py +++ b/connexion/apis/__init__.py @@ -13,5 +13,5 @@ """ -from .abstract import (AbstractAPI, AbstractMinimalAPI, # NOQA +from .abstract import (AbstractAPI, AbstractRoutingAPI, # NOQA AbstractSwaggerUIAPI) diff --git a/connexion/apis/abstract.py b/connexion/apis/abstract.py index 28cc9aaca..45ec68f5b 100644 --- a/connexion/apis/abstract.py +++ b/connexion/apis/abstract.py @@ -116,7 +116,7 @@ def add_swagger_ui(self): """ -class AbstractMinimalAPI(AbstractSpecAPI): +class AbstractRoutingAPI(AbstractSpecAPI): def __init__( self, @@ -203,7 +203,7 @@ def _handle_add_operation_error(self, path: str, method: str, exc_info: tuple): raise value.with_traceback(traceback) -class AbstractAPI(AbstractMinimalAPI, metaclass=AbstractAPIMeta): +class AbstractAPI(AbstractRoutingAPI, metaclass=AbstractAPIMeta): """ Defines an abstract interface for a Swagger API """ diff --git a/connexion/handlers.py b/connexion/handlers.py index c5198d2a4..2ee11d029 100644 --- a/connexion/handlers.py +++ b/connexion/handlers.py @@ -41,3 +41,10 @@ def randomize_endpoint(self): def get_path_parameter_types(self): return {} + + async def __call__(self, *args, **kwargs): + raise ResolverProblem( + title='Not Implemented', + detail=self.exception.reason, + status=self.status_code + ) diff --git a/connexion/middleware/routing.py b/connexion/middleware/routing.py index 9f5444dcd..f264f0125 100644 --- a/connexion/middleware/routing.py +++ b/connexion/middleware/routing.py @@ -1,33 +1,19 @@ import pathlib import typing as t -from contextlib import contextmanager from contextvars import ContextVar -from starlette.requests import Request as StarletteRequest from starlette.routing import Router from starlette.types import ASGIApp, Receive, Scope, Send -from connexion.apis import AbstractMinimalAPI +from connexion.apis import AbstractRoutingAPI from connexion.exceptions import NotFoundProblem from connexion.middleware import AppMiddleware -from connexion.operations import AbstractOperation, make_operation from connexion.resolver import Resolver ROUTING_CONTEXT = 'connexion_routing' -_scope_receive_send: ContextVar[tuple] = ContextVar('SCOPE_RECEIVE_SEND') - - -class MiddlewareResolver(Resolver): - - def __init__(self, call_next: t.Callable) -> None: - """Resolver that resolves each operation to the provided call_next function.""" - super().__init__() - self.call_next = call_next - - def resolve_function_from_operation_id(self, operation_id: str) -> t.Callable: - return self.call_next +_scope: ContextVar[dict] = ContextVar('SCOPE') class RoutingMiddleware(AppMiddleware): @@ -40,7 +26,7 @@ def __init__(self, app: ASGIApp) -> None: """ self.app = app # Pass unknown routes to next app - self.router = Router(default=self.default_fn) + self.router = Router(default=RoutingOperation(None, self.app)) def add_api( self, @@ -55,10 +41,8 @@ def add_api( :param base_path: Base path where to add this API. :param arguments: Jinja arguments to replace in the spec. """ - kwargs.pop("resolver", None) - resolver = MiddlewareResolver(self.create_call_next()) - api = MiddlewareAPI(specification, base_path=base_path, arguments=arguments, - resolver=resolver, default=self.default_fn, **kwargs) + api = RoutingAPI(specification, base_path=base_path, arguments=arguments, + next_app=self.app, **kwargs) self.router.mount(api.base_path, app=api.router) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: @@ -68,7 +52,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await self.app(scope, receive, send) return - _scope_receive_send.set((scope.copy(), receive, send)) + _scope.set(scope.copy()) # Needs to be set so starlette router throws exceptions instead of returning error responses scope['app'] = self @@ -77,42 +61,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: except ValueError: raise NotFoundProblem - async def default_fn(self, scope: Scope, receive: Receive, send: Send) -> None: - """Callback to call next app as default when no matching route is found.""" - original_scope, *_ = _scope_receive_send.get() - - api_base_path = scope.get('root_path', '')[len(original_scope.get('root_path', '')):] - - extensions = original_scope.setdefault('extensions', {}) - connexion_routing = extensions.setdefault(ROUTING_CONTEXT, {}) - connexion_routing.update({ - 'api_base_path': api_base_path - }) - await self.app(original_scope, receive, send) - - def create_call_next(self): - - async def call_next( - operation: AbstractOperation, - request: StarletteRequest = None - ) -> None: - """Attach operation to scope and pass it to the next app""" - scope, receive, send = _scope_receive_send.get() - - api_base_path = request.scope.get('root_path', '')[len(scope.get('root_path', '')):] - - extensions = scope.setdefault('extensions', {}) - connexion_routing = extensions.setdefault(ROUTING_CONTEXT, {}) - connexion_routing.update({ - 'api_base_path': api_base_path, - 'operation_id': operation.operation_id - }) - return await self.app(scope, receive, send) - - return call_next - -class MiddlewareAPI(AbstractMinimalAPI): +class RoutingAPI(AbstractRoutingAPI): def __init__( self, @@ -120,13 +70,14 @@ def __init__( base_path: t.Optional[str] = None, arguments: t.Optional[dict] = None, resolver: t.Optional[Resolver] = None, - default: ASGIApp = None, + next_app: ASGIApp = None, resolver_error_handler: t.Optional[t.Callable] = None, debug: bool = False, **kwargs ) -> None: """API implementation on top of Starlette Router for Connexion middleware.""" - self.router = Router(default=default) + self.next_app = next_app + self.router = Router(default=RoutingOperation(None, next_app)) super().__init__( specification, @@ -138,28 +89,31 @@ def __init__( ) def add_operation(self, path: str, method: str) -> None: - operation = make_operation( - self.specification, - self, - path, - method, - self.resolver - ) + operation_cls = self.specification.operation_cls + operation = operation_cls.from_spec(self.specification, self, path, method, self.resolver) + routing_operation = RoutingOperation(operation.operation_id, next_app=self.next_app) + self._add_operation_internal(method, path, routing_operation) + + def _add_operation_internal(self, method: str, path: str, operation: 'RoutingOperation') -> None: + self.router.add_route(path, operation, methods=[method]) - @contextmanager - def patch_operation_function(): - """Patch the operation function so no decorators are set in the middleware. This - should be cleaned up by separating the APIs and Operations between the App and - middleware""" - original_operation_function = AbstractOperation.function - AbstractOperation.function = operation._resolution.function - try: - yield - finally: - AbstractOperation.function = original_operation_function - - with patch_operation_function(): - self._add_operation_internal(method, path, operation) - - def _add_operation_internal(self, method: str, path: str, operation: AbstractOperation) -> None: - self.router.add_route(path, operation.function, methods=[method]) + +class RoutingOperation: + + def __init__(self, operation_id: t.Optional[str], next_app: ASGIApp) -> None: + self.operation_id = operation_id + self.next_app = next_app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """Attach operation to scope and pass it to the next app""" + original_scope = _scope.get() + + api_base_path = scope.get('root_path', '')[len(original_scope.get('root_path', '')):] + + extensions = original_scope.setdefault('extensions', {}) + connexion_routing = extensions.setdefault(ROUTING_CONTEXT, {}) + connexion_routing.update({ + 'api_base_path': api_base_path, + 'operation_id': self.operation_id + }) + await self.next_app(original_scope, receive, send)