environment.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. # -*- coding: utf-8 -*-
  2. """
  3. Umgebungsvariablen-Substitution für Konfigurationen.
  4. Ermöglicht die Verwendung von ${VAR} und ${VAR:-default} Syntax
  5. in Konfigurationsdateien.
  6. """
  7. from __future__ import annotations
  8. import os
  9. import re
  10. from dataclasses import dataclass
  11. from typing import Any
  12. @dataclass
  13. class SubstitutionResult:
  14. """
  15. Ergebnis einer Substitution.
  16. Enthält den substituierten Wert und Metadaten.
  17. """
  18. original: str
  19. """Original-String vor Substitution."""
  20. result: str
  21. """Ergebnis nach Substitution."""
  22. substitutions: list[tuple[str, str, str]]
  23. """Liste von (Platzhalter, Variable, Wert) Tupeln."""
  24. unresolved: list[str]
  25. """Liste von Variablen, die nicht aufgelöst werden konnten."""
  26. @property
  27. def has_unresolved(self) -> bool:
  28. """Prüft, ob unaufgelöste Variablen existieren."""
  29. return len(self.unresolved) > 0
  30. @property
  31. def substitution_count(self) -> int:
  32. """Anzahl der durchgeführten Substitutionen."""
  33. return len(self.substitutions)
  34. class EnvironmentSubstitutor:
  35. """
  36. Substituiert Umgebungsvariablen in Strings und Datenstrukturen.
  37. Unterstützt folgende Syntax:
  38. - ${VAR} - Ersetzt durch Wert von VAR
  39. - ${VAR:-default} - Ersetzt durch Wert von VAR oder "default"
  40. - ${VAR:=default} - Setzt VAR auf "default" wenn nicht definiert
  41. - ${VAR:+value} - Ersetzt durch "value" nur wenn VAR definiert ist
  42. - ${VAR:?error} - Wirft Fehler wenn VAR nicht definiert ist
  43. Example:
  44. substitutor = EnvironmentSubstitutor()
  45. # Einfache Substitution
  46. result = substitutor.substitute("Host: ${DB_HOST:-localhost}")
  47. # Verschachtelte Strukturen
  48. config = {"host": "${DB_HOST}", "port": "${DB_PORT:-5432}"}
  49. result = substitutor.substitute_recursive(config)
  50. """
  51. # Regex-Pattern für Variablen-Syntax
  52. PATTERN = re.compile(
  53. r"\$\{"
  54. r"(?P<var>[A-Za-z_][A-Za-z0-9_]*)"
  55. r"(?:"
  56. r"(?P<op>:-|:=|:\+|:\?)"
  57. r"(?P<default>[^}]*)"
  58. r")?"
  59. r"\}"
  60. )
  61. def __init__(
  62. self,
  63. env: dict[str, str] | None = None,
  64. strict: bool = False,
  65. expand_nested: bool = True,
  66. ) -> None:
  67. """
  68. Initialisiert den Substitutor.
  69. Args:
  70. env: Optionales Environment-Dictionary (Default: os.environ).
  71. strict: Ob bei fehlenden Variablen ein Fehler geworfen wird.
  72. expand_nested: Ob Variablen in Default-Werten expandiert werden.
  73. """
  74. self._env = env if env is not None else dict(os.environ)
  75. self._strict = strict
  76. self._expand_nested = expand_nested
  77. def get_variable(self, name: str, default: str | None = None) -> str | None:
  78. """
  79. Holt eine Umgebungsvariable.
  80. Args:
  81. name: Variablenname.
  82. default: Standardwert falls nicht definiert.
  83. Returns:
  84. Variablenwert oder Default.
  85. """
  86. return self._env.get(name, default)
  87. def set_variable(self, name: str, value: str) -> None:
  88. """
  89. Setzt eine Umgebungsvariable.
  90. Args:
  91. name: Variablenname.
  92. value: Wert.
  93. """
  94. self._env[name] = value
  95. def _replace_match(
  96. self,
  97. match: re.Match,
  98. substitutions: list[tuple[str, str, str]],
  99. unresolved: list[str],
  100. ) -> str:
  101. """Ersetzt einen einzelnen Match."""
  102. var_name = match.group("var")
  103. operator = match.group("op")
  104. default_value = match.group("default") or ""
  105. placeholder = match.group(0)
  106. value = self._env.get(var_name)
  107. if operator is None:
  108. # ${VAR} - Einfache Substitution
  109. if value is not None:
  110. substitutions.append((placeholder, var_name, value))
  111. return value
  112. elif self._strict:
  113. raise ValueError(
  114. f"Umgebungsvariable '{var_name}' ist nicht definiert"
  115. )
  116. else:
  117. unresolved.append(var_name)
  118. return placeholder
  119. elif operator == ":-":
  120. # ${VAR:-default} - Default wenn nicht gesetzt oder leer
  121. if value:
  122. substitutions.append((placeholder, var_name, value))
  123. return value
  124. else:
  125. # Default-Wert ggf. auch expandieren
  126. if self._expand_nested:
  127. default_value = self.substitute(default_value)
  128. substitutions.append((placeholder, var_name, default_value))
  129. return default_value
  130. elif operator == ":=":
  131. # ${VAR:=default} - Setze Variable wenn nicht definiert
  132. if value:
  133. substitutions.append((placeholder, var_name, value))
  134. return value
  135. else:
  136. if self._expand_nested:
  137. default_value = self.substitute(default_value)
  138. self._env[var_name] = default_value
  139. substitutions.append((placeholder, var_name, default_value))
  140. return default_value
  141. elif operator == ":+":
  142. # ${VAR:+value} - Value nur wenn Variable definiert
  143. if value:
  144. if self._expand_nested:
  145. default_value = self.substitute(default_value)
  146. substitutions.append((placeholder, var_name, default_value))
  147. return default_value
  148. else:
  149. substitutions.append((placeholder, var_name, ""))
  150. return ""
  151. elif operator == ":?":
  152. # ${VAR:?error} - Fehler wenn nicht definiert
  153. if value:
  154. substitutions.append((placeholder, var_name, value))
  155. return value
  156. else:
  157. error_msg = default_value or f"Variable '{var_name}' ist nicht definiert"
  158. raise ValueError(error_msg)
  159. return placeholder
  160. def substitute(self, text: str) -> str:
  161. """
  162. Substituiert Variablen in einem String.
  163. Args:
  164. text: String mit Variablenreferenzen.
  165. Returns:
  166. String mit substituierten Werten.
  167. """
  168. result = self.substitute_with_info(text)
  169. return result.result
  170. def substitute_with_info(self, text: str) -> SubstitutionResult:
  171. """
  172. Substituiert Variablen und gibt detaillierte Informationen zurück.
  173. Args:
  174. text: String mit Variablenreferenzen.
  175. Returns:
  176. SubstitutionResult mit Details.
  177. """
  178. substitutions: list[tuple[str, str, str]] = []
  179. unresolved: list[str] = []
  180. def replacer(match: re.Match) -> str:
  181. return self._replace_match(match, substitutions, unresolved)
  182. result = self.PATTERN.sub(replacer, text)
  183. return SubstitutionResult(
  184. original=text,
  185. result=result,
  186. substitutions=substitutions,
  187. unresolved=unresolved,
  188. )
  189. def substitute_recursive(
  190. self,
  191. data: Any,
  192. _depth: int = 0,
  193. max_depth: int = 100,
  194. ) -> Any:
  195. """
  196. Substituiert Variablen rekursiv in einer Datenstruktur.
  197. Args:
  198. data: Datenstruktur (dict, list, str, etc.).
  199. _depth: Aktuelle Rekursionstiefe (intern).
  200. max_depth: Maximale Rekursionstiefe.
  201. Returns:
  202. Datenstruktur mit substituierten Werten.
  203. """
  204. if _depth > max_depth:
  205. raise RecursionError(
  206. f"Maximale Rekursionstiefe {max_depth} überschritten"
  207. )
  208. if isinstance(data, str):
  209. return self.substitute(data)
  210. elif isinstance(data, dict):
  211. return {
  212. self.substitute(k) if isinstance(k, str) else k: self.substitute_recursive(
  213. v, _depth + 1, max_depth
  214. )
  215. for k, v in data.items()
  216. }
  217. elif isinstance(data, list):
  218. return [
  219. self.substitute_recursive(item, _depth + 1, max_depth)
  220. for item in data
  221. ]
  222. elif isinstance(data, tuple):
  223. return tuple(
  224. self.substitute_recursive(item, _depth + 1, max_depth)
  225. for item in data
  226. )
  227. else:
  228. return data
  229. def list_variables(self, text: str) -> list[str]:
  230. """
  231. Listet alle referenzierten Variablen in einem String.
  232. Args:
  233. text: String mit Variablenreferenzen.
  234. Returns:
  235. Liste der Variablennamen.
  236. """
  237. matches = self.PATTERN.findall(text)
  238. return [m[0] for m in matches] # Nur den Variablennamen
  239. def list_variables_recursive(self, data: Any) -> set[str]:
  240. """
  241. Listet alle Variablen in einer Datenstruktur.
  242. Args:
  243. data: Datenstruktur.
  244. Returns:
  245. Set der Variablennamen.
  246. """
  247. variables: set[str] = set()
  248. if isinstance(data, str):
  249. variables.update(self.list_variables(data))
  250. elif isinstance(data, dict):
  251. for key, value in data.items():
  252. if isinstance(key, str):
  253. variables.update(self.list_variables(key))
  254. variables.update(self.list_variables_recursive(value))
  255. elif isinstance(data, (list, tuple)):
  256. for item in data:
  257. variables.update(self.list_variables_recursive(item))
  258. return variables
  259. def validate(self, data: Any) -> list[str]:
  260. """
  261. Validiert, dass alle referenzierten Variablen definiert sind.
  262. Args:
  263. data: Datenstruktur zu validieren.
  264. Returns:
  265. Liste der fehlenden Variablen.
  266. """
  267. variables = self.list_variables_recursive(data)
  268. missing = [var for var in variables if var not in self._env]
  269. return missing
  270. def substitute_env(
  271. text: str,
  272. strict: bool = False,
  273. ) -> str:
  274. """
  275. Convenience-Funktion für Umgebungsvariablen-Substitution.
  276. Args:
  277. text: String mit Variablenreferenzen.
  278. strict: Ob bei fehlenden Variablen ein Fehler geworfen wird.
  279. Returns:
  280. String mit substituierten Werten.
  281. """
  282. substitutor = EnvironmentSubstitutor(strict=strict)
  283. return substitutor.substitute(text)
  284. def substitute_env_recursive(
  285. data: Any,
  286. strict: bool = False,
  287. ) -> Any:
  288. """
  289. Convenience-Funktion für rekursive Substitution.
  290. Args:
  291. data: Datenstruktur.
  292. strict: Ob bei fehlenden Variablen ein Fehler geworfen wird.
  293. Returns:
  294. Datenstruktur mit substituierten Werten.
  295. """
  296. substitutor = EnvironmentSubstitutor(strict=strict)
  297. return substitutor.substitute_recursive(data)