diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d036199..90e7dbb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,6 @@ jobs: - uses: astral-sh/setup-uv@v5 - run: | (cd packages/modern-di && uv version $GITHUB_REF_NAME && just publish modern-di) - (cd packages/modern-di-fastapi && uv version $GITHUB_REF_NAME && just publish modern-di-fastapi) (cd packages/modern-di-litestar && uv version $GITHUB_REF_NAME && just publish modern-di-litestar) (cd packages/modern-di-faststream && uv version $GITHUB_REF_NAME && just publish modern-di-faststream) env: diff --git a/.github/workflows/test-fastapi.yml b/.github/workflows/test-fastapi.yml deleted file mode 100644 index 3f6996d..0000000 --- a/.github/workflows/test-fastapi.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: test fastapi - -on: - push: - branches: - - main - paths: - - 'packages/modern-di-fastapi/**' - - '.github/workflows/test-fastapi.yml' - pull_request: - paths: - - 'packages/modern-di-fastapi/**' - - '.github/workflows/test-fastapi.yml' - -concurrency: - group: ${{ github.head_ref || github.run_id }} fastapi - cancel-in-progress: false - -jobs: - pytest: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "3.14" - steps: - - uses: actions/checkout@v4 - - uses: extractions/setup-just@v2 - - uses: astral-sh/setup-uv@v5 - - run: uv python install ${{ matrix.python-version }} - - run: | - just install-ci modern-di-fastapi - just test-fastapi diff --git a/Justfile b/Justfile index 386b19d..c67da16 100644 --- a/Justfile +++ b/Justfile @@ -26,9 +26,6 @@ test *args: test-core *args: uv run --directory=packages/modern-di pytest {{ args }} -test-fastapi *args: - uv run --directory=packages/modern-di-fastapi pytest {{ args }} - test-litestar *args: uv run --directory=packages/modern-di-litestar pytest {{ args }} diff --git a/packages/modern-di-fastapi/README.md b/packages/modern-di-fastapi/README.md deleted file mode 100644 index 2e8d059..0000000 --- a/packages/modern-di-fastapi/README.md +++ /dev/null @@ -1,6 +0,0 @@ -"Modern-DI-FastAPI" -== - -Integration of [Modern-DI](https://github.com/modern-python/modern-di) to FastAPI - -📚 [Documentation](https://modern-di.readthedocs.io) diff --git a/packages/modern-di-fastapi/modern_di_fastapi/__init__.py b/packages/modern-di-fastapi/modern_di_fastapi/__init__.py deleted file mode 100644 index 5803e33..0000000 --- a/packages/modern-di-fastapi/modern_di_fastapi/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from modern_di_fastapi.main import ( - FromDI, - build_di_container, - fastapi_request_provider, - fastapi_websocket_provider, - fetch_di_container, - setup_di, -) - - -__all__ = [ - "FromDI", - "build_di_container", - "fastapi_request_provider", - "fastapi_websocket_provider", - "fetch_di_container", - "setup_di", -] diff --git a/packages/modern-di-fastapi/modern_di_fastapi/main.py b/packages/modern-di-fastapi/modern_di_fastapi/main.py deleted file mode 100644 index c044d7f..0000000 --- a/packages/modern-di-fastapi/modern_di_fastapi/main.py +++ /dev/null @@ -1,71 +0,0 @@ -import contextlib -import dataclasses -import typing - -import fastapi -from fastapi.routing import _merge_lifespan_context -from modern_di import Container, Scope, providers -from starlette.requests import HTTPConnection - - -T_co = typing.TypeVar("T_co", covariant=True) - - -fastapi_request_provider = providers.ContextProvider(scope=Scope.REQUEST, context_type=fastapi.Request) -fastapi_websocket_provider = providers.ContextProvider(scope=Scope.SESSION, context_type=fastapi.WebSocket) - - -def fetch_di_container(app_: fastapi.FastAPI) -> Container: - return typing.cast(Container, app_.state.di_container) - - -@contextlib.asynccontextmanager -async def _lifespan_manager(app_: fastapi.FastAPI) -> typing.AsyncIterator[None]: - container = fetch_di_container(app_) - try: - yield - finally: - await container.close_async() - - -def setup_di(app: fastapi.FastAPI, container: Container) -> Container: - app.state.di_container = container - container.providers_registry.add_providers(fastapi_request_provider, fastapi_websocket_provider) - old_lifespan_manager = app.router.lifespan_context - app.router.lifespan_context = _merge_lifespan_context( - old_lifespan_manager, - _lifespan_manager, - ) - return container - - -async def build_di_container(connection: HTTPConnection) -> typing.AsyncIterator[Container]: - context: dict[type[typing.Any], typing.Any] = {} - scope: Scope | None = None - if isinstance(connection, fastapi.Request): - scope = Scope.REQUEST - context[fastapi.Request] = connection - elif isinstance(connection, fastapi.WebSocket): - context[fastapi.WebSocket] = connection - scope = Scope.SESSION - container = fetch_di_container(connection.app).build_child_container(context=context, scope=scope) - try: - yield container - finally: - await container.close_async() - - -@dataclasses.dataclass(slots=True, frozen=True) -class Dependency(typing.Generic[T_co]): - dependency: providers.AbstractProvider[T_co] | type[T_co] - - async def __call__( - self, request_container: typing.Annotated[Container, fastapi.Depends(build_di_container)] - ) -> T_co: - if isinstance(self.dependency, providers.AbstractProvider): - return request_container.resolve_provider(self.dependency) - return request_container.resolve(dependency_type=self.dependency) - - -def FromDI(dependency: providers.AbstractProvider[T_co] | type[T_co], *, use_cache: bool = True) -> T_co: # noqa: N802 - return typing.cast(T_co, fastapi.Depends(dependency=Dependency(dependency), use_cache=use_cache)) diff --git a/packages/modern-di-fastapi/modern_di_fastapi/py.typed b/packages/modern-di-fastapi/modern_di_fastapi/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/packages/modern-di-fastapi/pyproject.toml b/packages/modern-di-fastapi/pyproject.toml deleted file mode 100644 index 71f3f70..0000000 --- a/packages/modern-di-fastapi/pyproject.toml +++ /dev/null @@ -1,49 +0,0 @@ -[project] -name = "modern-di-fastapi" -description = "Modern-DI integration for FastAPI" -authors = [{ name = "Artur Shiriev", email = "me@shiriev.ru" }] -requires-python = ">=3.10,<4" -license = "MIT" -readme = "README.md" -keywords = ["DI", "dependency injector", "ioc-container", "FastAPI", "python"] -classifiers = [ - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Typing :: Typed", - "Topic :: Software Development :: Libraries", -] -dependencies = ["fastapi>=0.100", "modern-di>=2"] -version = "0" - -[project.urls] -repository = "https://github.com/modern-python/modern-di" -docs = "https://modern-di.readthedocs.io" - -[dependency-groups] -dev = [ - "pytest", - "pytest-cov", - "pytest-asyncio", - "ruff", - "mypy", - "typing-extensions", - "httpx", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build] -include = ["modern_di_fastapi"] - -[tool.pytest.ini_options] -addopts = "--cov=. --cov-report term-missing" -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" - -[tool.coverage.report] -exclude_also = ["if typing.TYPE_CHECKING:"] diff --git a/packages/modern-di-fastapi/tests_fastapi/__init__.py b/packages/modern-di-fastapi/tests_fastapi/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/packages/modern-di-fastapi/tests_fastapi/conftest.py b/packages/modern-di-fastapi/tests_fastapi/conftest.py deleted file mode 100644 index 3a87a47..0000000 --- a/packages/modern-di-fastapi/tests_fastapi/conftest.py +++ /dev/null @@ -1,23 +0,0 @@ -import typing - -import fastapi -import modern_di -import modern_di_fastapi -import pytest -from starlette.testclient import TestClient - -from tests_fastapi.dependencies import Dependencies - - -@pytest.fixture -async def app() -> fastapi.FastAPI: - app_ = fastapi.FastAPI() - container = modern_di.Container(groups=[Dependencies]) - modern_di_fastapi.setup_di(app_, container=container) - return app_ - - -@pytest.fixture -def client(app: fastapi.FastAPI) -> typing.Iterator[TestClient]: - with TestClient(app=app) as test_client: - yield test_client diff --git a/packages/modern-di-fastapi/tests_fastapi/dependencies.py b/packages/modern-di-fastapi/tests_fastapi/dependencies.py deleted file mode 100644 index 9044f66..0000000 --- a/packages/modern-di-fastapi/tests_fastapi/dependencies.py +++ /dev/null @@ -1,33 +0,0 @@ -import dataclasses - -import fastapi -from modern_di import Group, Scope, providers - - -@dataclasses.dataclass(kw_only=True, slots=True) -class SimpleCreator: - dep1: str - - -@dataclasses.dataclass(kw_only=True, slots=True) -class DependentCreator: - dep1: SimpleCreator - - -def fetch_method_from_request(request: fastapi.Request) -> str: - assert isinstance(request, fastapi.Request) - return request.method - - -def fetch_url_from_websocket(websocket: fastapi.WebSocket) -> str: - assert isinstance(websocket, fastapi.WebSocket) - return websocket.url.path - - -class Dependencies(Group): - app_factory = providers.Factory(creator=SimpleCreator, kwargs={"dep1": "original"}) - session_factory = providers.Factory(scope=Scope.SESSION, creator=DependentCreator, bound_type=None) - request_factory = providers.Factory(scope=Scope.REQUEST, creator=DependentCreator, bound_type=None) - action_factory = providers.Factory(scope=Scope.ACTION, creator=DependentCreator, bound_type=None) - request_method = providers.Factory(scope=Scope.REQUEST, creator=fetch_method_from_request, bound_type=None) - websocket_path = providers.Factory(scope=Scope.SESSION, creator=fetch_url_from_websocket, bound_type=None) diff --git a/packages/modern-di-fastapi/tests_fastapi/test_routes.py b/packages/modern-di-fastapi/tests_fastapi/test_routes.py deleted file mode 100644 index ce52413..0000000 --- a/packages/modern-di-fastapi/tests_fastapi/test_routes.py +++ /dev/null @@ -1,50 +0,0 @@ -import typing - -import fastapi -from modern_di import Container -from modern_di_fastapi import FromDI, build_di_container -from starlette import status -from starlette.testclient import TestClient - -from tests_fastapi.dependencies import Dependencies, DependentCreator, SimpleCreator - - -def test_factories(client: TestClient, app: fastapi.FastAPI) -> None: - @app.get("/") - async def read_root( - app_factory_instance: typing.Annotated[SimpleCreator, FromDI(SimpleCreator)], - request_factory_instance: typing.Annotated[DependentCreator, FromDI(Dependencies.request_factory)], - ) -> None: - assert isinstance(app_factory_instance, SimpleCreator) - assert isinstance(request_factory_instance, DependentCreator) - assert request_factory_instance.dep1 is not app_factory_instance - - response = client.get("/") - assert response.status_code == status.HTTP_200_OK - assert response.json() is None - - -def test_context_provider(client: TestClient, app: fastapi.FastAPI) -> None: - @app.get("/") - async def read_root( - method: typing.Annotated[str, FromDI(Dependencies.request_method)], - ) -> None: - assert method == "GET" - - response = client.get("/") - assert response.status_code == status.HTTP_200_OK - assert response.json() is None - - -def test_factories_action_scope(client: TestClient, app: fastapi.FastAPI) -> None: - @app.get("/") - async def read_root( - request_container: typing.Annotated[Container, fastapi.Depends(build_di_container)], - ) -> None: - action_container = request_container.build_child_container() - action_factory_instance = action_container.resolve_provider(Dependencies.action_factory) - assert isinstance(action_factory_instance, DependentCreator) - - response = client.get("/") - assert response.status_code == status.HTTP_200_OK - assert response.json() is None diff --git a/packages/modern-di-fastapi/tests_fastapi/test_websockets.py b/packages/modern-di-fastapi/tests_fastapi/test_websockets.py deleted file mode 100644 index f337f09..0000000 --- a/packages/modern-di-fastapi/tests_fastapi/test_websockets.py +++ /dev/null @@ -1,64 +0,0 @@ -import typing - -import fastapi -from modern_di import Container -from modern_di_fastapi import FromDI, build_di_container -from starlette.testclient import TestClient - -from tests_fastapi.dependencies import Dependencies, DependentCreator, SimpleCreator - - -async def test_factories(client: TestClient, app: fastapi.FastAPI) -> None: - @app.websocket("/ws") - async def websocket_endpoint( - websocket: fastapi.WebSocket, - app_factory_instance: typing.Annotated[SimpleCreator, FromDI(SimpleCreator)], - session_factory_instance: typing.Annotated[DependentCreator, FromDI(Dependencies.session_factory)], - ) -> None: - assert isinstance(app_factory_instance, SimpleCreator) - assert isinstance(session_factory_instance, DependentCreator) - assert session_factory_instance.dep1 is not app_factory_instance - - await websocket.accept() - await websocket.send_text("test") - await websocket.close() - - with client.websocket_connect("/ws") as websocket: - data = websocket.receive_text() - assert data == "test" - - -async def test_factories_request_scope(client: TestClient, app: fastapi.FastAPI) -> None: - @app.websocket("/ws") - async def websocket_endpoint( - websocket: fastapi.WebSocket, - session_container: typing.Annotated[Container, fastapi.Depends(build_di_container)], - ) -> None: - request_container = session_container.build_child_container() - request_factory_instance = request_container.resolve_provider(Dependencies.request_factory) - assert isinstance(request_factory_instance, DependentCreator) - - await websocket.accept() - await websocket.send_text("test") - await websocket.close() - - with client.websocket_connect("/ws") as websocket: - data = websocket.receive_text() - assert data == "test" - - -async def test_context_provider(client: TestClient, app: fastapi.FastAPI) -> None: - @app.websocket("/ws") - async def websocket_endpoint( - websocket: fastapi.WebSocket, - path: typing.Annotated[str, FromDI(Dependencies.websocket_path)], - ) -> None: - assert path == "/ws" - - await websocket.accept() - await websocket.send_text("test") - await websocket.close() - - with client.websocket_connect("/ws") as websocket: - data = websocket.receive_text() - assert data == "test"