Dependency injection
DiracX uses FastAPI's dependency injection system to provide dependencies to API route handlers. Dependencies are injected as function parameters using Python's type hints.
DB classes, OS DB classes, and ServiceSettingsBase subclasses are auto-detected by auto_inject_depends in diracx.tasks.plumbing.depends. This means you can simply type-annotate parameters with the bare class and the framework wraps them with the appropriate Depends call automatically. This auto-detection is applied:
- In routers: by
DiracxRouter.add_api_route - In tasks: by
wrap_taskindiracx.tasks.plumbing.factory - In sub-dependency functions: via the
@auto_injectdecorator
Available dependencies
Database connections
Database connections are automatically managed through dependency injection with automatic transaction handling. Import DB classes directly from their defining packages:
from diracx.db.sql import JobDB, JobLoggingDB
@router.get("/jobs/{job_id}")
async def get_job(
job_id: int,
job_db: JobDB,
job_logging_db: JobLoggingDB,
) -> JobInfo:
# Database connections are automatically managed
job_info = await job_db.get_job_info(job_id)
return job_info
Available database dependencies:
| Class | Package | Description |
|---|---|---|
JobDB |
diracx.db.sql |
Job management database |
AuthDB |
diracx.db.sql |
Authentication database |
JobLoggingDB |
diracx.db.sql |
Job logging database |
PilotAgentsDB |
diracx.db.sql |
Pilot agents database |
SandboxMetadataDB |
diracx.db.sql |
Sandbox metadata database |
TaskQueueDB |
diracx.db.sql |
Task queue database |
JobParametersDB |
diracx.db.os |
Job parameters (OpenSearch) database |
Connection Pool Management
Database connection pools are managed through FastAPI's lifetime functions and context managers:
SQL Databases:
- Engine Initialization: At application startup, each SQL database's
engine_context()is added to the FastAPIlifetime_functions - Connection Pool: The
engine_contextcreates an async SQLAlchemy engine with connection pooling (pool_recycle=1800s) - Pool Lifecycle: The connection pool is created at startup and properly disposed of at shutdown
- Per-Request Connections: Individual connections are acquired from the pool for each request transaction
OpenSearch Databases:
- Client Initialization: Each OpenSearch database's
client_context()is added to the FastAPIlifetime_functions - Client Pool: The
client_contextcreates anAsyncOpenSearchclient with built-in connection pooling - Client Lifecycle: The client connection pool is established at startup and closed at shutdown
- Per-Request Sessions: Individual sessions reuse pooled connections for each request
The db_transaction() function in factory.py manages per-request connection acquisition and includes health checking via cached database pings.
Transaction Management
SQL database connections have automatic transaction handling:
- Connections are managed through a central pool
- Transactions are opened for the duration of each request
- Successful requests (HTTP status < 400) automatically commit the transaction
- Failed requests (HTTP status >= 400) automatically roll back the transaction
- Connections are returned to the pool for reuse
The auto-detection applies the following rules:
# SQL databases -> Depends(cls.transaction, scope="function")
# OpenSearch databases -> Depends(cls.session, scope="function")
# Settings classes -> Depends(cls.create)
For advanced scenarios requiring explicit transaction commits (e.g., revoking tokens before returning an error):
from diracx.db.sql import AuthDB
@router.post("/token")
async def token(auth_db: AuthDB):
if refresh_token_attributes["status"] == RefreshTokenStatus.REVOKED:
# Revoke all the user tokens associated with the subject
await auth_db.revoke_user_refresh_tokens(sub)
# Explicitly commit to ensure revocation is saved
await auth_db.conn.commit()
# Raise error after commit
raise HTTPException(status_code=401)
For cases where a database connection is needed without a transaction (e.g., a task that manages its own transactions in batches), use the NoTransaction marker:
from typing import Annotated
from diracx.db.sql import SandboxMetadataDB
from diracx.tasks.plumbing.depends import NoTransaction
async def execute(
self,
sandbox_metadata_db: Annotated[SandboxMetadataDB, NoTransaction()],
) -> int:
# Caller manages transactions manually
...
For more details on the underlying database classes, see the Database Components documentation.
Configuration and settings
Configuration and application settings are injected using dedicated dependencies. Settings classes that inherit from ServiceSettingsBase are auto-detected. Config must be imported from diracx.routers.dependencies:
from diracx.core.settings import AuthSettings
from diracx.routers.dependencies import Config
@router.get("/config-info")
async def get_config_info(
config: Config,
auth_settings: AuthSettings,
) -> dict:
return {
"vo": config.vo,
"token_issuer": auth_settings.token_issuer,
}
Available configuration dependencies:
| Class | Package | Description |
|---|---|---|
Config |
diracx.routers.dependencies |
DiracX configuration |
AuthSettings |
diracx.core.settings |
Authentication settings |
DevelopmentSettings |
diracx.core.settings |
Development-specific settings |
SandboxStoreSettings |
diracx.core.settings |
Sandbox storage settings |
Config is special because it doesn't inherit from ServiceSettingsBase and uses ConfigSource.create as its dependency factory. It is the only dependency that requires importing a pre-wrapped Annotated type.
For more details on configuration and settings classes, see the Configuration documentation.
User authentication and authorization
User information and authentication are handled through specialized dependencies:
from diracx.routers.utils.users import AuthorizedUserInfo, verify_dirac_access_token
from diracx.routers.auth.utils import has_properties
from diracx.core.properties import JOB_ADMINISTRATOR
@router.post("/admin-action")
async def admin_action(
user_info: Annotated[AuthorizedUserInfo, Depends(verify_dirac_access_token)],
_: Annotated[None, has_properties(JOB_ADMINISTRATOR)],
) -> dict:
return {"user": user_info.preferred_username, "vo": user_info.vo}
Authentication dependencies:
| Function | Module | Description |
|---|---|---|
verify_dirac_access_token |
diracx.routers.utils.users |
Verifies JWT tokens and returns user information |
has_properties(property) |
diracx.routers.auth.utils |
Checks if user has specific DIRAC properties |
These functions handle JWT token validation and property-based authorization checks.
Security properties
Security properties can be injected to determine what properties are available:
from diracx.routers.dependencies import AvailableSecurityProperties
@router.get("/available-properties")
async def get_properties(
properties: AvailableSecurityProperties,
) -> list[str]:
return [prop.value for prop in properties]
Access policies
Access policies provide fine-grained authorization control:
from diracx.db.sql import JobDB
from diracx.routers.jobs.access_policies import CheckWMSPolicyCallable, ActionType
@router.post("/jobs")
async def create_job(
job_definition: str,
job_db: JobDB,
check_permissions: CheckWMSPolicyCallable,
) -> dict:
# Check if user can create jobs
await check_permissions(action=ActionType.CREATE, job_db=job_db)
# Proceed with job creation
...
Creating custom dependencies
Settings dependencies
Custom settings classes that inherit from ServiceSettingsBase are auto-detected. Simply define the class and use it as a type annotation:
from diracx.core.settings import ServiceSettingsBase
class MyCustomSettings(ServiceSettingsBase):
custom_option: str = "default_value"
@classmethod
def create(cls):
return cls()
@router.get("/my-endpoint")
async def my_endpoint(settings: MyCustomSettings) -> dict:
return {"custom_option": settings.custom_option}
Database dependencies
Database dependencies are also auto-detected. Any BaseSQLDB subclass used as a type annotation will automatically get wrapped with Depends(cls.transaction, scope="function"):
from my_extension.db.sql import MyCustomDB
@router.get("/my-data")
async def get_data(db: MyCustomDB) -> dict:
...
Complete example
Here's a complete example showing multiple dependency types:
from diracx.core.settings import AuthSettings
from diracx.db.sql import JobDB
from diracx.routers.dependencies import Config
from diracx.routers.utils.users import AuthorizedUserInfo, verify_dirac_access_token
from diracx.routers.auth.utils import has_properties
from diracx.core.properties import NORMAL_USER
@router.post("/submit-job")
async def submit_job(
job_definition: str,
config: Config,
auth_settings: AuthSettings,
job_db: JobDB,
user_info: Annotated[AuthorizedUserInfo, Depends(verify_dirac_access_token)],
_: Annotated[None, has_properties(NORMAL_USER)],
) -> dict:
"""Submit a job with full dependency injection."""
# All dependencies are automatically injected and managed
job_id = await job_db.insert_job(job_definition, user_info.preferred_username)
return {
"job_id": job_id,
"submitted_by": user_info.preferred_username,
"vo": config.vo,
}
Dependency lifecycle
- SQL Database connections: Connection pooling with automatic transaction handling per request
- Transactions opened at request start
- Auto-commit on success (HTTP status < 400)
- Auto-rollback on failure (HTTP status >= 400)
- OpenSearch Database connections: Connection pooling without automatic transactions
- Settings: Instantiated once and reused across requests
- User authentication: JWT token validated on each request
- Configuration: Loaded once at startup and cached, with automatic refresh