#!/usr/bin/env python3 """Load Manual Invoice's semantic model and query policy.""" from __future__ import annotations import os from fnmatch import fnmatchcase from pathlib import Path from typing import Any, Dict, Optional from runtime_config import PLUGIN_ROOT, find_project_config DEFAULT_MODEL = PLUGIN_ROOT / "skills" / "manual-invoice" / "references" / "semantic-model.yaml" def discover_semantic_model(start: Optional[Path] = None) -> Path: explicit = os.environ.get("MANUAL_INVOICE_SEMANTIC_MODEL") if explicit: return Path(explicit).expanduser().resolve() project_config = find_project_config(start) if project_config: project_model = project_config.parent / ".manual-invoice" / "semantic-model.yaml" if project_model.is_file(): return project_model return DEFAULT_MODEL def load_semantic_model(path: Optional[Path] = None, start: Optional[Path] = None) -> Dict[str, Any]: try: import yaml except ImportError as exc: raise RuntimeError("PyYAML is required; run bootstrap.py.") from exc model_path = (path or discover_semantic_model(start)).expanduser().resolve() if not model_path.is_file(): raise FileNotFoundError(f"Semantic model not found: {model_path}") data = yaml.safe_load(model_path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise ValueError(f"Semantic model must contain an object: {model_path}") data["_path"] = str(model_path) return data def resolve_entity(model: Dict[str, Any], name: str) -> Optional[Dict[str, Any]]: normalized = name.strip().lower() for entity_name, entity in model.get("entities", {}).items(): aliases = [entity_name, *entity.get("aliases", [])] if normalized in {str(alias).strip().lower() for alias in aliases}: resolved = dict(entity) resolved["name"] = entity_name return resolved return None def allowed_table_patterns(model: Dict[str, Any]) -> list[str]: return [ str(pattern).strip() for pattern in model.get("policy", {}).get("allowed_tables", []) if str(pattern).strip() ] def is_table_allowed(model: Dict[str, Any], table_id: str) -> bool: normalized = table_id.strip().strip("`").lower() return any( fnmatchcase(normalized, pattern.lower()) for pattern in allowed_table_patterns(model) ) def is_dataset_allowed(model: Dict[str, Any], dataset_id: str) -> bool: normalized = dataset_id.strip().strip("`").lower() return any( fnmatchcase(f"{normalized}.__manual_invoice_table__", pattern.lower()) for pattern in allowed_table_patterns(model) ) def allowed_projects(model: Dict[str, Any]) -> list[str]: projects = { pattern.split(".", 1)[0] for pattern in allowed_table_patterns(model) if "." in pattern and "*" not in pattern.split(".", 1)[0] } projects.update( str(dataset.get("project", "")).strip() for dataset in model.get("datasets", {}).values() if str(dataset.get("project", "")).strip() ) return sorted(projects)