"""API /api/models — registro modelli (repo Gitea + metadata).""" from __future__ import annotations import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException from core import db from core.auth import require_auth from core.model_spec import fetch_and_parse_spec router = APIRouter(prefix="/api/models", tags=["models"]) def _row(r) -> Optional[dict]: if r is None: return None d = dict(r) for k in ("created_at", "updated_at"): if d.get(k) is not None and hasattr(d[k], "isoformat"): d[k] = d[k].isoformat() return d @router.get("") async def list_models(user=Depends(require_auth)): rows = await db.fetch("SELECT * FROM models ORDER BY created_at DESC LIMIT 500") return {"count": len(rows), "models": [_row(r) for r in rows]} @router.post("", status_code=201) async def create_model(body: dict, user=Depends(require_auth)): required = ("name", "type", "gitea_repo") for k in required: if not body.get(k): raise HTTPException(400, f"missing field: {k}") # prova a pre-caricare model.yml dal default branch (non fatale) spec = None try: spec = await fetch_and_parse_spec(body["gitea_repo"], body.get("default_branch") or "main") except Exception: spec = None row = await db.fetchrow( """ INSERT INTO models (name, type, gitea_repo, default_branch, spec, created_by) VALUES ($1,$2,$3,$4,$5,$6) RETURNING * """, body["name"], body["type"], body["gitea_repo"], body.get("default_branch") or "main", spec, user.get("username") or "unknown", ) return _row(row) @router.get("/{model_id}") async def get_model(model_id: str, user=Depends(require_auth)): row = await db.fetchrow("SELECT * FROM models WHERE id = $1", uuid.UUID(model_id)) if not row: raise HTTPException(404, "not found") return _row(row) @router.patch("/{model_id}") async def patch_model(model_id: str, body: dict, user=Depends(require_auth)): allowed = {"name", "type", "default_branch"} sets = [] args: list = [] for k, v in body.items(): if k in allowed: args.append(v) sets.append(f"{k} = ${len(args)}") if not sets: raise HTTPException(400, "no fields to update") args.append(uuid.UUID(model_id)) row = await db.fetchrow( f"UPDATE models SET {', '.join(sets)} WHERE id = ${len(args)} RETURNING *", *args, ) if not row: raise HTTPException(404, "not found") return _row(row) @router.delete("/{model_id}", status_code=204) async def delete_model(model_id: str, user=Depends(require_auth)): await db.execute("DELETE FROM models WHERE id = $1", uuid.UUID(model_id)) return None # ── Notes ────────────────────────────────────────────────────────────────── @router.get("/{model_id}/notes") async def list_notes(model_id: str, user=Depends(require_auth)): rows = await db.fetch( "SELECT id, author, text, created_at FROM model_notes WHERE model_id = $1 ORDER BY created_at DESC", uuid.UUID(model_id), ) return [ { "id": str(r["id"]), "author": r["author"], "text": r["text"], "created_at": r["created_at"].isoformat(), } for r in rows ] @router.post("/{model_id}/notes", status_code=201) async def add_note(model_id: str, body: dict, user=Depends(require_auth)): text = (body.get("text") or "").strip() if not text: raise HTTPException(400, "text required") row = await db.fetchrow( "INSERT INTO model_notes (model_id, author, text) VALUES ($1, $2, $3) RETURNING *", uuid.UUID(model_id), user.get("username") or "unknown", text, ) return { "id": str(row["id"]), "author": row["author"], "text": row["text"], "created_at": row["created_at"].isoformat(), }