chore: initial import
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Plugin-Adapter für „Zutaten-Kategorien“.
|
||||
|
||||
Alles Verändernde läuft über tool/categories.py im Job-Runner, nie direkt aus einer
|
||||
Anfrage. Die Auswahl steht vorher in einer Plandatei, die sich ansehen, ändern
|
||||
und im Zweifel einfach nicht anwenden lässt.
|
||||
"""
|
||||
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 = "categories.py"
|
||||
|
||||
|
||||
class EditRequest(BaseModel):
|
||||
accepted: list[dict[str, Any]]
|
||||
|
||||
|
||||
class PlanRef(BaseModel):
|
||||
plan: str
|
||||
|
||||
|
||||
class ApplyRequest(BaseModel):
|
||||
plan: str
|
||||
apply: bool = True
|
||||
|
||||
|
||||
class PrefillRequest(BaseModel):
|
||||
plan: str
|
||||
model: str | None = None
|
||||
alle: bool = False
|
||||
|
||||
|
||||
class ProbeRequest(BaseModel):
|
||||
model: str | None = 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 aus der Oberfläche. Eng gehalten: je Zutat lassen
|
||||
sich nur `accept` und `proposed_id` ändern, und `proposed_id` muss eine
|
||||
der im Plan bekannten Kategorien sein (oder null). So kann die Oberfläche
|
||||
keine fremde ID unterschieben.
|
||||
"""
|
||||
file = resolve_plan(name)
|
||||
plan = json.loads(file.read_text(encoding="utf-8"))
|
||||
valid = {c["id"]: c["name"] for c in plan.get("categories", [])}
|
||||
by_aid = {f.get("aid"): f for f in plan.get("foods", []) if f.get("aid")}
|
||||
|
||||
gesetzt = 0
|
||||
for wish in request.accepted:
|
||||
food = by_aid.get(wish.get("aid"))
|
||||
if food is None:
|
||||
continue
|
||||
if "proposed_id" in wish:
|
||||
pid = wish.get("proposed_id")
|
||||
if pid is None:
|
||||
food["proposed_id"] = None
|
||||
food["proposed_name"] = ""
|
||||
elif pid in valid:
|
||||
food["proposed_id"] = pid
|
||||
food["proposed_name"] = valid[pid]
|
||||
# unbekannte ID wird ignoriert
|
||||
food["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 f in plan["foods"]
|
||||
if f.get("accept") and f.get("proposed_id") != f.get("current_id"))
|
||||
return {"ok": True, "gesetzt": gesetzt, "zuweisungen": 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="Kategorien 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]
|
||||
if request.alle:
|
||||
argv.append("--alle")
|
||||
job = await ctx.jobs.start(
|
||||
plugin=ctx.id, label=f"KI ordnet ein: {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=("Kategorien zuweisen" 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