11import os
22from contextlib import AbstractAsyncContextManager , asynccontextmanager
3- from typing import AsyncIterator , Callable , Optional
3+ from inspect import isawaitable
4+ from typing import TYPE_CHECKING , AsyncIterator , Callable , Optional , cast
45
56from fastapi import FastAPI
67from httpx import ASGITransport , AsyncClient , Timeout
910import logfire
1011from basic_memory .config import ConfigManager , ProjectMode
1112
13+ if TYPE_CHECKING :
14+ from sqlalchemy .ext .asyncio import AsyncEngine , AsyncSession , async_sessionmaker
15+
16+ LocalDatabaseState = tuple ["AsyncEngine" , "async_sessionmaker[AsyncSession]" ]
17+ _MISSING_STATE_VALUE = object ()
18+
1219
1320def _force_local_mode () -> bool :
1421 """Check if local mode is forced via environment variable."""
@@ -49,15 +56,48 @@ def _build_asgi_client(app: FastAPI, timeout: Timeout) -> AsyncClient:
4956 )
5057
5158
52- async def _prepare_local_asgi_database (app : FastAPI ) -> None :
53- """Initialize local ASGI database state before the first request."""
59+ async def _resolve_local_asgi_database (app : FastAPI ) -> LocalDatabaseState :
60+ """Resolve database state for a local ASGI request."""
61+ from basic_memory .deps import get_engine_factory
62+
63+ override = app .dependency_overrides .get (get_engine_factory )
64+ if override is not None :
65+ result = override ()
66+ if isawaitable (result ):
67+ result = await result
68+ return cast (LocalDatabaseState , result )
69+
5470 from basic_memory import db
5571
5672 config = ConfigManager ().config
57- engine , session_maker = await db .get_or_create_db (config .database_path )
73+ return await db .get_or_create_db (config .database_path )
74+
75+
76+ @asynccontextmanager
77+ async def _prepared_local_asgi_database (app : FastAPI ) -> AsyncIterator [None ]:
78+ """Initialize local ASGI database state before the first request."""
79+ previous_engine = getattr (app .state , "engine" , _MISSING_STATE_VALUE )
80+ previous_session_maker = getattr (app .state , "session_maker" , _MISSING_STATE_VALUE )
81+
82+ engine , session_maker = await _resolve_local_asgi_database (app )
5883 app .state .engine = engine
5984 app .state .session_maker = session_maker
6085
86+ try :
87+ yield
88+ finally :
89+ if previous_engine is _MISSING_STATE_VALUE :
90+ if hasattr (app .state , "engine" ):
91+ delattr (app .state , "engine" )
92+ else :
93+ app .state .engine = previous_engine
94+
95+ if previous_session_maker is _MISSING_STATE_VALUE :
96+ if hasattr (app .state , "session_maker" ):
97+ delattr (app .state , "session_maker" )
98+ else :
99+ app .state .session_maker = previous_session_maker
100+
61101
62102@asynccontextmanager
63103async def _asgi_client (timeout : Timeout ) -> AsyncIterator [AsyncClient ]:
@@ -71,9 +111,9 @@ async def _asgi_client(timeout: Timeout) -> AsyncIterator[AsyncClient]:
71111 # under Starlette's request loop and trigger CPython's empty-ready-queue race.
72112 # Outcome: request handling sees the same app.state database objects as API
73113 # lifespan startup would have provided.
74- await _prepare_local_asgi_database (fastapi_app )
75- async with _build_asgi_client (fastapi_app , timeout ) as client :
76- yield client
114+ async with _prepared_local_asgi_database (fastapi_app ):
115+ async with _build_asgi_client (fastapi_app , timeout ) as client :
116+ yield client
77117
78118
79119async def _resolve_cloud_token (config ) -> str :
0 commit comments