"""Centralized accessors for singleton objects stored on ``app.state``. **Getters** (used by routers): raise 503 when a required dependency is missing, except ``get_store`` which returns ``None``. Initialization is handled directly in ``app.py`` via :class:`AsyncExitStack``. """ from __future__ import annotations from collections.abc import AsyncGenerator from contextlib import AsyncExitStack, asynccontextmanager from typing import TYPE_CHECKING from fastapi import FastAPI, HTTPException, Request from deerflow.runtime import RunManager, StreamBridge if TYPE_CHECKING: from app.gateway.auth.local_provider import LocalAuthProvider from app.gateway.auth.repositories.sqlite import SQLiteUserRepository # --------------------------------------------------------------------------- # Getters – called by routers per-request # --------------------------------------------------------------------------- def get_stream_bridge(request: Request) -> StreamBridge: """Return the global :class:`StreamBridge`, or 503.""" bridge = getattr(request.app.state, "stream_bridge", None) if bridge is None: raise HTTPException(status_code=503, detail="Stream bridge not available") return bridge def get_run_manager(request: Request) -> RunManager: """Return the global :class:`RunManager`, or 503.""" mgr = getattr(request.app.state, "run_manager", None) if mgr is None: raise HTTPException(status_code=503, detail="Run manager not available") return mgr def get_checkpointer(request: Request): """Return the global checkpointer, or 503.""" cp = getattr(request.app.state, "checkpointer", None) if cp is None: raise HTTPException(status_code=503, detail="Checkpointer not available") return cp def get_store(request: Request): """Return the global store (may be ``None`` if not configured).""" return getattr(request.app.state, "store", None) # --------------------------------------------------------------------------- # Auth helpers (used by authz.py) # --------------------------------------------------------------------------- # Cached singletons to avoid repeated instantiation per request _cached_local_provider: LocalAuthProvider | None = None _cached_repo: SQLiteUserRepository | None = None def get_local_provider() -> LocalAuthProvider: """Get or create the cached LocalAuthProvider singleton.""" global _cached_local_provider, _cached_repo if _cached_repo is None: from app.gateway.auth.repositories.sqlite import SQLiteUserRepository _cached_repo = SQLiteUserRepository() if _cached_local_provider is None: from app.gateway.auth.local_provider import LocalAuthProvider _cached_local_provider = LocalAuthProvider(repository=_cached_repo) return _cached_local_provider async def get_current_user_from_request(request: Request): """Get the current authenticated user from the request cookie. Raises HTTPException 401 if not authenticated. """ from app.gateway.auth import decode_token from app.gateway.auth.errors import AuthErrorCode, AuthErrorResponse, TokenError, token_error_to_code access_token = request.cookies.get("access_token") if not access_token: raise HTTPException( status_code=401, detail=AuthErrorResponse(code=AuthErrorCode.NOT_AUTHENTICATED, message="Not authenticated").model_dump(), ) payload = decode_token(access_token) if isinstance(payload, TokenError): raise HTTPException( status_code=401, detail=AuthErrorResponse(code=token_error_to_code(payload), message=f"Token error: {payload.value}").model_dump(), ) provider = get_local_provider() user = await provider.get_user(payload.sub) if user is None: raise HTTPException( status_code=401, detail=AuthErrorResponse(code=AuthErrorCode.USER_NOT_FOUND, message="User not found").model_dump(), ) # Token version mismatch → password was changed, token is stale if user.token_version != payload.ver: raise HTTPException( status_code=401, detail=AuthErrorResponse(code=AuthErrorCode.TOKEN_INVALID, message="Token revoked (password changed)").model_dump(), ) return user async def get_optional_user_from_request(request: Request): """Get optional authenticated user from request. Returns None if not authenticated. """ try: return await get_current_user_from_request(request) except HTTPException: return None # --------------------------------------------------------------------------- # Runtime bootstrap # --------------------------------------------------------------------------- @asynccontextmanager async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]: """Bootstrap and tear down all LangGraph runtime singletons. Usage in ``app.py``:: async with langgraph_runtime(app): yield """ from deerflow.agents.checkpointer.async_provider import make_checkpointer from deerflow.runtime import make_store, make_stream_bridge async with AsyncExitStack() as stack: app.state.stream_bridge = await stack.enter_async_context(make_stream_bridge()) app.state.checkpointer = await stack.enter_async_context(make_checkpointer()) app.state.store = await stack.enter_async_context(make_store()) app.state.run_manager = RunManager() yield