from __future__ import annotations import uuid from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse from app.db import Database router = APIRouter(tags=["apps"]) def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() def _get_db() -> Database: from app.main import app return app.state.db @router.get("/api/apps") def list_apps(db: Database = Depends(_get_db)) -> list[dict]: apps = [] for workflow in db.list_workflows(): if not workflow["published"]: continue latest = db.get_latest_workflow_version(workflow["id"]) apps.append( { "id": workflow["id"], "name": workflow["name"], "description": workflow["description"], "version": workflow["latest_version"], "definition": latest["definition"] if latest else None, } ) return apps @router.post("/api/apps/{workflow_id}/runs") async def create_run( workflow_id: str, file: UploadFile = File(...), db: Database = Depends(_get_db), ) -> dict: workflow = db.get_workflow(workflow_id) if workflow is None or not workflow["published"]: raise HTTPException(status_code=404, detail="published workflow not found") latest = db.get_latest_workflow_version(workflow_id) if latest is None: raise HTTPException(status_code=422, detail="workflow has no version") run_id = f"run_{uuid.uuid4().hex[:12]}" filename = Path(file.filename or "upload.bin").name from app.config import STORAGE_DIR input_dir = STORAGE_DIR / "uploads" / run_id input_dir.mkdir(parents=True, exist_ok=True) input_uri = input_dir / filename content = await file.read() input_uri.write_bytes(content) now = _now_iso() db.create_run( { "id": run_id, "workflow_id": workflow_id, "workflow_version": latest["version"], "status": "QUEUED", "progress": 0, "input_uri": str(input_uri), "created_at": now, "updated_at": now, } ) return { "id": run_id, "status": "QUEUED", "progress": 0, "artifacts": [], } @router.get("/api/runs") def list_runs(db: Database = Depends(_get_db)) -> list[dict]: return db.list_runs() @router.get("/api/runs/{run_id}") def get_run(run_id: str, db: Database = Depends(_get_db)) -> dict: run = db.get_run(run_id) if run is None: raise HTTPException(status_code=404, detail="run not found") run["artifacts"] = db.list_artifacts(run_id) return run @router.get("/api/runs/{run_id}/artifacts") def list_run_artifacts(run_id: str, db: Database = Depends(_get_db)) -> list[dict]: if db.get_run(run_id) is None: raise HTTPException(status_code=404, detail="run not found") return db.list_artifacts(run_id) @router.get("/api/runs/{run_id}/artifacts/{artifact_name}") def download_artifact( run_id: str, artifact_name: str, db: Database = Depends(_get_db), ) -> FileResponse: artifact = db.get_artifact(run_id, artifact_name) if artifact is None: raise HTTPException(status_code=404, detail="artifact not found") path = Path(artifact["uri"]) if not path.is_file(): raise HTTPException(status_code=404, detail="artifact file missing") return FileResponse( path, media_type=artifact["mime_type"], filename=path.name, )