- Implemented HTML pages for datasets, models, training, testing, and results. - Created API endpoints for managing repositories, results, tests, and training sessions. - Added functionality for streaming training progress via Server-Sent Events (SSE). - Introduced a Dockerfile for the ML runner with necessary dependencies. - Developed an SDK for user code execution within the runner container. - Enhanced CSS styles for improved UI layout and navigation. - Established a layout template for consistent HTML structure across pages. - Added JavaScript for dynamic interactions on the models page. - Implemented WebSocket handling for real-time communication with kiosk devices and controllers. - Implemented model registration and management API at /api/models - Added Gitea proxy API for repository interactions at /api/repos - Created results API for listing and comparing training results at /api/results - Developed training management API for enqueueing and retrieving training jobs at /api/trainings - Introduced SSE endpoint for live training progress updates - Added HTML pages for models, datasets, and training management - Created a Dockerfile for the ML runner with necessary dependencies - Developed SDK for user code execution within the runner container - Enhanced CSS styles for improved UI/UX - Implemented WebSocket communication for real-time device and controller interactions in the kiosk system
132 lines
4.1 KiB
Python
132 lines
4.1 KiB
Python
"""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(),
|
|
}
|