Skip to content

Commit 9e1936e

Browse files
authored
migrate to modern-di 1.x (#37)
1 parent 53b9f48 commit 9e1936e

13 files changed

Lines changed: 165 additions & 249 deletions

File tree

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
steps:
1919
- uses: actions/checkout@v3
2020
- uses: astral-sh/setup-uv@v3
21-
- run: uv python install 3.13
21+
- run: uv python install 3.14
2222
- run: |
2323
uv sync --all-extras --frozen --no-install-project
2424
uv run ruff format . --check
@@ -45,7 +45,7 @@ jobs:
4545
steps:
4646
- uses: actions/checkout@v3
4747
- uses: astral-sh/setup-uv@v3
48-
- run: uv python install 3.13
48+
- run: uv python install 3.14
4949
- run: |
5050
uv sync --all-extras --frozen --no-install-project
5151
uv run alembic upgrade head

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.13-slim
1+
FROM python:3.14-slim
22

33
# required for psycopg2
44
RUN apt update \

app/api/decks.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sqlalchemy import orm
77
from starlette import status
88

9-
from app import ioc, models, schemas
9+
from app import models, schemas
1010
from app.repositories import CardsService, DecksService
1111

1212

@@ -15,7 +15,7 @@
1515

1616
@ROUTER.get("/decks/")
1717
async def list_decks(
18-
decks_service: DecksService = FromDI(ioc.Dependencies.decks_service),
18+
decks_service: DecksService = FromDI(DecksService),
1919
) -> schemas.Decks:
2020
objects = await decks_service.list()
2121
return typing.cast("schemas.Decks", {"items": objects})
@@ -24,7 +24,7 @@ async def list_decks(
2424
@ROUTER.get("/decks/{deck_id}/")
2525
async def get_deck(
2626
deck_id: int,
27-
decks_service: DecksService = FromDI(ioc.Dependencies.decks_service),
27+
decks_service: DecksService = FromDI(DecksService),
2828
) -> schemas.Deck:
2929
instance = await decks_service.get_one_or_none(
3030
models.Deck.id == deck_id,
@@ -40,7 +40,7 @@ async def get_deck(
4040
async def update_deck(
4141
deck_id: int,
4242
data: schemas.DeckCreate,
43-
decks_service: DecksService = FromDI(ioc.Dependencies.decks_service),
43+
decks_service: DecksService = FromDI(DecksService),
4444
) -> schemas.Deck:
4545
try:
4646
instance = await decks_service.update(data=data.model_dump(), item_id=deck_id)
@@ -53,7 +53,7 @@ async def update_deck(
5353
@ROUTER.post("/decks/")
5454
async def create_deck(
5555
data: schemas.DeckCreate,
56-
decks_service: DecksService = FromDI(ioc.Dependencies.decks_service),
56+
decks_service: DecksService = FromDI(DecksService),
5757
) -> schemas.Deck:
5858
instance = await decks_service.create(data.model_dump())
5959
return typing.cast("schemas.Deck", instance)
@@ -62,7 +62,7 @@ async def create_deck(
6262
@ROUTER.get("/decks/{deck_id}/cards/")
6363
async def list_cards(
6464
deck_id: int,
65-
cards_service: CardsService = FromDI(ioc.Dependencies.cards_service),
65+
cards_service: CardsService = FromDI(CardsService),
6666
) -> schemas.Cards:
6767
objects = await cards_service.list(models.Card.deck_id == deck_id)
6868
return typing.cast("schemas.Cards", {"items": objects})
@@ -71,7 +71,7 @@ async def list_cards(
7171
@ROUTER.get("/cards/{card_id}/")
7272
async def get_card(
7373
card_id: int,
74-
cards_service: CardsService = FromDI(ioc.Dependencies.cards_service),
74+
cards_service: CardsService = FromDI(CardsService),
7575
) -> schemas.Card:
7676
instance = await cards_service.get_one_or_none(models.Card.id == card_id)
7777
if not instance:
@@ -83,7 +83,7 @@ async def get_card(
8383
async def create_cards(
8484
deck_id: int,
8585
data: list[schemas.CardCreate],
86-
cards_service: CardsService = FromDI(ioc.Dependencies.cards_service),
86+
cards_service: CardsService = FromDI(CardsService),
8787
) -> schemas.Cards:
8888
objects = await cards_service.create_many(
8989
data=[models.Card(**card.model_dump(), deck_id=deck_id) for card in data],
@@ -95,7 +95,7 @@ async def create_cards(
9595
async def update_cards(
9696
deck_id: int,
9797
data: list[schemas.Card],
98-
cards_service: CardsService = FromDI(ioc.Dependencies.cards_service),
98+
cards_service: CardsService = FromDI(CardsService),
9999
) -> schemas.Cards:
100100
objects = await cards_service.upsert_many(
101101
data=[models.Card(**card.model_dump(exclude={"deck_id"}), deck_id=deck_id) for card in data],

app/application.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import dataclasses
2+
from typing import TYPE_CHECKING
23

3-
import fastapi
4+
import modern_di
45
import modern_di_fastapi
56
from advanced_alchemy.exceptions import DuplicateKeyError
67
from lite_bootstrap import FastAPIBootstrapper
78
from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
89
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
910

10-
from app import exceptions
11+
from app import exceptions, ioc
1112
from app.api.decks import ROUTER
1213
from app.settings import settings
1314

1415

16+
if TYPE_CHECKING:
17+
import fastapi
18+
19+
1520
def include_routers(app: fastapi.FastAPI) -> None:
1621
app.include_router(ROUTER, prefix="/api")
1722

1823

1924
def build_app() -> fastapi.FastAPI:
25+
di_container = modern_di.AsyncContainer(groups=[ioc.Dependencies])
2026
bootstrap_config = dataclasses.replace(
2127
settings.api_bootstrapper_config,
2228
opentelemetry_instrumentors=[
@@ -26,7 +32,7 @@ def build_app() -> fastapi.FastAPI:
2632
)
2733
bootstrapper = FastAPIBootstrapper(bootstrap_config=bootstrap_config)
2834
app: fastapi.FastAPI = bootstrapper.bootstrap()
29-
modern_di_fastapi.setup_di(app)
35+
modern_di_fastapi.setup_di(app, di_container)
3036
include_routers(app)
3137
app.add_exception_handler(
3238
DuplicateKeyError,

app/exceptions.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
from advanced_alchemy.exceptions import DuplicateKeyError
1+
from typing import TYPE_CHECKING
2+
23
from fastapi.responses import JSONResponse
34
from starlette import status
4-
from starlette.requests import Request
5+
6+
7+
if TYPE_CHECKING:
8+
from advanced_alchemy.exceptions import DuplicateKeyError
9+
from starlette.requests import Request
510

611

712
async def duplicate_key_error_handler(_: Request, exc: DuplicateKeyError) -> JSONResponse:

app/ioc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from modern_di import BaseGraph, Scope, providers
1+
from modern_di import Group, Scope, providers
22

33
from app import repositories
44
from app.resources.db import create_sa_engine, create_session
55

66

7-
class Dependencies(BaseGraph):
7+
class Dependencies(Group):
88
database_engine = providers.Resource(Scope.APP, create_sa_engine)
99
session = providers.Resource(Scope.REQUEST, create_session, engine=database_engine.cast)
1010

app/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Deck(BigIntAuditBase):
1414

1515
name: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False)
1616
description: orm.Mapped[str | None] = orm.mapped_column(sa.String, nullable=True)
17-
cards: orm.Mapped[list["Card"]] = orm.relationship("Card", lazy="noload", uselist=True)
17+
cards: orm.Mapped[list[Card]] = orm.relationship("Card", lazy="noload", uselist=True)
1818

1919

2020
class Card(BigIntAuditBase):

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ name = "fastapi-sqlalchemy-template"
33
version = "0"
44
description = "Async template on FastAPI and SQLAlchemy 2"
55
readme = "README.md"
6-
requires-python = ">=3.13"
6+
requires-python = ">=3.14"
77
authors = [
88
{ name = "Artur Shiriev", email = "me@shiriev.ru" },
99
]
1010
license = "MIT License"
1111
dependencies = [
1212
"fastapi>=0.76",
1313
"lite-bootstrap[fastapi-all]",
14-
"modern-di-fastapi",
14+
"modern-di-fastapi>=1",
1515
"advanced-alchemy",
1616
"pydantic-settings",
1717
"granian[uvloop]",
@@ -42,7 +42,7 @@ dev = [
4242
fix = true
4343
unsafe-fixes = true
4444
line-length = 120
45-
target-version = "py313"
45+
target-version = "py314"
4646
extend-exclude = ["bin"]
4747

4848
[tool.ruff.lint]
@@ -69,7 +69,7 @@ isort.no-lines-before = ["standard-library", "local-folder"]
6969
]
7070

7171
[tool.mypy]
72-
python_version = "3.13"
72+
python_version = "3.14"
7373
strict = true
7474
pretty = true
7575

tests/conftest.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import typing
22

3-
import fastapi
4-
import modern_di
53
import modern_di_fastapi
64
import pytest
75
from asgi_lifespan import LifespanManager
@@ -12,6 +10,11 @@
1210
from app.application import build_app
1311

1412

13+
if typing.TYPE_CHECKING:
14+
import fastapi
15+
import modern_di
16+
17+
1518
@pytest.fixture
1619
async def app() -> typing.AsyncIterator[fastapi.FastAPI]:
1720
app_ = build_app()
@@ -29,17 +32,17 @@ async def client(app: fastapi.FastAPI) -> typing.AsyncIterator[AsyncClient]:
2932

3033

3134
@pytest.fixture
32-
def di_container(app: fastapi.FastAPI) -> modern_di.Container:
35+
def di_container(app: fastapi.FastAPI) -> modern_di.AsyncContainer:
3336
return modern_di_fastapi.fetch_di_container(app)
3437

3538

3639
@pytest.fixture(autouse=True)
37-
async def db_session(di_container: modern_di.Container) -> typing.AsyncIterator[AsyncSession]:
38-
engine = await ioc.Dependencies.database_engine.async_resolve(di_container)
40+
async def db_session(di_container: modern_di.AsyncContainer) -> typing.AsyncIterator[AsyncSession]:
41+
engine = await di_container.resolve_provider(ioc.Dependencies.database_engine)
3942
connection = await engine.connect()
4043
transaction = await connection.begin()
4144
await connection.begin_nested()
42-
ioc.Dependencies.database_engine.override(connection, di_container)
45+
di_container.override(ioc.Dependencies.database_engine, connection)
4346

4447
try:
4548
yield AsyncSession(connection, expire_on_commit=False, autoflush=False)

tests/test_cards.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
from typing import TYPE_CHECKING
2+
13
from fastapi import status
2-
from httpx import AsyncClient
3-
from sqlalchemy.ext.asyncio import AsyncSession
44

55
from tests import factories
66

77

8+
if TYPE_CHECKING:
9+
from httpx import AsyncClient
10+
from sqlalchemy.ext.asyncio import AsyncSession
11+
12+
813
async def test_get_cards_empty(client: AsyncClient, db_session: AsyncSession) -> None:
914
factories.DeckModelFactory.__async_session__ = db_session
1015
deck = await factories.DeckModelFactory.create_async()

0 commit comments

Comments
 (0)