Skip to content

Commit f745de0

Browse files
committed
fix: make distributabe
1 parent c8c6279 commit f745de0

File tree

6 files changed

+93
-29
lines changed

6 files changed

+93
-29
lines changed

deploy/dev/backend/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS app
1+
FROM ghcr.io/astral-sh/uv:0.7.13-python3.13-bookworm-slim AS app
22

33
WORKDIR /app
44

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from importlib.metadata import version
2+
3+
4+
__version__ = version("app_name_kebab_case")
Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
from dishka import Provider, Scope, make_async_container, provide
22

3+
from app_name_snake_case import __version__
34
from app_name_snake_case.main.common.di import CommonProvider
45
from app_name_snake_case.presentation.fastapi.app import (
56
FastAPIAppCoroutines,
67
FastAPIAppRouters,
8+
FastAPIAppVersion,
79
)
810
from app_name_snake_case.presentation.fastapi.routers import all_routers
911

1012

1113
class FastAPIProvider(Provider):
12-
scope = Scope.APP
13-
14-
@provide
14+
@provide(scope=Scope.APP)
1515
def provide_routers(self) -> FastAPIAppRouters:
16-
return all_routers
16+
return FastAPIAppRouters(all_routers)
1717

18-
@provide
18+
@provide(scope=Scope.APP)
1919
def provide_coroutines(self) -> FastAPIAppCoroutines:
20-
return []
20+
return FastAPIAppCoroutines(tuple())
21+
22+
@provide(scope=Scope.APP)
23+
def provide_version(self) -> FastAPIAppVersion:
24+
return FastAPIAppVersion(__version__)
2125

2226

2327
container = make_async_container(FastAPIProvider(), CommonProvider())

src/app_name_snake_case/presentation/fastapi/app.py

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import asyncio
2-
from collections.abc import AsyncIterator, Coroutine, Iterable
3-
from contextlib import asynccontextmanager, suppress
4-
from typing import Any, cast
2+
from collections.abc import AsyncIterator, Callable, Coroutine
3+
from contextlib import asynccontextmanager
4+
from dataclasses import dataclass, field
5+
from types import TracebackType
6+
from typing import Any, NewType, Self, cast
57

68
from dishka import AsyncContainer
79
from dishka.integrations.fastapi import setup_dishka
@@ -12,21 +14,70 @@
1214
from app_name_snake_case.presentation.fastapi.tags import tags_metadata
1315

1416

15-
type FastAPIAppCoroutines = Iterable[Coroutine[Any, Any, Any]]
16-
type FastAPIAppRouters = Iterable[APIRouter]
17+
FastAPIAppCoroutines = NewType(
18+
"FastAPIAppCoroutines",
19+
tuple[Callable[[], Coroutine[Any, Any, Any]], ...],
20+
)
21+
FastAPIAppRouters = NewType("FastAPIAppRouters", tuple[APIRouter, ...])
22+
FastAPIAppVersion = NewType("FastAPIAppVersion", str)
23+
24+
25+
@dataclass(frozen=True, unsafe_hash=False)
26+
class AppBackgroundTasks:
27+
_loop: asyncio.AbstractEventLoop = field(
28+
default_factory=asyncio.get_running_loop,
29+
)
30+
_tasks: set[asyncio.Task[Any]] = field(init=False, default_factory=set)
31+
32+
async def __aenter__(self) -> Self:
33+
return self
34+
35+
async def __aexit__(
36+
self,
37+
error_type: type[BaseException] | None,
38+
error: BaseException | None,
39+
traceback: TracebackType | None,
40+
) -> None:
41+
for task in self._tasks:
42+
task.cancel()
43+
44+
await asyncio.gather(*self._tasks, return_exceptions=True)
45+
46+
def create_task(
47+
self,
48+
func: Callable[[], Coroutine[Any, Any, Any]],
49+
) -> None:
50+
decorated_func = self._decorator(func)
51+
self._create_task(decorated_func())
52+
53+
def _decorator(
54+
self,
55+
func: Callable[[], Coroutine[Any, Any, Any]],
56+
) -> Callable[[], Coroutine[Any, Any, Any]]:
57+
async def decorated_func() -> None:
58+
try:
59+
await func()
60+
except Exception as error:
61+
self._create_task(decorated_func())
62+
raise error from error
63+
64+
return decorated_func
65+
66+
def _create_task(self, coro: Coroutine[Any, Any, Any]) -> None:
67+
task = self._loop.create_task(coro)
68+
69+
self._tasks.add(task)
70+
task.add_done_callback(self._tasks.discard)
1771

1872

1973
@asynccontextmanager
2074
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
21-
with suppress(asyncio.CancelledError):
22-
async with asyncio.TaskGroup() as tasks:
23-
for coroutine in cast("FastAPIAppCoroutines", app.state.coroutines):
24-
tasks.create_task(coroutine)
25-
26-
yield
75+
async with AppBackgroundTasks() as tasks:
76+
for coroutine in cast("FastAPIAppCoroutines", app.state.coroutines):
77+
tasks.create_task(coroutine)
2778

28-
await app.state.dishka_container.close()
29-
raise asyncio.CancelledError
79+
yield
80+
await app.state.dishka_container.close()
3081

3182

3283
class _FastAPIWithAdditionalModels(FastAPI):
@@ -48,8 +99,8 @@ def openapi(self) -> dict[str, Any]:
4899

49100
async def app_from(container: AsyncContainer) -> FastAPI:
50101
author_url = "https://github.com/emptybutton"
51-
repo_url = f"{author_url}/app_name_snake_case"
52-
version = "0.1.0"
102+
repo_url = f"{author_url}/app_name_kebab_case"
103+
version: FastAPIAppVersion = await container.get(FastAPIAppVersion)
53104

54105
app = _FastAPIWithAdditionalModels(
55106
title="app-name-kebab-case",
@@ -65,12 +116,9 @@ async def app_from(container: AsyncContainer) -> FastAPI:
65116
root_path=f"/api/{version}",
66117
)
67118

68-
coroutines = await container.get(FastAPIAppCoroutines)
69-
routers = await container.get(FastAPIAppRouters)
70-
71-
app.state.coroutines = coroutines
119+
app.state.coroutines = await container.get(FastAPIAppCoroutines)
72120

73-
for router in routers:
121+
for router in await container.get(FastAPIAppRouters):
74122
app.include_router(router)
75123

76124
setup_dishka(container=container, app=app)

src/app_name_snake_case/py.typed

Whitespace-only changes.

tests/test_app_name_snake_case/test_presentation/test_fastapi/test_app.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from app_name_snake_case.presentation.fastapi.app import (
1010
FastAPIAppCoroutines,
1111
FastAPIAppRouters,
12+
FastAPIAppVersion,
1213
app_from,
1314
)
1415

@@ -30,8 +31,15 @@ async def endpoint(x: FromDishka[X]) -> Response:
3031
async def test_app_from() -> None:
3132
provider = Provider(scope=Scope.APP)
3233
provider.provide(lambda: X(x=4), provides=X)
33-
provider.provide(lambda: [router], provides=FastAPIAppRouters)
34-
provider.provide(list, provides=FastAPIAppCoroutines)
34+
provider.provide(
35+
lambda: FastAPIAppRouters((router, )), provides=FastAPIAppRouters,
36+
)
37+
provider.provide(
38+
lambda: FastAPIAppCoroutines(tuple()), provides=FastAPIAppCoroutines,
39+
)
40+
provider.provide(
41+
lambda: FastAPIAppVersion("0.0.0"), provides=FastAPIAppVersion,
42+
)
3543
container = make_async_container(provider)
3644

3745
app = await app_from(container)

0 commit comments

Comments
 (0)