"""Safe, small input helpers for harness contracts.""" from __future__ import annotations import json from pathlib import Path from typing import Any, TypeVar import yaml from pydantic import BaseModel MAX_INPUT_BYTES = 2 * 1024 * 1024 ModelT = TypeVar("ModelT", bound=BaseModel) class InputError(ValueError): """Raised when an input file cannot be safely decoded as a model payload.""" def load_mapping(path: str | Path, *, max_bytes: int = MAX_INPUT_BYTES) -> dict[str, Any]: """Load one JSON/YAML mapping without constructing arbitrary Python objects.""" input_path = Path(path) try: size = input_path.stat().st_size except OSError as exc: raise InputError(f"입력 파일을 읽을 수 없습니다: {input_path}") from exc if size > max_bytes: raise InputError(f"입력 파일이 {max_bytes}바이트 제한을 초과했습니다: {input_path}") try: text = input_path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError) as exc: raise InputError(f"입력 파일은 UTF-8 텍스트여야 합니다: {input_path}") from exc suffix = input_path.suffix.casefold() try: if suffix == ".json": value = json.loads(text) elif suffix in {".yaml", ".yml"}: value = yaml.safe_load(text) else: raise InputError("지원 형식은 .json, .yaml, .yml입니다.") except (json.JSONDecodeError, yaml.YAMLError) as exc: raise InputError(f"JSON/YAML 구문이 올바르지 않습니다: {input_path}") from exc if not isinstance(value, dict): raise InputError(f"입력 최상위 값은 객체(mapping)여야 합니다: {input_path}") return value def load_model(path: str | Path, model_type: type[ModelT]) -> ModelT: """Load and validate a Pydantic contract from JSON/YAML.""" return model_type.model_validate(load_mapping(path)) def dump_json(model: BaseModel) -> str: """Serialize a contract deterministically for audit-friendly output.""" return json.dumps( model.model_dump(mode="json"), ensure_ascii=False, indent=2, sort_keys=True, ) __all__ = ["InputError", "MAX_INPUT_BYTES", "dump_json", "load_mapping", "load_model"]