Files
boehmitools-main/plugins/tandoor-conversions/backend.py
T
2026-07-24 21:37:03 +02:00

195 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Plugin-Adapter für „Einheiten-Umrechnungen“.
Alles Verändernde läuft über tool/conversions.py im Job-Runner. Die Auswahl
steht vorher in einer Plandatei. Angelegte Umrechnungen sind über die
Sicherungen wieder löschbar.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel
from core.jobs import job_router
from core.backups import backup_router
TOOL = "conversions.py"
class EditRequest(BaseModel):
accepted: list[dict[str, Any]]
class PrefillRequest(BaseModel):
plan: str
model: str | None = None
class ApplyRequest(BaseModel):
plan: str
apply: bool = True
class ProbeRequest(BaseModel):
model: str | None = None
def _num(value: Any) -> float | None:
try:
f = float(value)
return f if f > 0 else None
except (TypeError, ValueError):
return None
def create_app(ctx):
tool = ctx.path("tool", TOOL)
plans_dir = ctx.data_dir / "plaene"
plans_dir.mkdir(parents=True, exist_ok=True)
def tool_env() -> dict:
env = ctx.settings.tool_env()
env["DATA_DIR"] = str(ctx.data_dir)
return env
def resolve_plan(name: str) -> Path:
candidate = plans_dir / Path(name).name
if candidate.suffix != ".json" or not candidate.is_file():
raise HTTPException(404, f"Plandatei „{name}“ gibt es nicht.")
return candidate
app = FastAPI(title=ctx.meta.name, docs_url="/api/docs", redoc_url=None)
@app.get("/", include_in_schema=False)
def index():
return FileResponse(ctx.path("static", "index.html"))
@app.get("/api/state")
def state() -> dict[str, Any]:
running = [j.info() for j in ctx.jobs.running(ctx.id)]
return {
"plans": sorted((p.name for p in plans_dir.glob("*.json")), reverse=True),
"tandoor": ctx.settings.status()["tandoor"],
"openai": ctx.settings.status()["openai"],
"running": running[0] if running else None,
}
@app.get("/api/plans/{name}")
def read_plan(name: str) -> dict[str, Any]:
return json.loads(resolve_plan(name).read_text(encoding="utf-8"))
@app.post("/api/plans/{name}")
def save_plan(name: str, request: EditRequest) -> dict[str, Any]:
"""
Übernimmt die Auswahl. Eng gehalten: je Kandidat lassen sich nur
`accept`, die Ausgangsmenge (`base_amount`) und das Gramm-Gewicht
(`grams`) ändern. Einheit und Zutat stehen fest (immer nach Gramm), die
Oberfläche kann daran nichts drehen.
"""
file = resolve_plan(name)
plan = json.loads(file.read_text(encoding="utf-8"))
by_aid = {k.get("aid"): k for k in plan.get("candidates", []) if k.get("aid")}
gesetzt = 0
for wish in request.accepted:
k = by_aid.get(wish.get("aid"))
if k is None:
continue
if "base_amount" in wish:
v = _num(wish["base_amount"])
if v is not None:
k["base_amount"] = v
if "grams" in wish:
v = _num(wish["grams"])
if v is not None:
k["grams"] = v
k["accept"] = bool(wish.get("accept"))
gesetzt += 1
file.write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8")
offen = sum(1 for k in plan["candidates"]
if k.get("accept") and k.get("grams") and k.get("base_amount"))
return {"ok": True, "gesetzt": gesetzt, "umrechnungen": offen}
# ---------------------------------------------------------------- Läufe
@app.post("/api/run/pruefen")
async def run_scan() -> dict[str, Any]:
if ctx.jobs.running(ctx.id):
raise HTTPException(409, "Es läuft bereits etwas.")
if not ctx.settings.status()["tandoor"]:
raise HTTPException(400, "Tandoor-URL und Token fehlen.")
job = await ctx.jobs.start(
plugin=ctx.id, label="Umrechnungen prüfen",
argv=[sys.executable, str(tool), "pruefen"],
cwd=ctx.data_dir, env=tool_env(),
)
return job.info()
@app.post("/api/run/vorschlagen")
async def run_prefill(request: PrefillRequest) -> dict[str, Any]:
if ctx.jobs.running(ctx.id):
raise HTTPException(409, "Es läuft bereits etwas.")
if not ctx.settings.status()["openai"]:
raise HTTPException(400, "OpenAI-Key fehlt siehe Einstellungen.")
file = resolve_plan(request.plan)
argv = [sys.executable, str(tool), "vorschlagen", "--plan", str(file)]
if request.model:
argv += ["--model", request.model]
job = await ctx.jobs.start(
plugin=ctx.id, label=f"KI schlägt vor: {file.stem}",
argv=argv, cwd=ctx.data_dir, env=tool_env(),
)
return job.info()
@app.post("/api/run/anwenden")
async def run_apply(request: ApplyRequest) -> dict[str, Any]:
if ctx.jobs.running(ctx.id):
raise HTTPException(409, "Es läuft bereits etwas.")
if not ctx.settings.status()["tandoor"]:
raise HTTPException(400, "Tandoor-URL und Token fehlen.")
file = resolve_plan(request.plan)
argv = [sys.executable, str(tool), "anwenden", "--plan", str(file),
"--continue-on-error"]
if request.apply:
argv.append("--apply")
job = await ctx.jobs.start(
plugin=ctx.id,
label=("Umrechnungen anlegen" if request.apply else "Trockenübung")
+ f": {file.stem}",
argv=argv, cwd=ctx.data_dir, env=tool_env(),
)
return job.info()
@app.post("/api/run/probe")
async def run_probe(request: ProbeRequest) -> dict[str, Any]:
if ctx.jobs.running(ctx.id):
raise HTTPException(409, "Es läuft bereits etwas.")
if not ctx.settings.status()["openai"]:
raise HTTPException(400, "OpenAI-Key fehlt siehe Einstellungen.")
argv = [sys.executable, str(tool), "probe"]
if request.model:
argv += ["--model", request.model]
job = await ctx.jobs.start(
plugin=ctx.id, label="OpenAI-Verbindung testen",
argv=argv, cwd=ctx.data_dir, env=tool_env(),
)
return job.info()
def restore_argv(run: str, apply: bool, force: bool) -> list[str]:
argv = [sys.executable, str(tool), "zurueck", "--lauf", run]
if apply:
argv.append("--apply")
return argv
app.include_router(backup_router(ctx, "laeufe", restore_argv))
app.include_router(job_router(ctx.jobs, ctx.id))
return app