11import 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
68from dishka import AsyncContainer
79from dishka .integrations .fastapi import setup_dishka
1214from 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
2074async 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
3283class _FastAPIWithAdditionalModels (FastAPI ):
@@ -48,8 +99,8 @@ def openapi(self) -> dict[str, Any]:
4899
49100async 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 )
0 commit comments