|
14 | 14 |
|
15 | 15 | import contextlib |
16 | 16 | import json |
| 17 | +import os |
17 | 18 | from collections.abc import AsyncIterator |
18 | 19 | from pathlib import Path |
19 | 20 | from typing import Annotated, Any, Self, TypeVar, cast |
20 | 21 |
|
| 22 | +import dotenv |
21 | 23 | from cryptography.fernet import Fernet |
22 | 24 | from joserfc.jwk import KeySet, KeySetSerialization |
23 | 25 | from pydantic import ( |
|
29 | 31 | SecretStr, |
30 | 32 | TypeAdapter, |
31 | 33 | UrlConstraints, |
| 34 | + field_validator, |
32 | 35 | model_validator, |
33 | 36 | ) |
34 | 37 | from pydantic_settings import BaseSettings, SettingsConfigDict |
35 | 38 | from signurlarity.aio.client import AsyncClient |
36 | 39 | from signurlarity.exceptions import SignurlarityError |
37 | 40 |
|
| 41 | +from .config.sources import ConfigSourceUrl |
| 42 | +from .extensions import DiracEntryPoint, select_from_extension |
38 | 43 | from .properties import SecurityProperty |
39 | 44 | from .s3 import s3_bucket_exists |
| 45 | +from .utils import dotenv_files_from_environment |
40 | 46 |
|
41 | 47 | T = TypeVar("T") |
42 | 48 |
|
@@ -350,3 +356,111 @@ def s3_client(self) -> AsyncClient: |
350 | 356 | if self._client is None: |
351 | 357 | raise RuntimeError("S3 client accessed before lifetime function") |
352 | 358 | return self._client |
| 359 | + |
| 360 | + |
| 361 | +class FactorySettings(ServiceSettingsBase): |
| 362 | + """Factory settings. |
| 363 | +
|
| 364 | + Settings which do not fit into dedicated classes, |
| 365 | + or are dynamically generated. |
| 366 | + """ |
| 367 | + |
| 368 | + # We want to be able to read both from specific environment variables |
| 369 | + # but also to create the object directly with the attribute name |
| 370 | + # https://pydantic.dev/docs/validation/latest/concepts/alias#validation |
| 371 | + model_config = SettingsConfigDict( |
| 372 | + use_attribute_docstrings=True, validate_by_alias=True, validate_by_name=True |
| 373 | + ) |
| 374 | + |
| 375 | + config_backend_url: ConfigSourceUrl | None = Field( |
| 376 | + default=None, |
| 377 | + validation_alias="DIRACX_CONFIG_BACKEND_URL", |
| 378 | + ) |
| 379 | + """The URL of the configuration backend. |
| 380 | + """ |
| 381 | + |
| 382 | + legacy_exchange_hashed_api_key: str = Field( |
| 383 | + default="", validation_alias="DIRACX_LEGACY_EXCHANGE_HASHED_API_KEY" |
| 384 | + ) |
| 385 | + """The hashed API key for the legacy exchange endpoint. |
| 386 | + """ |
| 387 | + |
| 388 | + tasks_redis_url: str = Field( |
| 389 | + default="redis://localhost", validation_alias="DIRACX_TASKS_REDIS_URL" |
| 390 | + ) |
| 391 | + """The url for the redis server to manage tasks""" |
| 392 | + |
| 393 | + enabled_services: dict[str, bool] = Field(default_factory=dict) |
| 394 | + """The following environment variables dictates which routers are enabled.""" |
| 395 | + |
| 396 | + opensearch_dbs: dict[str, str] = Field(default_factory=dict) |
| 397 | + """The following environment variables configure the OpenSearch database connections.""" |
| 398 | + |
| 399 | + sql_dbs: dict[str, str] = Field(default_factory=dict) |
| 400 | + """The following environment variables configure the SQL database connections.""" |
| 401 | + |
| 402 | + @model_validator(mode="before") |
| 403 | + @classmethod |
| 404 | + def load_dotenv_files(cls, data: Any) -> Any: |
| 405 | + """Load dotenv files before reading settings from environment.""" |
| 406 | + for env_file in dotenv_files_from_environment("DIRACX_SERVICE_DOTENV"): |
| 407 | + if not dotenv.load_dotenv(env_file): |
| 408 | + raise NotImplementedError(f"Could not load dotenv file {env_file}") |
| 409 | + return data |
| 410 | + |
| 411 | + @field_validator("enabled_services", mode="before") |
| 412 | + @classmethod |
| 413 | + def build_enabled_services(cls, value: Any) -> dict[str, bool]: |
| 414 | + """Build enabled services from the installed service entry points.""" |
| 415 | + enabled_services: dict[str, bool] = { |
| 416 | + entry_point.name: True |
| 417 | + for entry_point in select_from_extension(group=DiracEntryPoint.SERVICES) |
| 418 | + if "well-known" not in entry_point.name |
| 419 | + } |
| 420 | + |
| 421 | + for service_name in enabled_services: |
| 422 | + env_name = f"DIRACX_SERVICE_{service_name.upper()}_ENABLED" |
| 423 | + if env_value := os.environ.get(env_name): |
| 424 | + enabled_services[service_name] = TypeAdapter(bool).validate_python( |
| 425 | + env_value |
| 426 | + ) |
| 427 | + |
| 428 | + if isinstance(value, dict): |
| 429 | + enabled_services.update(value) |
| 430 | + return enabled_services |
| 431 | + |
| 432 | + @field_validator("opensearch_dbs", mode="before") |
| 433 | + @classmethod |
| 434 | + def build_opensearch_dbs(cls, value: Any) -> dict[str, str]: |
| 435 | + """Build OpenSearch database URLs from the installed entry points.""" |
| 436 | + opensearch_dbs: dict[str, str] = { |
| 437 | + entry_point.name: "" |
| 438 | + for entry_point in select_from_extension(group=DiracEntryPoint.OS_DB) |
| 439 | + } |
| 440 | + |
| 441 | + for db_name in opensearch_dbs: |
| 442 | + env_name = f"DIRACX_OS_DB_{db_name.upper()}" |
| 443 | + if env_value := os.environ.get(env_name): |
| 444 | + opensearch_dbs[db_name] = env_value |
| 445 | + |
| 446 | + if isinstance(value, dict): |
| 447 | + opensearch_dbs.update(value) |
| 448 | + return opensearch_dbs |
| 449 | + |
| 450 | + @field_validator("sql_dbs", mode="before") |
| 451 | + @classmethod |
| 452 | + def build_sql_dbs(cls, value: Any) -> dict[str, str]: |
| 453 | + """Build SQL database URLs from the installed entry points.""" |
| 454 | + sql_dbs: dict[str, str] = { |
| 455 | + entry_point.name: "" |
| 456 | + for entry_point in select_from_extension(group=DiracEntryPoint.SQL_DB) |
| 457 | + } |
| 458 | + |
| 459 | + for db_name in sql_dbs: |
| 460 | + env_name = f"DIRACX_DB_URL_{db_name.upper()}" |
| 461 | + if env_value := os.environ.get(env_name): |
| 462 | + sql_dbs[db_name] = env_value |
| 463 | + |
| 464 | + if isinstance(value, dict): |
| 465 | + sql_dbs.update(value) |
| 466 | + return sql_dbs |
0 commit comments