diff --git a/connexion/middleware/exceptions.py b/connexion/middleware/exceptions.py index cba002d7b..37b4a4c87 100644 --- a/connexion/middleware/exceptions.py +++ b/connexion/middleware/exceptions.py @@ -66,6 +66,4 @@ def common_error_handler(_request: StarletteRequest, exc: Exception) -> Response ) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - # Needs to be set so starlette router throws exceptions instead of returning error responses - scope["app"] = "connexion" await super().__call__(scope, receive, send) diff --git a/connexion/middleware/main.py b/connexion/middleware/main.py index ded10d925..0f405f539 100644 --- a/connexion/middleware/main.py +++ b/connexion/middleware/main.py @@ -471,4 +471,7 @@ def run(self, import_string: str = None, **kwargs): async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if self.middleware_stack is None: self.app, self.middleware_stack = self._build_middleware_stack() + # Set so starlette router throws exceptions instead of returning error responses + # This instance is also passed to any lifespan handler + scope["app"] = self await self.app(scope, receive, send) diff --git a/docs/index.rst b/docs/index.rst index 35c33b028..014b82ba9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -71,6 +71,7 @@ Documentation validation security context + lifespan cookbook exceptions cli diff --git a/docs/lifespan.rst b/docs/lifespan.rst new file mode 100644 index 000000000..0de70909a --- /dev/null +++ b/docs/lifespan.rst @@ -0,0 +1,109 @@ +Lifespan +======== + +You can register lifespan handlers to run code before the app starts, or after it shuts down. +This ideal for setting up and tearing down database connections or machine learning models for +instance. + +.. tab-set:: + + .. tab-item:: AsyncApp + :sync: AsyncApp + + .. code-block:: python + + import contextlib + import typing + + from connexion import AsyncApp, ConnexionMiddleware, request + + @contextlib.asynccontextmanager + def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator: + """Called at startup and shutdown, can yield state which will be available on the + request.""" + client = Client() + yield {"client": client} + client.close() + + def route(): + """Endpoint function called when receiving a request, you can access the state + on the request here.""" + client = request.state.client + client.call() + + app = AsyncApp(__name__, lifespan=lifespan_handler) + + .. tab-item:: FlaskApp + :sync: FlaskApp + + .. code-block:: python + + import contextlib + import typing + + from connexion import FlaskApp, ConnexionMiddleware, request + + @contextlib.asynccontextmanager + def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator: + """Called at startup and shutdown, can yield state which will be available on the + request.""" + client = Client() + yield {"client": client} + client.close() + + def route(): + """Endpoint function called when receiving a request, you can access the state + on the request here.""" + client = request.state.client + client.call() + + app = FlaskApp(__name__, lifespan=lifespan_handler) + + .. tab-item:: ConnexionMiddleware + :sync: ConnexionMiddleware + + .. code-block:: python + + import contextlib + import typing + + from asgi_framework import App + from connexion import ConnexionMiddleware, request + + @contextlib.asynccontextmanager + def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator: + """Called at startup and shutdown, can yield state which will be available on the + request.""" + client = Client() + yield {"client": client} + client.close() + + def endpoint(): + """Endpoint function called when receiving a request, you can access the state + on the request here.""" + client = request.state.client + client.call() + + app = App(__name__) + app = ConnexionMiddleware(app, lifespan=lifespan_handler) + +Running lifespan in tests +------------------------- + +If you want lifespan handlers to be called during tests, you can use the ``test_client`` as a +context manager. + +.. code-block:: python + + def test_homepage(): + app = ... # Set up app + with app.test_client() as client: + # Application's lifespan is called on entering the block. + response = client.get("/") + assert response.status_code == 200 + + # And the lifespan's teardown is run when exiting the block. + +For more information, please refer to the `Starlette documentation`_. + +.. _Starlette documentation: https://www.starlette.io/lifespan/ \ No newline at end of file