118 lines
3.8 KiB
Python
118 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
|
|
def _norm(name: str) -> str:
|
|
return "".join(ch if ch.isalnum() else "_" for ch in name.upper())
|
|
|
|
|
|
class Node(dict):
|
|
__getattr__ = dict.get
|
|
|
|
@classmethod
|
|
def wrap(cls, value: Any) -> Any:
|
|
if isinstance(value, dict):
|
|
return cls({k: cls.wrap(v) for k, v in value.items()})
|
|
if isinstance(value, (list, tuple)):
|
|
return [cls.wrap(v) for v in value]
|
|
return value
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BuildContext:
|
|
project_root: Path
|
|
live_dir: Path
|
|
artifacts_root: Path
|
|
phase: str
|
|
kind: str
|
|
name: str
|
|
profile_name: str = ""
|
|
base_name: str = ""
|
|
feature_name: str = ""
|
|
profile: Mapping[str, Any] = field(default_factory=dict)
|
|
base: Mapping[str, Any] = field(default_factory=dict)
|
|
features: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "profile", Node.wrap(dict(self.profile)))
|
|
object.__setattr__(self, "base", Node.wrap(dict(self.base)))
|
|
object.__setattr__(self, "features", {k: Node.wrap(dict(v)) for k, v in self.features.items()})
|
|
|
|
def to_env(self) -> dict[str, str]:
|
|
env = {
|
|
"PROJECT_ROOT": str(self.project_root),
|
|
"LIVE_DIR": str(self.live_dir),
|
|
"ARTIFACTS_ROOT": str(self.artifacts_root),
|
|
"PHASE": self.phase,
|
|
"CURRENT_KIND": self.kind,
|
|
"CURRENT_NAME": self.name,
|
|
"PROFILE_NAME": self.profile_name,
|
|
"BASE_NAME": self.base_name,
|
|
"FEATURE_NAME": self.feature_name,
|
|
"FEATURE_NAMES": "|".join(self.features.keys()),
|
|
}
|
|
env.update(_flatten("PROFILE", self.profile))
|
|
env.update(_flatten("BASE", self.base))
|
|
for feat_name, feat_cfg in self.features.items():
|
|
env.update(_flatten(f"FEAT__{_norm(feat_name)}", feat_cfg))
|
|
return env
|
|
|
|
@classmethod
|
|
def from_env(cls, env: Mapping[str, str]) -> "BuildContext":
|
|
feature_names = [x for x in env.get("FEATURE_NAMES", "").split("|") if x]
|
|
profile = _inflate("PROFILE", env)
|
|
base = _inflate("BASE", env)
|
|
features = {name: _inflate(f"FEAT__{_norm(name)}", env) for name in feature_names}
|
|
return cls(
|
|
project_root=Path(env["PROJECT_ROOT"]),
|
|
live_dir=Path(env["LIVE_DIR"]),
|
|
artifacts_root=Path(env["ARTIFACTS_ROOT"]),
|
|
phase=env["PHASE"],
|
|
kind=env["CURRENT_KIND"],
|
|
name=env["CURRENT_NAME"],
|
|
profile_name=env.get("PROFILE_NAME", ""),
|
|
base_name=env.get("BASE_NAME", ""),
|
|
feature_name=env.get("FEATURE_NAME", ""),
|
|
profile=profile,
|
|
base=base,
|
|
features=features,
|
|
)
|
|
|
|
|
|
def _stringify(value: Any) -> str:
|
|
if isinstance(value, bool):
|
|
return "1" if value else "0"
|
|
if value is None:
|
|
return ""
|
|
if isinstance(value, (list, tuple)):
|
|
return "|".join(_stringify(v) for v in value)
|
|
return str(value)
|
|
|
|
|
|
def _flatten(prefix: str, data: Mapping[str, Any]) -> dict[str, str]:
|
|
out: dict[str, str] = {}
|
|
for key, value in data.items():
|
|
name = f"{prefix}__{_norm(str(key))}"
|
|
if isinstance(value, dict):
|
|
out.update(_flatten(name, value))
|
|
else:
|
|
out[name] = _stringify(value)
|
|
return out
|
|
|
|
|
|
def _inflate(prefix: str, env: Mapping[str, str]) -> dict[str, Any]:
|
|
root: dict[str, Any] = {}
|
|
marker = f"{prefix}__"
|
|
for key, value in env.items():
|
|
if not key.startswith(marker):
|
|
continue
|
|
parts = key[len(marker):].split("__")
|
|
node = root
|
|
for part in parts[:-1]:
|
|
node = node.setdefault(part.lower(), {})
|
|
node[parts[-1].lower()] = value
|
|
return root
|