"""Parse e validazione del contratto `model.yml` nelle repo utente. Schema sintetico (vedi piano): name, type, version, python train: {entrypoint, inputs, outputs, metrics} test: {entrypoint, io, input_schema[], output_schema[]} resources: {cpu, mem_mb, gpu} """ from __future__ import annotations from typing import Any, Optional import yaml from pydantic import BaseModel, ValidationError from core import gitea, redis_client class _FieldSpec(BaseModel): name: str dtype: str min: Optional[float] = None max: Optional[float] = None unit: Optional[str] = None class _Train(BaseModel): entrypoint: str inputs: dict = {} outputs: dict = {} metrics: dict = {} class _Test(BaseModel): entrypoint: str io: str = "stdio_json" input_schema: list[_FieldSpec] = [] output_schema: list[_FieldSpec] = [] class ModelSpec(BaseModel): name: str type: str version: str = "0.1.0" python: str = "3.11" train: _Train test: Optional[_Test] = None resources: dict = {} def parse_yaml(content: bytes | str) -> dict: """Parsa stringa YAML → dict validato. Solleva ValueError su errore.""" if isinstance(content, bytes): content = content.decode("utf-8") try: raw = yaml.safe_load(content) or {} spec = ModelSpec(**raw) return spec.model_dump() except (yaml.YAMLError, ValidationError) as e: raise ValueError(f"invalid model.yml: {e}") from e async def fetch_and_parse_spec(owner_repo: str, ref: str) -> Optional[dict]: """Recupera model.yml dalla repo alla revisione e lo parsa. Cache Redis `ml:modelspec:{repo}:{ref}` TTL 1h. """ cache_key = f"ml:modelspec:{owner_repo}:{ref}" try: cached = await redis_client.client().get(cache_key) if cached: import json return json.loads(cached) except Exception: pass try: raw = await gitea.get_file_raw(owner_repo, ref, "model.yml") except Exception: try: raw = await gitea.get_file_raw(owner_repo, ref, "model.yaml") except Exception: return None spec = parse_yaml(raw) try: import json await redis_client.client().set(cache_key, json.dumps(spec), ex=3600) except Exception: pass return spec