Coverage for arclith / infrastructure / secret_loader.py: 100%
26 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 15:02 +0100
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 15:02 +0100
1from __future__ import annotations
3from copy import deepcopy
4from typing import TYPE_CHECKING
6if TYPE_CHECKING:
7 from arclith.domain.ports.secret_resolver import SecretResolver
10def resolve_dict_secrets(data: dict, resolver: "SecretResolver") -> dict:
11 """Inject resolved secrets into a raw config dict.
13 Operates on a deepcopy — the original dict is never mutated.
14 Works with any dict structure: AppConfig, AgentConfig, or any custom config.
16 :param data: raw config dict (from yaml.safe_load)
17 :param resolver: SecretResolver instance (Vault, Yaml, Env, Chain…)
18 :returns: new dict with secrets injected at their dot-notation paths
19 """
20 secrets: dict = data.get("secrets") or {}
21 mappings: dict[str, str] = secrets.get("mappings") or {}
22 if not mappings:
23 return data
25 result = deepcopy(data)
26 missing: list[str] = []
28 for field_path, secret_key in mappings.items():
29 value = resolver.get(field_path, secret_key)
30 if value is not None:
31 _set_nested(result, field_path, value)
32 else:
33 missing.append(field_path)
35 if missing:
36 raise RuntimeError(
37 f"Secrets non résolus pour les champs suivants : {missing}. "
38 "Vérifiez votre Vault, secrets.yaml ou variables d'environnement."
39 )
41 return result
44def _set_nested(data: dict, path: str, value: str) -> None:
45 keys = path.split(".")
46 current = data
47 for key in keys[:-1]:
48 if not isinstance(current.get(key), dict):
49 current[key] = {}
50 current = current[key]
51 current[keys[-1]] = value