inheritance.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. # -*- coding: utf-8 -*-
  2. """
  3. Konfigurationsvererbung für mehrschichtige Konfigurationen.
  4. Ermöglicht das Laden von Basis-Konfigurationen mit
  5. Environment-spezifischen Overrides.
  6. """
  7. from __future__ import annotations
  8. import json
  9. from dataclasses import dataclass, field
  10. from pathlib import Path
  11. from typing import Any, Sequence
  12. @dataclass
  13. class ConfigLayer:
  14. """
  15. Eine Schicht in der Konfigurationshierarchie.
  16. """
  17. name: str
  18. """Name der Schicht (z.B. 'base', 'production')."""
  19. data: dict[str, Any]
  20. """Konfigurationsdaten dieser Schicht."""
  21. source: str = ""
  22. """Quelle der Konfiguration (z.B. Dateipfad)."""
  23. priority: int = 0
  24. """Priorität (höher = überschreibt niedrigere)."""
  25. def deep_merge(
  26. base: dict[str, Any],
  27. override: dict[str, Any],
  28. merge_lists: bool = False,
  29. ) -> dict[str, Any]:
  30. """
  31. Führt zwei Dictionaries tief zusammen.
  32. Args:
  33. base: Basis-Dictionary.
  34. override: Override-Dictionary.
  35. merge_lists: Ob Listen zusammengeführt statt ersetzt werden.
  36. Returns:
  37. Zusammengeführtes Dictionary.
  38. """
  39. result = dict(base)
  40. for key, value in override.items():
  41. if key in result:
  42. base_value = result[key]
  43. # Rekursives Mergen für verschachtelte Dicts
  44. if isinstance(base_value, dict) and isinstance(value, dict):
  45. result[key] = deep_merge(base_value, value, merge_lists)
  46. # Optionales Listen-Merging
  47. elif merge_lists and isinstance(base_value, list) and isinstance(value, list):
  48. result[key] = base_value + value
  49. # Sonst ersetzen
  50. else:
  51. result[key] = value
  52. else:
  53. result[key] = value
  54. return result
  55. class ConfigInheritance:
  56. """
  57. Verwaltet mehrschichtige Konfigurationen mit Vererbung.
  58. Lädt Konfigurationen in der Reihenfolge:
  59. base.json → {environment}.json → local.json
  60. Spätere Schichten überschreiben frühere.
  61. Example:
  62. inheritance = ConfigInheritance("config/")
  63. config = inheritance.load(environment="production")
  64. # Lädt:
  65. # 1. config/base.json
  66. # 2. config/production.json
  67. # 3. config/local.json (falls vorhanden)
  68. """
  69. DEFAULT_LAYERS = ["base", "local"]
  70. def __init__(
  71. self,
  72. config_dir: str | Path,
  73. extension: str = ".json",
  74. merge_lists: bool = False,
  75. ) -> None:
  76. """
  77. Initialisiert die Konfigurationsvererbung.
  78. Args:
  79. config_dir: Verzeichnis mit Konfigurationsdateien.
  80. extension: Dateiendung (z.B. '.json', '.yaml').
  81. merge_lists: Ob Listen zusammengeführt werden.
  82. """
  83. self._config_dir = Path(config_dir)
  84. self._extension = extension
  85. self._merge_lists = merge_lists
  86. self._layers: list[ConfigLayer] = []
  87. self._merged: dict[str, Any] | None = None
  88. @property
  89. def layers(self) -> list[ConfigLayer]:
  90. """Gibt die geladenen Schichten zurück."""
  91. return list(self._layers)
  92. @property
  93. def merged_config(self) -> dict[str, Any]:
  94. """Gibt die zusammengeführte Konfiguration zurück."""
  95. if self._merged is None:
  96. self._merged = self._merge_all()
  97. return dict(self._merged)
  98. def _load_file(self, path: Path) -> dict[str, Any]:
  99. """Lädt eine Konfigurationsdatei."""
  100. if not path.exists():
  101. return {}
  102. with open(path, "r", encoding="utf-8") as f:
  103. if self._extension in [".json", ".jsn"]:
  104. return json.load(f)
  105. elif self._extension in [".yaml", ".yml"]:
  106. try:
  107. import yaml
  108. return yaml.safe_load(f) or {}
  109. except ImportError:
  110. raise ImportError(
  111. "PyYAML ist erforderlich für YAML-Dateien"
  112. )
  113. else:
  114. # Versuche JSON als Fallback
  115. return json.load(f)
  116. def _get_config_path(self, name: str) -> Path:
  117. """Erstellt den Pfad zu einer Konfigurationsdatei."""
  118. return self._config_dir / f"{name}{self._extension}"
  119. def add_layer(
  120. self,
  121. name: str,
  122. data: dict[str, Any] | None = None,
  123. source: str = "",
  124. priority: int | None = None,
  125. ) -> "ConfigInheritance":
  126. """
  127. Fügt eine Konfigurationsschicht hinzu.
  128. Args:
  129. name: Name der Schicht.
  130. data: Konfigurationsdaten (oder None zum Laden aus Datei).
  131. source: Quelle der Konfiguration.
  132. priority: Priorität (Default: nächsthöher).
  133. Returns:
  134. Self für Method-Chaining.
  135. """
  136. if data is None:
  137. path = self._get_config_path(name)
  138. data = self._load_file(path)
  139. source = str(path)
  140. if priority is None:
  141. priority = len(self._layers)
  142. self._layers.append(ConfigLayer(
  143. name=name,
  144. data=data,
  145. source=source,
  146. priority=priority,
  147. ))
  148. self._merged = None # Cache invalidieren
  149. return self
  150. def load(
  151. self,
  152. environment: str | None = None,
  153. extra_layers: Sequence[str] | None = None,
  154. ) -> dict[str, Any]:
  155. """
  156. Lädt und mergt Konfigurationen.
  157. Args:
  158. environment: Optionale Umgebung (z.B. 'production').
  159. extra_layers: Zusätzliche Schichten.
  160. Returns:
  161. Zusammengeführte Konfiguration.
  162. """
  163. self._layers.clear()
  164. # Base laden
  165. self.add_layer("base")
  166. # Environment laden
  167. if environment:
  168. self.add_layer(environment)
  169. # Extra-Schichten
  170. if extra_layers:
  171. for layer in extra_layers:
  172. self.add_layer(layer)
  173. # Local (optional, überschreibt alles)
  174. local_path = self._get_config_path("local")
  175. if local_path.exists():
  176. self.add_layer("local")
  177. return self.merged_config
  178. def _merge_all(self) -> dict[str, Any]:
  179. """Führt alle Schichten zusammen."""
  180. # Nach Priorität sortieren
  181. sorted_layers = sorted(self._layers, key=lambda l: l.priority)
  182. result: dict[str, Any] = {}
  183. for layer in sorted_layers:
  184. result = deep_merge(result, layer.data, self._merge_lists)
  185. return result
  186. def get_layer_value(
  187. self,
  188. key: str,
  189. layer_name: str | None = None,
  190. ) -> tuple[Any, str | None]:
  191. """
  192. Findet einen Wert und seine Quelle.
  193. Args:
  194. key: Konfigurationsschlüssel (z.B. 'network.host').
  195. layer_name: Optionaler spezifischer Layer.
  196. Returns:
  197. Tupel (Wert, Layer-Name).
  198. """
  199. keys = key.split(".")
  200. def get_nested(data: dict, keys: list[str]) -> Any:
  201. for k in keys:
  202. if isinstance(data, dict) and k in data:
  203. data = data[k]
  204. else:
  205. return None
  206. return data
  207. if layer_name:
  208. for layer in self._layers:
  209. if layer.name == layer_name:
  210. value = get_nested(layer.data, keys)
  211. if value is not None:
  212. return value, layer.name
  213. return None, None
  214. # Rückwärts suchen (höchste Priorität zuerst)
  215. sorted_layers = sorted(
  216. self._layers, key=lambda l: l.priority, reverse=True
  217. )
  218. for layer in sorted_layers:
  219. value = get_nested(layer.data, keys)
  220. if value is not None:
  221. return value, layer.name
  222. return None, None
  223. def get_overrides(self, key: str) -> list[tuple[str, Any]]:
  224. """
  225. Findet alle Overrides für einen Schlüssel.
  226. Args:
  227. key: Konfigurationsschlüssel.
  228. Returns:
  229. Liste von (Layer-Name, Wert) Tupeln.
  230. """
  231. keys = key.split(".")
  232. results: list[tuple[str, Any]] = []
  233. def get_nested(data: dict, keys: list[str]) -> Any:
  234. for k in keys:
  235. if isinstance(data, dict) and k in data:
  236. data = data[k]
  237. else:
  238. return None
  239. return data
  240. for layer in sorted(self._layers, key=lambda l: l.priority):
  241. value = get_nested(layer.data, keys)
  242. if value is not None:
  243. results.append((layer.name, value))
  244. return results
  245. def reload(self) -> dict[str, Any]:
  246. """
  247. Lädt alle Schichten neu.
  248. Returns:
  249. Neu gemergte Konfiguration.
  250. """
  251. for layer in self._layers:
  252. if layer.source:
  253. path = Path(layer.source)
  254. if path.exists():
  255. layer.data = self._load_file(path)
  256. self._merged = None
  257. return self.merged_config
  258. class EnvironmentConfig:
  259. """
  260. Environment-aware Konfigurationslader.
  261. Lädt automatisch die richtige Konfiguration basierend
  262. auf Umgebungsvariablen.
  263. """
  264. ENV_VAR = "TRIXY_ENV"
  265. DEFAULT_ENV = "development"
  266. def __init__(
  267. self,
  268. config_dir: str | Path,
  269. env_var: str | None = None,
  270. ) -> None:
  271. """
  272. Initialisiert den Environment-Konfigurationslader.
  273. Args:
  274. config_dir: Konfigurationsverzeichnis.
  275. env_var: Umgebungsvariable für Environment-Name.
  276. """
  277. self._config_dir = Path(config_dir)
  278. self._env_var = env_var or self.ENV_VAR
  279. self._inheritance = ConfigInheritance(config_dir)
  280. @property
  281. def environment(self) -> str:
  282. """Gibt das aktuelle Environment zurück."""
  283. import os
  284. return os.environ.get(self._env_var, self.DEFAULT_ENV)
  285. def load(
  286. self,
  287. environment: str | None = None,
  288. ) -> dict[str, Any]:
  289. """
  290. Lädt die Konfiguration für ein Environment.
  291. Args:
  292. environment: Optionales Environment (Default: aus Umgebungsvariable).
  293. Returns:
  294. Zusammengeführte Konfiguration.
  295. """
  296. env = environment or self.environment
  297. return self._inheritance.load(environment=env)
  298. def get(self, key: str, default: Any = None) -> Any:
  299. """
  300. Holt einen Konfigurationswert.
  301. Args:
  302. key: Konfigurationsschlüssel.
  303. default: Standardwert.
  304. Returns:
  305. Konfigurationswert oder Default.
  306. """
  307. config = self._inheritance.merged_config
  308. keys = key.split(".")
  309. for k in keys:
  310. if isinstance(config, dict) and k in config:
  311. config = config[k]
  312. else:
  313. return default
  314. return config