Source code for qfa.api.dependencies

"""FastAPI dependency functions for authentication and service injection."""

from collections.abc import AsyncIterator
from typing import Callable
from uuid import UUID

from fastapi import Depends, Request, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

from qfa.domain.errors import AuthenticationError, AuthorizationError
from qfa.domain.models import TenantApiKey
from qfa.domain.ports import UsageRepositoryPort
from qfa.domain.usage_models import CallContext, Operation
from qfa.services.analyze import AnalyzeService
from qfa.services.auth_orchestrator import AuthOrchestrator
from qfa.services.call_context import call_scope
from qfa.services.coding import CodingService
from qfa.services.sensitivity import SensitivityService
from qfa.services.summarize import SummarizeService


[docs] def get_sensitivity_service(request: Request) -> SensitivityService: """Return the sensitivity-detection service from app state. One provider per use-case service (ADR-017): the detect-sensitive handler depends on this service alone, not on the type that reaches every use case. Parameters ---------- request : Request The incoming HTTP request. Returns ------- SensitivityService The sensitivity-detection service instance. """ return request.app.state.sensitivity_service
[docs] def get_coding_service(request: Request) -> CodingService: """Return the coding service from app state. One provider per use-case service, per ADR-017: the assign-codes route annotates against :class:`~qfa.services.coding.CodingService` alone, so its dependency surface is readable from the handler signature. Parameters ---------- request : Request The incoming HTTP request. Returns ------- CodingService The coding service instance. """ return request.app.state.coding_service
[docs] def get_analyze_service(request: Request) -> AnalyzeService: """Return the analyze service from app state. One provider per use-case service (ADR-017): the analyze route depends on this rather than on a shared service, so its type signature names exactly the service it uses. Parameters ---------- request : Request The incoming HTTP request. Returns ------- AnalyzeService The analyze service instance. """ return request.app.state.analyze_service
[docs] def get_summarize_service(request: Request) -> SummarizeService: """Return the summarisation service from app state. One provider per use-case service, so each route handler annotates against the single service it actually uses (ADR-017). Parameters ---------- request : Request The incoming HTTP request. Returns ------- SummarizeService The summarisation service instance. """ return request.app.state.summarize_service
[docs] def get_auth_orchestrator(request: Request) -> AuthOrchestrator: """Return the auth orchestrator from app state. Parameters ---------- request : Request The incoming HTTP request. Returns ------- AuthOrchestrator The auth orchestrator service instance. """ return request.app.state.auth_orchestrator
[docs] def get_usage_repo(request: Request) -> UsageRepositoryPort: """Return the usage repository from app state. Parameters ---------- request : Request The incoming HTTP request. Returns ------- UsageRepositoryPort The usage repository instance. """ return request.app.state.usage_repo
[docs] async def authenticate_request( request: Request, credentials: HTTPAuthorizationCredentials = Security(HTTPBearer(auto_error=False)), ) -> TenantApiKey: """Validate a Bearer token from the Authorization header. Parameters ---------- request : Request The incoming HTTP request. credentials : HTTPAuthorizationCredentials The parsed Authorization header credentials. Returns ------- TenantApiKey The authenticated tenant API key. Raises ------ AuthenticationError If the credentials are missing or invalid. """ error_message = ( "A valid API key is required. Provide it as: Authorization: Bearer <key>" ) if credentials is None: raise AuthenticationError(error_message) try: return await request.app.state.auth_orchestrator.validate_api_key( credentials.credentials ) except AuthenticationError: raise AuthenticationError(error_message)
[docs] def call_scope_for( operation: Operation, ) -> Callable[..., AsyncIterator[CallContext]]: """Build a FastAPI dependency that enters ``call_scope`` for ``operation``. The returned dependency reads the authenticated tenant from :func:`authenticate_request`, enters ``call_scope`` for the duration of the request, and yields the resulting ``CallContext``. It is the *driving adapter*'s contribution to the cross-adapter correlation bridge: the route declares which operation it represents, and the dependency arranges for ``current_call_context`` to be set before the route body (and the application service beneath it) runs. This is then used for anything that requires the call context, such at usage tracking in :class:`~TrackingLLMAdapter`. Use inline at the route, e.g. ``Depends(call_scope_for(Operation.ANALYZE))``. FastAPI evaluates the default value once at module-load time (when the route function is defined), so there's no per-request cost to inlining. Parameters ---------- operation : Operation The public use-case operation this dependency represents. Returns ------- Callable[..., AsyncIterator[CallContext]] A FastAPI dependency suitable for ``Depends(...)``. """ async def _scope( request: Request, tenant: TenantApiKey = Depends(authenticate_request), ) -> AsyncIterator[CallContext]: async with call_scope( tenant_id=tenant.tenant_id, operation=operation, request_id=UUID(request.state.request_id), ) as ctx: yield ctx return _scope
[docs] def require_superuser( tenant: TenantApiKey = Depends(authenticate_request), ) -> TenantApiKey: """FastAPI dependency that authenticates and checks superuser status. Parameters ---------- tenant : TenantApiKey The authenticated tenant (injected by ``authenticate_request``). Returns ------- TenantApiKey The authenticated superuser tenant. Raises ------ AuthorizationError If the tenant is not a superuser. """ if not tenant.is_superuser: raise AuthorizationError("Superuser access required") return tenant