import copy
import hashlib
import json
from src.shared.common import cfg, get_config_path, get_nested
_DEFAULT_POLICY = "minimal"
_VALID_POLICIES = {"none", "minimal", "full"}
_MINIMAL_PATHS = (
("compression", "mode"),
("compression", "topk_ratio"),
("data", "feature_cols"),
("data", "processed_dir"),
("data", "train_end"),
("data", "val_end"),
("data_download", "end_date"),
("federated", "num_clients"),
("federated", "rho"),
("model", "horizon"),
("model", "hidden_size"),
("model", "input_size"),
("model", "lstm_dropout"),
("model", "num_layers"),
("model", "seq_len"),
("model", "server_head_dropout"),
("model", "server_head_width"),
("profiler", "enabled"),
("scheduler", "enabled"),
("training", "checkpoint_interval"),
("training", "classification_loss_type"),
("training", "classification_positive_weight"),
("training", "classification_loss_weight"),
("training", "focal_alpha"),
("training", "focal_gamma"),
("training", "num_rounds"),
("training", "rain_threshold_mm"),
("training", "rain_probability_threshold"),
("training", "regression_loss_weight"),
("training", "seed"),
("training", "target_transform"),
)
def _set_nested(target: dict, path: tuple[str, ...], value) -> None:
cursor = target
for key in path[:-1]:
if key not in cursor or not isinstance(cursor[key], dict):
cursor[key] = {}
cursor = cursor[key]
cursor[path[-1]] = value
def _stable_json_payload(source: dict) -> bytes:
return json.dumps(
source,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
default=str,
).encode("utf-8")
def resolve_config_snapshot_policy(config: dict | None = None) -> str:
source = cfg if config is None else config
raw_policy = str(get_nested(source, ("artifacts", "config_snapshot_policy"), _DEFAULT_POLICY)).lower().strip()
if raw_policy not in _VALID_POLICIES:
return _DEFAULT_POLICY
return raw_policy
def config_sha256(config: dict | None = None) -> str:
source = cfg if config is None else config
return hashlib.sha256(_stable_json_payload(source)).hexdigest()
def build_config_ref(config: dict | None = None) -> dict[str, str]:
source = cfg if config is None else config
return {
"config_path": get_config_path(),
"config_sha256": config_sha256(source),
}
def build_minimal_config_snapshot(config: dict | None = None) -> dict:
source = cfg if config is None else config
snapshot: dict = {}
for path in _MINIMAL_PATHS:
value = get_nested(source, path, None)
if value is not None:
_set_nested(snapshot, path, value)
return snapshot
def build_config_snapshot(config: dict | None = None) -> tuple[dict | None, str]:
source = cfg if config is None else config
policy = resolve_config_snapshot_policy(source)
if policy == "none":
return None, policy
if policy == "full":
return copy.deepcopy(source), policy
return build_minimal_config_snapshot(source), policy