chore: initial import

This commit is contained in:
2026-07-24 21:37:03 +02:00
commit 45bc449ea0
53 changed files with 12329 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
# -*- 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