|
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, |
32 | | - create_model, |
| 34 | + field_validator, |
33 | 35 | model_validator, |
34 | 36 | ) |
35 | 37 | from pydantic_settings import BaseSettings, SettingsConfigDict |
36 | 38 | from signurlarity.aio.client import AsyncClient |
37 | 39 | from signurlarity.exceptions import SignurlarityError |
38 | 40 |
|
| 41 | +from .config.sources import ConfigSourceUrl |
| 42 | +from .extensions import DiracEntryPoint, select_from_extension |
39 | 43 | from .properties import SecurityProperty |
40 | 44 | from .s3 import s3_bucket_exists |
| 45 | +from .utils import dotenv_files_from_environment |
41 | 46 |
|
42 | 47 | T = TypeVar("T") |
43 | 48 |
|
@@ -353,81 +358,100 @@ def s3_client(self) -> AsyncClient: |
353 | 358 | return self._client |
354 | 359 |
|
355 | 360 |
|
356 | | -# def make_factory_settings(model_cls: type[StaticFactorySettings]): |
357 | | -# new_fields = {} |
358 | | -# for var, val in os.environ.items(): |
359 | | -# if var.startswith("XDG_"): |
360 | | -# new_fields[var] = (str, val) |
361 | | -# return create_model("FactorySettings", __base__=model_cls, **new_fields) |
362 | | - |
363 | | - |
364 | | -# FactorySettings = make_factory_settings(StaticFactorySettings) |
365 | | - |
366 | | - |
367 | | -from pydantic import Field |
368 | | - |
369 | | -# Could come from code, YAML, DB, plugin system, etc. |
370 | | -FIELD_SPECS = { |
371 | | - "state_key": { |
372 | | - "type": str, |
373 | | - "default": ..., |
374 | | - "env": "DIRACX_SERVICE_AUTH_STATE_KEY", |
375 | | - "description": "Fernet key used to encrypt/decrypt OAuth2 state.", |
376 | | - }, |
377 | | - "token_issuer": { |
378 | | - "type": str, |
379 | | - "default": ..., |
380 | | - "env": "DIRACX_SERVICE_AUTH_TOKEN_ISSUER", |
381 | | - "description": "JWT issuer value ('iss' claim).", |
382 | | - }, |
383 | | - "access_token_expire_minutes": { |
384 | | - "type": int, |
385 | | - "default": 20, |
386 | | - "env": "DIRACX_SERVICE_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES", |
387 | | - "description": "Access token lifetime in minutes.", |
388 | | - }, |
389 | | -} |
| 361 | +def _parse_env_bool(value: str) -> bool: |
| 362 | + """Parse a boolean environment variable value.""" |
| 363 | + return TypeAdapter(bool).validate_python(value) |
390 | 364 |
|
391 | | -from .extensions import DiracEntryPoint, select_from_extension |
392 | 365 |
|
| 366 | +class FactorySettings(ServiceSettingsBase): |
| 367 | + """Factory settings. |
393 | 368 |
|
394 | | -def _build_factory_settings_model() -> type[ServiceSettingsBase]: |
395 | | - class _BaseFactorySettings(ServiceSettingsBase): |
396 | | - """Factory settings. |
397 | | -
|
398 | | - Settings which do not fit into dedicated classes, |
399 | | - or are dynamically generated |
400 | | - """ |
401 | | - |
402 | | - diracx_legacy_exchange_hashed_api_key: str = "" |
403 | | - enabled_services: dict[str, bool] |
404 | | - |
405 | | - fields: dict[str, tuple[Any, Any]] = {} |
406 | | - |
407 | | - for entry_point in select_from_extension(group=DiracEntryPoint.SERVICES): |
408 | | - if "well-known" in entry_point.name: |
409 | | - continue |
410 | | - fields[f"diracx_service_{entry_point.name}_enabled"] = ( |
411 | | - bool, |
412 | | - Field( |
413 | | - default=True, |
414 | | - description=f"Enable the {entry_point.name.upper()} router", |
415 | | - ), |
416 | | - ) |
417 | | - |
418 | | - new_mod = create_model( |
419 | | - "FactorySettings", |
420 | | - __doc__=_BaseFactorySettings.__doc__, |
421 | | - __base__=_BaseFactorySettings, |
422 | | - # __config__=SettingsConfigDict( |
423 | | - # frozen=True, |
424 | | - # extra="ignore", |
425 | | - # use_attribute_docstrings=True, |
426 | | - # ), |
427 | | - **fields, |
428 | | - ) |
| 369 | + Settings which do not fit into dedicated classes, |
| 370 | + or are dynamically generated. |
| 371 | + """ |
429 | 372 |
|
430 | | - return new_mod |
| 373 | + model_config = SettingsConfigDict(use_attribute_docstrings=True) |
| 374 | + |
| 375 | + diracx_config_backend_url: ConfigSourceUrl | None = None |
| 376 | + """The URL of the configuration backend. |
| 377 | + """ |
| 378 | + |
| 379 | + diracx_legacy_exchange_hashed_api_key: str = "" |
| 380 | + """The hashed API key for the legacy exchange endpoint. |
| 381 | + """ |
431 | 382 |
|
| 383 | + diracx_tasks_redis_url: str = "redis://localhost" |
| 384 | + """The url for the redis server to manage tasks""" |
432 | 385 |
|
433 | | -FactorySettings = _build_factory_settings_model() |
| 386 | + enabled_services: dict[str, bool] = Field(default_factory=dict) |
| 387 | + """The following environment variables dictates which routers are enabled.""" |
| 388 | + |
| 389 | + opensearch_dbs: dict[str, str] = Field(default_factory=dict) |
| 390 | + """The following environment variables configure the OpenSearch database connections.""" |
| 391 | + |
| 392 | + sql_dbs: dict[str, str] = Field(default_factory=dict) |
| 393 | + """The following environment variables configure the SQL database connections.""" |
| 394 | + |
| 395 | + @model_validator(mode="before") |
| 396 | + @classmethod |
| 397 | + def load_dotenv_files(cls, data: Any) -> Any: |
| 398 | + """Load dotenv files before reading settings from environment.""" |
| 399 | + for env_file in dotenv_files_from_environment("DIRACX_SERVICE_DOTENV"): |
| 400 | + if not dotenv.load_dotenv(env_file): |
| 401 | + raise NotImplementedError(f"Could not load dotenv file {env_file}") |
| 402 | + return data |
| 403 | + |
| 404 | + @field_validator("enabled_services", mode="before") |
| 405 | + @classmethod |
| 406 | + def build_enabled_services(cls, value: Any) -> dict[str, bool]: |
| 407 | + """Build enabled services from the installed service entry points.""" |
| 408 | + enabled_services: dict[str, bool] = { |
| 409 | + entry_point.name: True |
| 410 | + for entry_point in select_from_extension(group=DiracEntryPoint.SERVICES) |
| 411 | + if "well-known" not in entry_point.name |
| 412 | + } |
| 413 | + |
| 414 | + for service_name in enabled_services: |
| 415 | + env_name = f"DIRACX_SERVICE_{service_name.upper()}_ENABLED" |
| 416 | + if env_value := os.environ.get(env_name): |
| 417 | + enabled_services[service_name] = _parse_env_bool(env_value) |
| 418 | + |
| 419 | + if isinstance(value, dict): |
| 420 | + enabled_services.update(value) |
| 421 | + return enabled_services |
| 422 | + |
| 423 | + @field_validator("opensearch_dbs", mode="before") |
| 424 | + @classmethod |
| 425 | + def build_opensearch_dbs(cls, value: Any) -> dict[str, str]: |
| 426 | + """Build OpenSearch database URLs from the installed entry points.""" |
| 427 | + opensearch_dbs: dict[str, str] = { |
| 428 | + entry_point.name: "" |
| 429 | + for entry_point in select_from_extension(group=DiracEntryPoint.OS_DB) |
| 430 | + } |
| 431 | + |
| 432 | + for db_name in opensearch_dbs: |
| 433 | + env_name = f"DIRACX_OS_DB_{db_name.upper()}" |
| 434 | + if env_value := os.environ.get(env_name): |
| 435 | + opensearch_dbs[db_name] = env_value |
| 436 | + |
| 437 | + if isinstance(value, dict): |
| 438 | + opensearch_dbs.update(value) |
| 439 | + return opensearch_dbs |
| 440 | + |
| 441 | + @field_validator("sql_dbs", mode="before") |
| 442 | + @classmethod |
| 443 | + def build_sql_dbs(cls, value: Any) -> dict[str, str]: |
| 444 | + """Build SQL database URLs from the installed entry points.""" |
| 445 | + sql_dbs: dict[str, str] = { |
| 446 | + entry_point.name: "" |
| 447 | + for entry_point in select_from_extension(group=DiracEntryPoint.SQL_DB) |
| 448 | + } |
| 449 | + |
| 450 | + for db_name in sql_dbs: |
| 451 | + env_name = f"DIRACX_DB_URL_{db_name.upper()}" |
| 452 | + if env_value := os.environ.get(env_name): |
| 453 | + sql_dbs[db_name] = env_value |
| 454 | + |
| 455 | + if isinstance(value, dict): |
| 456 | + sql_dbs.update(value) |
| 457 | + return sql_dbs |
0 commit comments