-
-
Notifications
You must be signed in to change notification settings - Fork 308
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
Add Starlette lifespan handler implementation #683
base: master
Are you sure you want to change the base?
Changes from all 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 |
---|---|---|
@@ -0,0 +1,39 @@ | ||
Integration With Starlette-based Frameworks | ||
=========================================== | ||
|
||
This is a `Starlette <https://www.starlette.io/>`_ + | ||
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application | ||
utilizing `lifespan API <https://www.starlette.io/lifespan/>`_. | ||
|
||
.. note:: | ||
|
||
Pretty much `any framework built on top of Starlette <https://www.starlette.io/third-party-packages/#frameworks>`_ | ||
supports this feature (`FastAPI <https://fastapi.tiangolo.com/advanced/events/#lifespan>`_, | ||
`Xpresso <https://xpresso-api.dev/latest/tutorial/lifespan/>`_, etc...). | ||
|
||
Run | ||
--- | ||
|
||
Create virtual environment: | ||
|
||
.. code-block:: bash | ||
|
||
python -m venv env | ||
. env/bin/activate | ||
|
||
Install requirements: | ||
|
||
.. code-block:: bash | ||
|
||
pip install -r requirements.txt | ||
|
||
To run the application do: | ||
|
||
.. code-block:: bash | ||
|
||
python example.py | ||
# or (logging won't be configured): | ||
uvicorn --factory example:container.app | ||
|
||
After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``, | ||
etc). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
#!/usr/bin/env python | ||
|
||
from logging import basicConfig, getLogger | ||
|
||
from dependency_injector.containers import DeclarativeContainer | ||
from dependency_injector.ext.starlette import Lifespan | ||
from dependency_injector.providers import Factory, Resource, Self, Singleton | ||
from starlette.applications import Starlette | ||
from starlette.requests import Request | ||
from starlette.responses import JSONResponse | ||
from starlette.routing import Route | ||
|
||
count = 0 | ||
|
||
|
||
def init(): | ||
log = getLogger(__name__) | ||
log.info("Inittializing resources") | ||
yield | ||
log.info("Cleaning up resources") | ||
|
||
|
||
async def homepage(request: Request) -> JSONResponse: | ||
global count | ||
response = JSONResponse({"hello": "world", "count": count}) | ||
count += 1 | ||
return response | ||
|
||
|
||
class Container(DeclarativeContainer): | ||
__self__ = Self() | ||
lifespan = Singleton(Lifespan, __self__) | ||
logging = Resource( | ||
basicConfig, | ||
level="DEBUG", | ||
datefmt="%Y-%m-%d %H:%M", | ||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", | ||
) | ||
init = Resource(init) | ||
app = Factory( | ||
Starlette, | ||
debug=True, | ||
lifespan=lifespan, | ||
routes=[Route("/", homepage)], | ||
) | ||
|
||
|
||
container = Container() | ||
|
||
if __name__ == "__main__": | ||
import uvicorn | ||
|
||
uvicorn.run( | ||
container.app, | ||
factory=True, | ||
# NOTE: `None` prevents uvicorn from configuring logging, which is | ||
# impossible via CLI | ||
log_config=None, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
dependency-injector | ||
starlette | ||
uvicorn |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import sys | ||
from abc import ABCMeta, abstractmethod | ||
from typing import Any, Callable, Coroutine, Optional | ||
|
||
if sys.version_info >= (3, 11): # pragma: no cover | ||
from typing import Self | ||
else: # pragma: no cover | ||
from typing_extensions import Self | ||
|
||
from dependency_injector.containers import Container | ||
|
||
|
||
class Lifespan: | ||
"""A starlette lifespan handler performing container resource initialization and shutdown. | ||
|
||
See https://www.starlette.io/lifespan/ for details. | ||
|
||
Usage: | ||
|
||
.. code-block:: python | ||
|
||
from dependency_injector.containers import DeclarativeContainer | ||
from dependency_injector.ext.starlette import Lifespan | ||
from dependency_injector.providers import Factory, Self, Singleton | ||
from starlette.applications import Starlette | ||
|
||
class Container(DeclarativeContainer): | ||
__self__ = Self() | ||
lifespan = Singleton(Lifespan, __self__) | ||
app = Factory(Starlette, lifespan=lifespan) | ||
|
||
:param container: container instance | ||
""" | ||
|
||
container: Container | ||
|
||
def __init__(self, container: Container) -> None: | ||
self.container = container | ||
|
||
def __call__(self, app: Any) -> Self: | ||
return self | ||
|
||
async def __aenter__(self) -> None: | ||
result = self.container.init_resources() | ||
|
||
if result is not None: | ||
await result | ||
|
||
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. Hi! 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. You generally want explicit |
||
async def __aexit__(self, *exc_info: Any) -> None: | ||
result = self.container.shutdown_resources() | ||
|
||
if result is not None: | ||
await result |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from typing import AsyncIterator, Iterator | ||
from unittest.mock import ANY | ||
|
||
from pytest import mark | ||
|
||
from dependency_injector.containers import DeclarativeContainer | ||
from dependency_injector.ext.starlette import Lifespan | ||
from dependency_injector.providers import Resource | ||
|
||
|
||
class TestLifespan: | ||
@mark.parametrize("sync", [False, True]) | ||
@mark.asyncio | ||
async def test_context_manager(self, sync: bool) -> None: | ||
init, shutdown = False, False | ||
|
||
def sync_resource() -> Iterator[None]: | ||
nonlocal init, shutdown | ||
|
||
init = True | ||
yield | ||
shutdown = True | ||
|
||
async def async_resource() -> AsyncIterator[None]: | ||
nonlocal init, shutdown | ||
|
||
init = True | ||
yield | ||
shutdown = True | ||
|
||
class Container(DeclarativeContainer): | ||
x = Resource(sync_resource if sync else async_resource) | ||
|
||
container = Container() | ||
lifespan = Lifespan(container) | ||
|
||
async with lifespan(ANY) as scope: | ||
assert scope is None | ||
assert init | ||
|
||
assert shutdown |
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.
Here you need to change the return type probably to
Optional[Awaitable]
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.
def __aenter__(self) -> Awaitable[None]:
-> Optional[Awaitable]
means function returns eitherNone
or something that you canawait
(e.g. signature of theinit_resources
). Given the pt.1, annotating it with your suggestion will result inAwaitable[Optional[Awaitable]]
, which does not conforms to StatelessLifespan protocol.