Files
alfred/alfred/infrastructure/subtitle/rule_repository.py
T
francwa 249c5de76a feat: major architectural refactor
- Refactor memory system (episodic/STM/LTM with components)
- Implement complete subtitle domain (scanner, matcher, placer)
- Add YAML workflow infrastructure
- Externalize knowledge base (patterns, release groups)
- Add comprehensive testing suite
- Create manual testing CLIs
2026-05-11 21:55:06 +02:00

117 lines
4.2 KiB
Python

"""RuleSetRepository — loads SubtitleRuleSet from .alfred/ YAML files."""
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import yaml
from alfred.domain.subtitles.aggregates import SubtitleRuleSet
from alfred.domain.subtitles.value_objects import RuleScope
if TYPE_CHECKING:
from alfred.infrastructure.persistence.memory.ltm.components.subtitle_preferences import SubtitlePreferences
logger = logging.getLogger(__name__)
def _load_yaml(path: Path) -> dict:
if not path.exists():
return {}
try:
with open(path, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except Exception as e:
logger.warning(f"RuleSetRepository: could not read {path}: {e}")
return {}
class RuleSetRepository:
"""
Builds a fully chained SubtitleRuleSet by reading YAML from .alfred/.
Inheritance chain:
global (hardcoded defaults)
└── release_group (.alfred/release_groups/{GROUP}.yaml)
└── local (.alfred/rules.yaml)
Rules are delta-only — None means "inherit from parent".
The repository only creates intermediate nodes when the corresponding
file exists and contains an override section.
"""
def __init__(self, library_root: Path):
self._alfred_dir = library_root / ".alfred"
def load(
self,
release_group: str | None = None,
subtitle_preferences: "SubtitlePreferences | None" = None,
) -> SubtitleRuleSet:
"""
Build and return the resolved RuleSet chain.
If subtitle_preferences is provided, it seeds the global base rule set
from LTM (overriding the hardcoded DEFAULT_RULES).
Returns global default if no overrides exist.
"""
base = SubtitleRuleSet.global_default()
if subtitle_preferences is not None:
base.override(
languages=subtitle_preferences.languages,
formats=subtitle_preferences.formats,
types=subtitle_preferences.types,
)
current = base
# Release group level
if release_group:
rg_path = self._alfred_dir / "release_groups" / f"{release_group}.yaml"
rg_data = _load_yaml(rg_path).get("override", {})
if rg_data:
rg_ruleset = SubtitleRuleSet(
scope=RuleScope(level="release_group", identifier=release_group),
parent=current,
)
rg_ruleset.override(**_filter_override(rg_data))
current = rg_ruleset
logger.debug(f"RuleSetRepository: loaded release_group override for '{release_group}'")
# Local (show/movie) level
local_data = _load_yaml(self._alfred_dir / "rules.yaml").get("override", {})
if local_data:
local_ruleset = SubtitleRuleSet(
scope=RuleScope(level="show"),
parent=current,
)
local_ruleset.override(**_filter_override(local_data))
current = local_ruleset
logger.debug("RuleSetRepository: loaded local rules.yaml override")
return current
def save_local(self, delta: dict) -> None:
"""Write or update .alfred/rules.yaml with override delta."""
self._alfred_dir.mkdir(parents=True, exist_ok=True)
path = self._alfred_dir / "rules.yaml"
existing = _load_yaml(path)
existing_override = existing.get("override", {})
existing_override.update(delta)
data = {"override": existing_override}
tmp = path.with_suffix(".yaml.tmp")
try:
with open(tmp, "w", encoding="utf-8") as f:
yaml.safe_dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
tmp.rename(path)
logger.info(f"RuleSetRepository: saved local rules to {path}")
except Exception as e:
logger.error(f"RuleSetRepository: could not write {path}: {e}")
tmp.unlink(missing_ok=True)
raise
def _filter_override(data: dict) -> dict:
"""Keep only keys that SubtitleRuleSet.override() accepts."""
valid = {"languages", "formats", "types", "format_priority", "min_confidence"}
return {k: v for k, v in data.items() if k in valid}