from __future__ import annotations import json from dataclasses import dataclass from typing import Any, Dict, Iterable, Mapping from .util import parse_iso class ValidationError(Exception): """Raised when user payload fails validation.""" @dataclass class Node: id: str name: str type: str version: str | None status: str config: Dict[str, Any] labels: Iterable[str] meta_data: Dict[str, Any] health: Dict[str, Any] register_time: str | None last_report: str | None agent_last_report: str | None last_updated: str | None def serialize_node_row(row: Mapping[str, Any]) -> Dict[str, Any]: def _json_or_default(value: str | None, default: Any) -> Any: if value is None or value == "": return default try: return json.loads(value) except json.JSONDecodeError: return default config = _json_or_default(row["config_json"], {}) labels = _json_or_default(row["labels_json"], []) meta = _json_or_default(row["meta_json"], {}) health = _json_or_default(row["health_json"], {}) return { "id": row["id"], "name": row["name"], "type": row["type"], "version": row["version"], "status": row["status"], "config": config if isinstance(config, dict) else {}, "label": list(labels) if isinstance(labels, list) else [], "meta_data": meta if isinstance(meta, dict) else {}, "health": health if isinstance(health, dict) else {}, "register_time": row["register_time"], "last_report": row["last_report"], "agent_last_report": row["agent_last_report"], "last_updated": row["last_updated"], } def serialize_node_summary(row: Mapping[str, Any]) -> Dict[str, Any]: return { "id": row["id"], "name": row["name"], "status": row["status"], "type": row["type"], "version": row["version"], } def validate_registration_payload(payload: Mapping[str, Any]) -> Dict[str, Any]: if not isinstance(payload, Mapping): raise ValidationError("Request body must be a JSON object") name = payload.get("name") if not isinstance(name, str) or not name.strip(): raise ValidationError("Field 'name' is required and must be a non-empty string") node_type = payload.get("type", "agent") if not isinstance(node_type, str) or not node_type: raise ValidationError("Field 'type' must be a string") version = payload.get("version") if version is not None and not isinstance(version, str): raise ValidationError("Field 'version' must be a string if provided") meta = payload.get("meta_data") if not isinstance(meta, Mapping): raise ValidationError("Field 'meta_data' must be an object") required_meta = ["hostname", "ip", "env", "user", "instance", "cpu_number", "memory_in_bytes", "gpu_number"] for key in required_meta: if key not in meta: raise ValidationError(f"meta_data.{key} is required") cpu_number = meta["cpu_number"] memory_in_bytes = meta["memory_in_bytes"] gpu_number = meta["gpu_number"] if not isinstance(cpu_number, int) or cpu_number < 0: raise ValidationError("meta_data.cpu_number must be a non-negative integer") if not isinstance(memory_in_bytes, int) or memory_in_bytes < 0: raise ValidationError("meta_data.memory_in_bytes must be a non-negative integer") if not isinstance(gpu_number, int) or gpu_number < 0: raise ValidationError("meta_data.gpu_number must be a non-negative integer") node_id = payload.get("id") if node_id is not None and (not isinstance(node_id, str) or not node_id.strip()): raise ValidationError("Field 'id' must be a non-empty string when provided") return { "id": node_id, "name": name, "type": node_type, "version": version, "meta_data": dict(meta), } def validate_status_payload(payload: Mapping[str, Any]) -> Dict[str, Any]: if not isinstance(payload, Mapping): raise ValidationError("Request body must be a JSON object") timestamp = payload.get("timestamp") if not isinstance(timestamp, str) or not timestamp: raise ValidationError("Field 'timestamp' is required and must be a string") parsed = parse_iso(timestamp) if parsed is None: raise ValidationError("Field 'timestamp' must be an ISO8601 datetime string") health = payload.get("health", {}) if not isinstance(health, Mapping): raise ValidationError("Field 'health' must be an object if provided") sanitized_health: Dict[str, Any] = {} for key, value in health.items(): if not isinstance(key, str): raise ValidationError("Keys in 'health' must be strings") if not isinstance(value, (Mapping, list, str, int, float, bool)) and value is not None: raise ValidationError("Values in 'health' must be JSON-compatible") sanitized_health[key] = value return { "timestamp": timestamp, "parsed_timestamp": parsed, "health": sanitized_health, } def validate_config_payload(payload: Mapping[str, Any]) -> Dict[str, Any]: if not isinstance(payload, Mapping): raise ValidationError("Request body must be a JSON object") result: Dict[str, Any] = {} if "config" in payload: config = payload["config"] if not isinstance(config, Mapping): raise ValidationError("Field 'config' must be an object") result["config"] = dict(config) if "label" in payload: labels = payload["label"] if not isinstance(labels, list) or not all(isinstance(item, str) for item in labels): raise ValidationError("Field 'label' must be an array of strings") result["label"] = list(labels) if not result: raise ValidationError("At least one of 'config' or 'label' must be provided") return result