"""Mock de ejemplo. Replicá esta estructura para tu mock real. Patrones aplicados: - /health endpoint (Coolify/Traefik) - /admin/reset endpoint (para rehearsals) - Pydantic con coerción de tipos (issue I-003) - Custom auth header X-Orchestrate-Token (regla C2) - Logs estructurados como JSON """ from __future__ import annotations import json import os import sys from datetime import datetime, timezone from typing import Optional from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel, field_validator # ─── Helpers de coerción (issue I-003) ────────────────────────────────────── def _coerce_int(v): if isinstance(v, str) and v.strip().lstrip("-").isdigit(): return int(v) return v def _coerce_list(v): if isinstance(v, str): try: return json.loads(v) except Exception: return [v] return v # ─── Seed data ────────────────────────────────────────────────────────────── SEED = { "users": [ {"username": "juan.perez", "email": "juan.perez@example.com", "active": True, "department": "Finanzas"}, {"username": "maria.lopez", "email": "maria.lopez@example.com", "active": True, "department": "Operaciones"}, {"username": "ana.torres", "email": "ana.torres@example.com", "active": False, "department": "RRHH"}, ], } state: dict = {} def _reset(): global state state = {"users": [u.copy() for u in SEED["users"]]} _reset() # ─── Models ───────────────────────────────────────────────────────────────── class LookupInput(BaseModel): username: str class ExecuteInput(BaseModel): target_id: int options: list[str] = [] notify: bool = True _coerce_id = field_validator("target_id", mode="before")(_coerce_int) _coerce_opts = field_validator("options", mode="before")(_coerce_list) # ─── App ──────────────────────────────────────────────────────────────────── app = FastAPI(title="Example Mock") def _log(event: str, **kwargs): """Structured log for Coolify aggregation.""" payload = {"ts": datetime.now(timezone.utc).isoformat(), "event": event, **kwargs} print(json.dumps(payload), file=sys.stdout, flush=True) def _check_token(token: Optional[str]): expected = os.environ.get("MOCK_API_TOKEN") if expected and token != expected: raise HTTPException(status_code=401, detail="bad token") @app.get("/health") def health(): return {"status": "ok"} @app.post("/admin/reset") def admin_reset(): """Para rehearsals: vuelve al seed.""" _reset() _log("admin.reset") return {"reset": True} @app.post( "/users/lookup", description="Busca un usuario por username. Devuelve datos del usuario." ) def lookup_user( payload: LookupInput, x_orchestrate_token: str | None = Header(default=None), ): """Lookup de usuario — para que el agente decida si actuar.""" _check_token(x_orchestrate_token) user = next((u for u in state["users"] if u["username"] == payload.username), None) if not user: raise HTTPException(status_code=404, detail="user not found") _log("tool.lookup_user", username=payload.username, found=True) return user @app.post( "/execute", description="Ejecuta una acción sobre un target_id. Devuelve resultado." ) def execute_action( payload: ExecuteInput, x_orchestrate_token: str | None = Header(default=None), ): _check_token(x_orchestrate_token) _log("tool.execute", target_id=payload.target_id, options=payload.options) return { "ok": True, "target_id": payload.target_id, "completed_at": datetime.now(timezone.utc).isoformat(), }