Skip to content

Commit d54a46f

Browse files
committed
progress
1 parent 87746a3 commit d54a46f

18 files changed

Lines changed: 470 additions & 171 deletions

File tree

diracx-core/src/diracx/core/config/sources.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import asyncio
99
import logging
1010
import os
11+
from abc import ABCMeta, abstractmethod
1112
from datetime import datetime, timezone
1213
from pathlib import Path
1314
from tempfile import TemporaryDirectory
@@ -76,7 +77,12 @@ def __init_subclass__(cls) -> None:
7677

7778
@classmethod
7879
def create(cls):
79-
return cls.create_from_url(backend_url=os.environ["DIRACX_CONFIG_BACKEND_URL"])
80+
# Avoid circular import
81+
from diracx.core.settings import FactorySettings
82+
83+
return cls.create_from_url(
84+
backend_url=FactorySettings().diracx_config_backend_url
85+
)
8086

8187
@classmethod
8288
def create_from_url(

diracx-core/src/diracx/core/settings.py

Lines changed: 97 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414

1515
import contextlib
1616
import json
17+
import os
1718
from collections.abc import AsyncIterator
1819
from pathlib import Path
1920
from typing import Annotated, Any, Self, TypeVar, cast
2021

22+
import dotenv
2123
from cryptography.fernet import Fernet
2224
from joserfc.jwk import KeySet, KeySetSerialization
2325
from pydantic import (
@@ -29,15 +31,18 @@
2931
SecretStr,
3032
TypeAdapter,
3133
UrlConstraints,
32-
create_model,
34+
field_validator,
3335
model_validator,
3436
)
3537
from pydantic_settings import BaseSettings, SettingsConfigDict
3638
from signurlarity.aio.client import AsyncClient
3739
from signurlarity.exceptions import SignurlarityError
3840

41+
from .config.sources import ConfigSourceUrl
42+
from .extensions import DiracEntryPoint, select_from_extension
3943
from .properties import SecurityProperty
4044
from .s3 import s3_bucket_exists
45+
from .utils import dotenv_files_from_environment
4146

4247
T = TypeVar("T")
4348

@@ -353,81 +358,100 @@ def s3_client(self) -> AsyncClient:
353358
return self._client
354359

355360

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)
390364

391-
from .extensions import DiracEntryPoint, select_from_extension
392365

366+
class FactorySettings(ServiceSettingsBase):
367+
"""Factory settings.
393368
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+
"""
429372

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+
"""
431382

383+
diracx_tasks_redis_url: str = "redis://localhost"
384+
"""The url for the redis server to manage tasks"""
432385

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

diracx-db/src/diracx/db/os/utils.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import contextlib
44
import json
55
import logging
6-
import os
76
from abc import ABCMeta, abstractmethod
87
from collections.abc import AsyncIterator
98
from contextvars import ContextVar
@@ -14,6 +13,7 @@
1413

1514
from diracx.core.exceptions import InvalidQueryError
1615
from diracx.core.extensions import DiracEntryPoint, select_from_extension
16+
from diracx.core.settings import FactorySettings
1717
from diracx.db.exceptions import DBUnavailableError
1818

1919
logger = logging.getLogger(__name__)
@@ -38,7 +38,8 @@ class BaseOSDB(metaclass=ABCMeta):
3838
This method returns a dictionary of database names to connection parameters.
3939
The available databases are determined by the `diracx.dbs.os` entrypoint in
4040
the `pyproject.toml` file and the connection parameters are taken from the
41-
environment variables prefixed with `DIRACX_OS_DB_{DB_NAME}`.
41+
`opensearch_dbs` field in FactorySettings, which reads from environment variables
42+
prefixed with `DIRACX_OS_DB_{DB_NAME}`.
4243
4344
If extensions to DiracX are being used, there can be multiple implementations
4445
of the same database. To list the available implementations use
@@ -104,19 +105,26 @@ def available_implementations(cls, db_name: str) -> list[type[BaseOSDB]]:
104105
def available_urls(cls) -> dict[str, dict[str, Any]]:
105106
"""Return a dict of available OpenSearch database urls.
106107
107-
The list of available URLs is determined by environment variables
108+
The list of available URLs is determined by the opensearch_dbs field
109+
in FactorySettings, which reads from environment variables
108110
prefixed with ``DIRACX_OS_DB_{DB_NAME}``.
109111
"""
112+
factory_settings = FactorySettings()
113+
opensearch_dbs = factory_settings.opensearch_dbs
114+
110115
conn_kwargs: dict[str, dict[str, Any]] = {}
111116
for entry_point in select_from_extension(group=DiracEntryPoint.OS_DB):
112117
db_name = entry_point.name
113-
var_name = f"DIRACX_OS_DB_{entry_point.name.upper()}"
114-
if var_name in os.environ:
115-
try:
116-
conn_kwargs[db_name] = json.loads(os.environ[var_name])
117-
except Exception:
118-
logger.error("Error loading connection parameters for %s", db_name)
119-
raise
118+
# Get the field value from the OpenSearchDBSettings model
119+
if field_value := opensearch_dbs.get(db_name):
120+
if field_value:
121+
try:
122+
conn_kwargs[db_name] = json.loads(field_value)
123+
except Exception:
124+
logger.error(
125+
"Error loading connection parameters for %s", db_name
126+
)
127+
raise
120128
return conn_kwargs
121129

122130
@classmethod

diracx-db/src/diracx/db/sql/utils/base.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import contextlib
44
import logging
5-
import os
65
import re
76
from abc import ABCMeta
87
from collections.abc import AsyncIterator
@@ -53,8 +52,9 @@ class BaseSQLDB(metaclass=ABCMeta):
5352
The available databases are discovered by calling `BaseSQLDB.available_urls`.
5453
This method returns a mapping of database names to connection URLs. The
5554
available databases are determined by the `diracx.dbs.sql` entrypoint in the
56-
`pyproject.toml` file and the connection URLs are taken from the environment
57-
variables of the form `DIRACX_DB_URL_<db-name>`.
55+
`pyproject.toml` file and the connection URLs are taken from the
56+
`sql_dbs` field in FactorySettings, which reads from environment variables
57+
of the form `DIRACX_DB_URL_<db-name>`.
5858
5959
If extensions to DiracX are being used, there can be multiple implementations
6060
of the same database. To list the available implementations use
@@ -125,16 +125,21 @@ def available_implementations(cls, db_name: str) -> list[type["BaseSQLDB"]]:
125125
def available_urls(cls) -> dict[str, str]:
126126
"""Return a dict of available database urls.
127127
128-
The list of available URLs is determined by environment variables
128+
The list of available URLs is determined by the sql_dbs field
129+
in FactorySettings, which reads from environment variables
129130
prefixed with ``DIRACX_DB_URL_{DB_NAME}``.
130131
"""
132+
from diracx.core.settings import FactorySettings
133+
134+
factory_settings = FactorySettings()
135+
sql_dbs = factory_settings.sql_dbs
136+
131137
db_urls: dict[str, str] = {}
132138
for entry_point in select_from_extension(group=DiracEntryPoint.SQL_DB):
133139
db_name = entry_point.name
134-
var_name = f"DIRACX_DB_URL_{entry_point.name.upper()}"
135-
if var_name in os.environ:
140+
# Get the field value from the SqlDBSettings model
141+
if db_url := sql_dbs.get(db_name):
136142
try:
137-
db_url = os.environ[var_name]
138143
if db_url == "sqlite+aiosqlite:///:memory:":
139144
db_urls[db_name] = db_url
140145
# pydantic does not allow for underscore in scheme

diracx-logic/src/diracx/logic/__main__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,15 @@ async def delete_jwk(args):
9393
async def cleanup_authdb(args):
9494
"""Maintain AuthDB partitions and remove expired flows."""
9595
logger.info("Maintaining AuthDB partitions and removing expired flows")
96-
import os
9796

98-
from diracx.core.settings import AuthSettings
97+
from diracx.core.settings import AuthSettings, FactorySettings
9998
from diracx.db.sql import AuthDB
10099
from diracx.logic.auth.management import cleanup_expired_data
101100

102101
settings = AuthSettings()
103-
db_url = os.environ["DIRACX_DB_URL_AUTHDB"]
102+
factory_settings = FactorySettings()
103+
db_url = factory_settings.sql_dbs.AuthDB
104+
104105
db = AuthDB(db_url)
105106
async with db.engine_context():
106107
async with db:

diracx-routers/src/diracx/routers/auth/token.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import logging
6-
import os
76
from http import HTTPStatus
87
from typing import Annotated, Literal
98

@@ -21,7 +20,7 @@
2120
RefreshTokenPayload,
2221
TokenResponse,
2322
)
24-
from diracx.core.settings import AuthSettings
23+
from diracx.core.settings import AuthSettings, FactorySettings
2524
from diracx.db.sql import AuthDB
2625
from diracx.logic.auth import create_token
2726
from diracx.logic.auth import get_oidc_token as get_oidc_token_bl
@@ -182,6 +181,7 @@ async def perform_legacy_exchange(
182181
auth_db: AuthDB,
183182
available_properties: AvailableSecurityProperties,
184183
settings: AuthSettings,
184+
factory_settings: FactorySettings,
185185
config: Config,
186186
all_access_policies: Annotated[
187187
dict[str, BaseAccessPolicy], Depends(BaseAccessPolicy.all_used_access_policies)
@@ -193,9 +193,7 @@ async def perform_legacy_exchange(
193193
This route is disabled if DIRACX_LEGACY_EXCHANGE_HASHED_API_KEY is not set
194194
in the environment.
195195
"""
196-
if not (
197-
expected_api_key := os.environ.get("DIRACX_LEGACY_EXCHANGE_HASHED_API_KEY")
198-
):
196+
if not (expected_api_key := factory_settings.diracx_legacy_exchange_hashed_api_key):
199197
raise HTTPException(
200198
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
201199
detail="Legacy exchange is not enabled",

0 commit comments

Comments
 (0)