Source code for app

#!/usr/bin/env python3
"""FastAPI front-end for the Octopus widget scaffolder."""

import json
import os
from pathlib import Path
from typing import Iterable

import uvicorn
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import (
    HTMLResponse,
    PlainTextResponse,
    Response,
    StreamingResponse,
)
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.responses import FileResponse

from scaffolder.generator import scaffold_widget

app = FastAPI(title="Octopus Widget Generator")
BASE_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = BASE_DIR / "flask_templates"
OUTPUT_DIR = BASE_DIR / "output"
OUTPUT_DIR.mkdir(exist_ok=True)

templates = Jinja2Templates(directory=str(TEMPLATE_DIR))
app.mount(
    "/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static"
)


[docs] def to_pascal(name: str) -> str: return name[0].upper() + name[1:] if name else name
[docs] @app.get("/", response_class=HTMLResponse) async def index(request: Request) -> HTMLResponse: asset_version = int( max( (BASE_DIR / "static" / "js" / "script.js").stat().st_mtime, (BASE_DIR / "static" / "css" / "style.css").stat().st_mtime, ) ) # Compatibility across Starlette versions: # - new: TemplateResponse(request, name, context) # - old: TemplateResponse(name, context) try: return templates.TemplateResponse( request, "index.html", {"request": request, "asset_version": asset_version}, ) except TypeError: return templates.TemplateResponse( "index.html", {"request": request, "asset_version": asset_version}, )
def _prepare_stream( pascal: str, operations: list, var_defaults: dict, var_types: dict, app_id: str, feature_flags: dict | None, ) -> Iterable[str]: for msg in scaffold_widget( pascal, "widget", operations=operations, var_defaults=var_defaults, var_types=var_types, app_id=app_id, feature_flags=feature_flags, ): if msg.startswith("ZIP_READY:"): zip_path = msg.split("ZIP_READY:", 1)[1].strip() yield f"ZIP_READY:/download/{Path(zip_path).name}\n" else: yield msg + "\n"
[docs] @app.post("/generate") async def generate( pascal: str = Form(""), app_id: str = Form(""), ops: str = Form(""), features: str = Form(""), ) -> Response: pascal = to_pascal(pascal.strip()) app_id = app_id.strip() try: operations = json.loads(ops.strip()) if ops.strip() else [] except Exception as exc: # pragma: no cover - defensive logging only return PlainTextResponse(f"ERROR: Invalid ops JSON: {exc}\n") try: feature_flags = ( json.loads(features.strip()) if features.strip() else {} ) except Exception as exc: return PlainTextResponse(f"ERROR: Invalid features JSON: {exc}\n") var_defaults: dict = {} var_types: dict = {} for op in operations: for key, spec in (op.get("vars") or {}).items(): var_defaults[key] = spec.get("default") var_types[key] = spec.get("type") stream = _prepare_stream( pascal, operations, var_defaults, var_types, app_id, feature_flags ) return StreamingResponse(stream, media_type="text/plain")
[docs] @app.get("/download/{filename:path}") async def download(filename: str) -> FileResponse: candidate = (OUTPUT_DIR / filename).resolve() try: candidate.relative_to(OUTPUT_DIR.resolve()) except ValueError as exc: raise HTTPException(status_code=404, detail="File not found") from exc if not candidate.exists(): raise HTTPException(status_code=404, detail="File not found") return FileResponse(candidate, filename=candidate.name)
if __name__ == "__main__": debug_mode = os.getenv("FASTAPI_DEBUG", "0").lower() in { "1", "true", "yes", } reload_kwargs = {} if debug_mode: reload_kwargs = {"reload": True, "reload_dirs": [str(BASE_DIR)]} uvicorn.run("app:app", host="0.0.0.0", port=5002, **reload_kwargs)