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