chore: initial import
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user