feat: unify source libraries, generation plans and terminal bindings
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
from fastapi import Depends, Query, Request
|
||||
from fastapi.responses import Response
|
||||
from pydantic import Field
|
||||
|
||||
from pigway_frame_flow.core.auth import require
|
||||
from pigway_frame_flow.core.photo_preferences import set_flags
|
||||
from pigway_frame_flow.display.libraries import GenerationPlan
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.models import ImageExclusion, ImageStar, StrictModel
|
||||
|
||||
|
||||
class BindingInput(StrictModel):
|
||||
source_id: str = Field(min_length=1, max_length=120)
|
||||
plan_id: str = Field(pattern=r"^[a-f0-9]{32}$")
|
||||
|
||||
|
||||
class ImportInput(StrictModel):
|
||||
item_id: str = Field(min_length=1, max_length=2048)
|
||||
|
||||
|
||||
class RunInput(StrictModel):
|
||||
item_id: str | None = Field(default=None, min_length=1, max_length=2048)
|
||||
deliver: bool = False
|
||||
|
||||
|
||||
class LibraryModeInput(StrictModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
class GenerateInput(StrictModel):
|
||||
plan_id: str | None = Field(default=None, pattern=r"^[a-f0-9]{32}$")
|
||||
|
||||
|
||||
def install_libraries(app, service):
|
||||
libraries, db = service.libraries, service.db
|
||||
|
||||
@app.put(
|
||||
"/api/v1/jobs/{job_id}/library-mode",
|
||||
dependencies=[Depends(require("jobs:write"))],
|
||||
)
|
||||
def set_mode(job_id: str, payload: LibraryModeInput):
|
||||
return app.state.scheduler.set_library_mode(job_id, payload.enabled)
|
||||
|
||||
@app.get("/api/v1/source-libraries", dependencies=[Depends(require("images:read"))])
|
||||
def sources():
|
||||
return [
|
||||
{
|
||||
**source,
|
||||
"image_count": db.one(
|
||||
"SELECT COUNT(*) AS n FROM source_images WHERE source_id=?",
|
||||
(source["id"],),
|
||||
)["n"],
|
||||
}
|
||||
for source in service.photo_sources.list()
|
||||
]
|
||||
|
||||
@app.get(
|
||||
"/api/v1/source-libraries/{source_id}/images",
|
||||
dependencies=[Depends(require("images:read"))],
|
||||
)
|
||||
def pictures(
|
||||
source_id: str,
|
||||
offset: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
):
|
||||
service.photo_sources.get(source_id)
|
||||
rows = db.all(
|
||||
"SELECT id FROM source_images WHERE source_id=? ORDER BY created_at DESC,id DESC LIMIT ? OFFSET ?",
|
||||
(source_id, limit, offset),
|
||||
)
|
||||
return {
|
||||
"items": [libraries.picture(row["id"]) for row in rows],
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"total": db.one(
|
||||
"SELECT COUNT(*) AS n FROM source_images WHERE source_id=?",
|
||||
(source_id,),
|
||||
)["n"],
|
||||
}
|
||||
|
||||
@app.post(
|
||||
"/api/v1/source-libraries/{source_id}/import",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def import_picture(source_id: str, payload: ImportInput):
|
||||
return libraries.import_picture(source_id, payload.item_id)
|
||||
|
||||
@app.post(
|
||||
"/api/v1/source-libraries/{source_id}/run",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def run(source_id: str, payload: RunInput, request: Request):
|
||||
with service.lock:
|
||||
return libraries.pipeline(
|
||||
source_id,
|
||||
payload.item_id,
|
||||
deliver=payload.deliver,
|
||||
caller=request.state.client_id,
|
||||
)
|
||||
|
||||
@app.get("/api/v1/generation-plans", dependencies=[Depends(require("images:read"))])
|
||||
def plans(source_id: str):
|
||||
return libraries.plans(source_id)
|
||||
|
||||
@app.post("/api/v1/generation-plans", dependencies=[Depends(require("admin"))])
|
||||
def create_plan(payload: GenerationPlan):
|
||||
return libraries.save_plan(payload)
|
||||
|
||||
@app.put(
|
||||
"/api/v1/generation-plans/{plan_id}", dependencies=[Depends(require("admin"))]
|
||||
)
|
||||
def update_plan(plan_id: str, payload: GenerationPlan):
|
||||
return libraries.save_plan(payload, plan_id)
|
||||
|
||||
@app.delete(
|
||||
"/api/v1/generation-plans/{plan_id}",
|
||||
status_code=204,
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def delete_plan(plan_id: str):
|
||||
libraries.archive(plan_id)
|
||||
|
||||
@app.post(
|
||||
"/api/v1/source-images/{picture_id}/generate",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def generate(picture_id: str, payload: GenerateInput):
|
||||
with service.lock:
|
||||
picture = libraries.picture(picture_id)
|
||||
ids = (
|
||||
[payload.plan_id]
|
||||
if payload.plan_id
|
||||
else [
|
||||
p["id"]
|
||||
for p in libraries.plans(picture["source_id"])
|
||||
if not p["archived"]
|
||||
]
|
||||
)
|
||||
return [libraries.generate(picture_id, plan_id) for plan_id in ids]
|
||||
|
||||
@app.post(
|
||||
"/api/v1/source-images/{picture_id}/preview",
|
||||
dependencies=[Depends(require("images:read"))],
|
||||
)
|
||||
def preview(picture_id: str, payload: GenerationPlan):
|
||||
picture = libraries.picture(picture_id)
|
||||
if picture["source_id"] != payload.source_id:
|
||||
raise FlowError(
|
||||
E.CONFLICT, "Picture and plan must share a source library", 409
|
||||
)
|
||||
data = libraries.render(
|
||||
service.assets.path(picture_id).read_bytes(), picture["caption"], payload
|
||||
)
|
||||
return Response(
|
||||
data, media_type="image/jpeg", headers={"Cache-Control": "no-store"}
|
||||
)
|
||||
|
||||
@app.put(
|
||||
"/api/v1/source-images/{picture_id}/star",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def star(picture_id: str, payload: ImageStar):
|
||||
picture = libraries.picture(picture_id)
|
||||
return set_flags(
|
||||
db, picture["source_id"], picture["item_id"], starred=payload.starred
|
||||
)
|
||||
|
||||
@app.put(
|
||||
"/api/v1/source-images/{picture_id}/exclusion",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def exclude(picture_id: str, payload: ImageExclusion):
|
||||
picture = libraries.picture(picture_id)
|
||||
return set_flags(
|
||||
db, picture["source_id"], picture["item_id"], excluded=payload.excluded
|
||||
)
|
||||
|
||||
@app.delete(
|
||||
"/api/v1/source-images/{picture_id}",
|
||||
status_code=204,
|
||||
dependencies=[Depends(require("images:delete"))],
|
||||
)
|
||||
def delete_picture(picture_id: str):
|
||||
libraries.delete_picture(picture_id)
|
||||
|
||||
@app.delete(
|
||||
"/api/v1/library-versions/{asset_id}",
|
||||
status_code=204,
|
||||
dependencies=[Depends(require("images:delete"))],
|
||||
)
|
||||
def delete_version(asset_id: str):
|
||||
with service.lock:
|
||||
if not db.one(
|
||||
"SELECT asset_id FROM library_versions WHERE asset_id=?", (asset_id,)
|
||||
):
|
||||
raise FlowError(E.NOT_FOUND, "Generated image not found", 404)
|
||||
libraries.check_asset_references(asset_id)
|
||||
libraries.remove_asset(asset_id)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/library-terminals", dependencies=[Depends(require("images:read"))]
|
||||
)
|
||||
def terminals():
|
||||
return libraries.terminals()
|
||||
|
||||
@app.put(
|
||||
"/api/v1/library-terminals/{terminal_id}/binding",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def bind(terminal_id: str, payload: BindingInput):
|
||||
return libraries.bind(terminal_id, payload.source_id, payload.plan_id)
|
||||
|
||||
@app.delete(
|
||||
"/api/v1/library-terminals/{terminal_id}/binding",
|
||||
status_code=204,
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def unbind(terminal_id: str):
|
||||
with service.lock:
|
||||
db.execute(
|
||||
"DELETE FROM terminal_bindings WHERE terminal_id=?", (terminal_id,)
|
||||
)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/library-terminals/{terminal_id}/images",
|
||||
dependencies=[Depends(require("images:read"))],
|
||||
)
|
||||
def terminal_images(
|
||||
terminal_id: str,
|
||||
offset: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
):
|
||||
if not any(t["id"] == terminal_id for t in libraries.terminals()):
|
||||
raise FlowError(E.NOT_FOUND, "Terminal not found", 404)
|
||||
binding = libraries.binding(terminal_id)
|
||||
if not binding:
|
||||
return {"items": [], "total": 0, "offset": offset, "limit": limit}
|
||||
where = "FROM library_versions v JOIN source_images s ON s.id=v.source_image_id WHERE v.plan_id=? AND s.source_id=?"
|
||||
args = (binding["plan_id"], binding["source_id"])
|
||||
rows = db.all(
|
||||
"SELECT v.asset_id,v.source_image_id "
|
||||
+ where
|
||||
+ " ORDER BY v.created_at DESC,v.asset_id LIMIT ? OFFSET ?",
|
||||
(*args, limit, offset),
|
||||
)
|
||||
items = []
|
||||
for row in rows:
|
||||
version = next(
|
||||
v
|
||||
for v in libraries.versions(row["source_image_id"])
|
||||
if v["asset_id"] == row["asset_id"]
|
||||
)
|
||||
delivery = db.one(
|
||||
"SELECT operation_id FROM terminal_deliveries WHERE terminal_id=? AND asset_id=? ORDER BY created_at DESC,operation_id DESC LIMIT 1",
|
||||
(terminal_id, row["asset_id"]),
|
||||
)
|
||||
operation = (
|
||||
db.one(
|
||||
"SELECT id AS operation_id,status,created_at,updated_at FROM operations WHERE id=?",
|
||||
(delivery["operation_id"],),
|
||||
)
|
||||
if delivery and delivery["operation_id"]
|
||||
else None
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
**version,
|
||||
"operation": operation,
|
||||
"delivery_status": operation["status"]
|
||||
if operation
|
||||
else "GENERATED",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"items": items,
|
||||
"total": db.one("SELECT COUNT(*) AS n " + where, args)["n"],
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
@app.post(
|
||||
"/api/v1/library-terminals/{terminal_id}/images/{asset_id}/send",
|
||||
dependencies=[Depends(require("display:write"))],
|
||||
)
|
||||
def send(terminal_id: str, asset_id: str, request: Request):
|
||||
return {
|
||||
"operation_id": libraries.send(
|
||||
terminal_id, asset_id, client_id=request.state.client_id
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user