Coverage for arclith / infrastructure / config.py: 96%
68 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 pathlib import Path
4from typing import Literal
6import yaml
7from pydantic import BaseModel, field_validator, model_validator
9_DUCKDB_SUPPORTED_EXTENSIONS = {".csv", ".parquet", ".json", ".arrow"}
12class MongoDBSettings(BaseModel):
13 uri: str | None = None
14 db_name: str
15 collection_name: str | None = None
18class DuckDBSettings(BaseModel):
19 path: str
21 @field_validator("path")
22 @classmethod
23 def must_be_supported_format(cls, v: str) -> str:
24 p = Path(v)
25 if p.is_dir() or v.endswith("/"):
26 return v
27 ext = p.suffix.lower()
28 if ext not in _DUCKDB_SUPPORTED_EXTENSIONS:
29 raise ValueError(
30 f"Format '{ext}' non supporté par DuckDB. "
31 f"Formats acceptés : {', '.join(sorted(_DUCKDB_SUPPORTED_EXTENSIONS))}"
32 )
33 return v
36class SoftDeleteSettings(BaseModel):
37 retention_days: float | None = None
39 @field_validator("retention_days")
40 @classmethod
41 def must_be_positive(cls, v: float | None) -> float | None:
42 if v is not None and v < 0:
43 raise ValueError("retention_days doit être >= 0")
44 return v
47class AdaptersSettings(BaseModel):
48 logger: Literal["console"] = "console"
49 repository: Literal["memory", "mongodb", "duckdb"] = "memory"
50 multitenant: bool = False
51 mongodb: MongoDBSettings | None = None
52 duckdb: DuckDBSettings | None = None
54 @model_validator(mode = "after")
55 def validate_repository_config(self) -> "AdaptersSettings":
56 if self.repository == "mongodb":
57 if self.mongodb is None:
58 raise ValueError("repository=mongodb mais aucune section [adapters.mongodb] dans config.yaml")
59 elif self.repository == "duckdb" and self.duckdb is None:
60 raise ValueError("repository=duckdb mais aucune section [adapters.duckdb] dans config.yaml")
61 return self
64class ApiSettings(BaseModel):
65 host: str = "0.0.0.0" # nosec B104
66 port: int = 8000
67 reload: bool = True
70class McpSettings(BaseModel):
71 host: str = "127.0.0.1"
72 port: int = 8001
75class AppConfig(BaseModel):
76 adapters: AdaptersSettings = AdaptersSettings()
77 soft_delete: SoftDeleteSettings = SoftDeleteSettings()
78 api: ApiSettings = ApiSettings()
79 mcp: McpSettings = McpSettings()
82def load_config(path: Path) -> AppConfig:
83 with open(path) as f:
84 data = yaml.safe_load(f)
85 data = data or {}
87 from arclith.infrastructure.secret_factory import build_secret_resolver
88 from arclith.infrastructure.secret_loader import resolve_dict_secrets
89 from contextlib import suppress
91 resolver = build_secret_resolver(data, path.parent)
92 if resolver: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 with suppress(Exception):
94 data = resolve_dict_secrets(data, resolver)
96 return AppConfig.model_validate(data)