Add optional photo providers, layout studio and scoped integrations
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""Explicit widget mutations, queued once with conservative uncertain-write handling."""
|
||||
|
||||
from fastapi import Depends, Header, Request
|
||||
from pydantic import Field
|
||||
|
||||
from pigway_frame_flow.core.auth import require
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.models import Accepted, StrictModel
|
||||
from pigway_frame_flow.storage.database import encode
|
||||
|
||||
|
||||
class WidgetSelection(StrictModel):
|
||||
cover_id: int | None = Field(default=None, gt=0)
|
||||
args: dict[str, str] = Field(default_factory=dict, max_length=30)
|
||||
|
||||
|
||||
def install_official(app, service):
|
||||
import json
|
||||
|
||||
for device_id, connection in service.registry.official.items():
|
||||
row = service.db.one(
|
||||
"SELECT payload FROM app_config WHERE key=?", ("mock-widget:" + device_id,)
|
||||
)
|
||||
if connection.reader.mock and row:
|
||||
connection.reader.mock_selection = json.loads(row["payload"])["cover_id"]
|
||||
|
||||
def check(device_id, payload):
|
||||
service.registry.get(device_id)
|
||||
connection = service.registry.official.get(device_id)
|
||||
if not connection:
|
||||
raise FlowError(
|
||||
E.UNSUPPORTED_CAPABILITY, "This driver has no official widgets", 422
|
||||
)
|
||||
if not service.settings.mock and not service.settings.write_enabled:
|
||||
raise FlowError(E.WRITE_DISABLED, "Device writes are disabled", 403)
|
||||
view = connection.widgets()
|
||||
if not view.configured:
|
||||
raise FlowError(
|
||||
E.OFFICIAL_AUTH_REQUIRED,
|
||||
"Connect and associate an official account first",
|
||||
401,
|
||||
)
|
||||
if payload.cover_id is not None:
|
||||
widget = next((x for x in view.catalog if x.id == payload.cover_id), None)
|
||||
if not widget:
|
||||
raise FlowError(
|
||||
E.INVALID_REQUEST, "Widget is not in the authenticated catalog"
|
||||
)
|
||||
if set(payload.args) - set(widget.arg_keys):
|
||||
raise FlowError(E.INVALID_REQUEST, "Unknown widget parameter")
|
||||
if any(len(k) > 120 or len(v) > 2000 for k, v in payload.args.items()):
|
||||
raise FlowError(E.INVALID_REQUEST, "Widget parameter is too long")
|
||||
elif payload.args:
|
||||
raise FlowError(
|
||||
E.INVALID_REQUEST, "Cancellation does not accept parameters"
|
||||
)
|
||||
return connection
|
||||
|
||||
@app.post(
|
||||
"/api/v1/devices/{device_id}/widgets/selection",
|
||||
response_model=Accepted,
|
||||
status_code=202,
|
||||
tags=["Official"],
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def select(
|
||||
device_id: str,
|
||||
payload: WidgetSelection,
|
||||
request: Request,
|
||||
idempotency_key: str | None = Header(default=None),
|
||||
):
|
||||
check(device_id, payload)
|
||||
id = service.submit(
|
||||
"official-widget",
|
||||
payload.model_dump(),
|
||||
device_id,
|
||||
request.state.client_id,
|
||||
request.state.request_id,
|
||||
idempotency_key,
|
||||
)
|
||||
return Accepted(operation_id=id, job_id=id, status="accepted")
|
||||
|
||||
def execute(op, payload):
|
||||
selection = WidgetSelection.model_validate(payload)
|
||||
connection = check(op["device_id"], selection)
|
||||
service.update(op["id"], "UPLOADING")
|
||||
service.db.execute(
|
||||
"UPDATE operations SET write_started=1 WHERE id=?", (op["id"],)
|
||||
)
|
||||
with connection.lock:
|
||||
if connection.reader.mock:
|
||||
connection.reader.mock_selection = selection.cover_id
|
||||
service.db.execute(
|
||||
"INSERT OR REPLACE INTO app_config VALUES(?,?)",
|
||||
("mock-widget:" + op["device_id"], encode(selection.model_dump())),
|
||||
)
|
||||
else:
|
||||
connection.reader.rpc.set_widget(
|
||||
connection.reader._robot_id, selection.cover_id, selection.args
|
||||
)
|
||||
connection.reader._cached = None
|
||||
try:
|
||||
view = connection.widgets()
|
||||
except Exception:
|
||||
raise FlowError(
|
||||
E.WRITE_RESULT_UNKNOWN,
|
||||
"Widget write sent but read-only confirmation failed",
|
||||
409,
|
||||
) from None
|
||||
if (
|
||||
not view.current_verified
|
||||
or (view.current.id if view.current else None) != selection.cover_id
|
||||
):
|
||||
raise FlowError(
|
||||
E.WRITE_RESULT_UNKNOWN,
|
||||
"Widget write acknowledged but selection is not confirmed",
|
||||
409,
|
||||
)
|
||||
service.update(op["id"], "COMPLETED", {"cover_id": selection.cover_id})
|
||||
service.db.history(op, "COMPLETED")
|
||||
service.db.event(
|
||||
"display.changed",
|
||||
{
|
||||
"device_id": op["device_id"],
|
||||
"operation_id": op["id"],
|
||||
"cover_id": selection.cover_id,
|
||||
},
|
||||
)
|
||||
|
||||
service.operation_handlers["official-widget"] = execute
|
||||
|
||||
def reconcile(op, acknowledge):
|
||||
id = op["operation_id"]
|
||||
row = service.db.one("SELECT payload FROM operations WHERE id=?", (id,))
|
||||
expected_cover = json.loads(row["payload"])["cover_id"]
|
||||
connection = service.registry.official[op["device_id"]]
|
||||
with connection.lock:
|
||||
connection.reader._cached = None
|
||||
view = connection.widgets()
|
||||
confirmed = (
|
||||
view.current_verified
|
||||
and (view.current.id if view.current else None) == expected_cover
|
||||
)
|
||||
resolved = op["status"] != "UNKNOWN"
|
||||
if op["status"] == "UNKNOWN" and (confirmed or acknowledge):
|
||||
service.update(
|
||||
id,
|
||||
"COMPLETED" if confirmed else "FAILED",
|
||||
{"cover_id": expected_cover} if confirmed else None,
|
||||
)
|
||||
service.db.history(
|
||||
dict(
|
||||
id=id,
|
||||
client_id="admin",
|
||||
request_id=op["request_id"],
|
||||
device_id=op["device_id"],
|
||||
action="reconcile",
|
||||
),
|
||||
"CONFIRMED" if confirmed else "ACKNOWLEDGED",
|
||||
)
|
||||
resolved = True
|
||||
return dict(
|
||||
operation_id=id,
|
||||
resolved=resolved,
|
||||
current_image_id=None,
|
||||
expected_image_id=None,
|
||||
cloud_images=[],
|
||||
)
|
||||
|
||||
service.reconciliation_handlers["official-widget"] = reconcile
|
||||
Reference in New Issue
Block a user