feat: content provenance and complete frame management workflows
This commit is contained in:
@@ -23,6 +23,7 @@ from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.models import (
|
||||
Accepted,
|
||||
Asset,
|
||||
CurrentDisplay,
|
||||
Device,
|
||||
DisplayRequest,
|
||||
ErrorResponse,
|
||||
@@ -31,9 +32,11 @@ from pigway_frame_flow.models import (
|
||||
Health,
|
||||
History,
|
||||
ImageContent,
|
||||
ImageURLConfig,
|
||||
Job,
|
||||
JobInput,
|
||||
Operation,
|
||||
Options,
|
||||
Playlist,
|
||||
PlaylistInput,
|
||||
Profile,
|
||||
@@ -46,8 +49,10 @@ from pigway_frame_flow.models import (
|
||||
TemplateInfo,
|
||||
URLContent,
|
||||
Version,
|
||||
WeatherConfig,
|
||||
WeatherView,
|
||||
)
|
||||
from pigway_frame_flow.providers.weather import MockWeatherProvider
|
||||
from pigway_frame_flow.providers.weather import ConfigurableWeatherProvider
|
||||
from pigway_frame_flow.renderers.images import Assets, convert
|
||||
from pigway_frame_flow.renderers.templates import TemplateRenderer
|
||||
from pigway_frame_flow.scheduler.engine import Scheduler
|
||||
@@ -61,9 +66,17 @@ def create_app(settings=None, start_worker=True):
|
||||
(settings.data_dir / kind).mkdir(parents=True, exist_ok=True)
|
||||
settings.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
db = Database(settings.data_dir / "frameflow.db")
|
||||
import json
|
||||
|
||||
image_config = db.one("SELECT payload FROM app_config WHERE key='image_url_hosts'")
|
||||
if image_config:
|
||||
settings.image_url_hosts = ImageURLConfig.model_validate(
|
||||
json.loads(image_config["payload"])
|
||||
).allowed_hosts
|
||||
registry = DeviceRegistry(settings)
|
||||
assets = Assets(db, settings)
|
||||
renderer = TemplateRenderer(db, settings, MockWeatherProvider(), assets)
|
||||
weather = ConfigurableWeatherProvider(db)
|
||||
renderer = TemplateRenderer(db, settings, weather, assets)
|
||||
service = DisplayService(db, settings, registry, assets, renderer)
|
||||
scheduler = Scheduler(service)
|
||||
seed_client(db, settings.api_token)
|
||||
@@ -737,9 +750,145 @@ def create_app(settings=None, start_worker=True):
|
||||
api_token_configured=bool(settings.api_token),
|
||||
timezone=settings.timezone,
|
||||
device_id=settings.device_id,
|
||||
weather_provider="mock",
|
||||
weather_provider=weather.config().provider,
|
||||
)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/devices/{device_id}/current-content",
|
||||
response_model=CurrentDisplay,
|
||||
tags=["Devices"],
|
||||
dependencies=[Depends(require("devices:read"))],
|
||||
)
|
||||
def current_content(device_id: str):
|
||||
return service.current_content(device_id)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/providers/weather",
|
||||
response_model=WeatherView,
|
||||
tags=["Providers"],
|
||||
dependencies=[Depends(require("templates:read"))],
|
||||
)
|
||||
def weather_view():
|
||||
values = weather.fetch()
|
||||
return WeatherView(config=weather.config(), **values)
|
||||
|
||||
@app.put(
|
||||
"/api/v1/settings/weather",
|
||||
response_model=WeatherConfig,
|
||||
tags=["Settings"],
|
||||
dependencies=[Depends(require("settings:write"))],
|
||||
)
|
||||
def weather_config(payload: WeatherConfig):
|
||||
return weather.configure(payload)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/assets/{asset_id}/source",
|
||||
response_model=dict[str, Any],
|
||||
tags=["Templates"],
|
||||
dependencies=[Depends(require("templates:read"))],
|
||||
)
|
||||
def asset_source(asset_id: str):
|
||||
assets.info(asset_id)
|
||||
row = db.one("SELECT payload FROM asset_sources WHERE asset_id=?", (asset_id,))
|
||||
import json
|
||||
|
||||
return {
|
||||
"source_known": bool(row),
|
||||
"source": json.loads(row["payload"]) if row else None,
|
||||
}
|
||||
|
||||
@app.get(
|
||||
"/api/v1/jobs/{job_id}",
|
||||
response_model=Job,
|
||||
tags=["Automation"],
|
||||
dependencies=[Depends(require("jobs:read"))],
|
||||
)
|
||||
def get_job(job_id: str):
|
||||
job = next((j for j in scheduler.jobs() if j["id"] == job_id), None)
|
||||
if not job:
|
||||
raise FlowError(E.NOT_FOUND, "Job not found", 404)
|
||||
return job
|
||||
|
||||
@app.post(
|
||||
"/api/v1/jobs/{job_id}/run",
|
||||
response_model=Accepted,
|
||||
status_code=202,
|
||||
tags=["Automation"],
|
||||
dependencies=[Depends(require("jobs:write"))],
|
||||
)
|
||||
def run_job(
|
||||
job_id: str,
|
||||
request: Request,
|
||||
idempotency_key: str | None = Header(default=None),
|
||||
):
|
||||
job = JobInput.model_validate(
|
||||
{k: v for k, v in get_job(job_id).items() if k in JobInput.model_fields}
|
||||
)
|
||||
write_guard(job.device_id)
|
||||
if job.source.type == "restore":
|
||||
return submit(request, "restore", {}, job.device_id, idempotency_key)
|
||||
payload = DisplayRequest(
|
||||
device_id=job.device_id,
|
||||
content=job.source,
|
||||
options=Options(
|
||||
duration=job.display_duration,
|
||||
restore_previous=job.restore_previous,
|
||||
priority=job.priority,
|
||||
),
|
||||
)
|
||||
service.validate(payload)
|
||||
return submit(
|
||||
request,
|
||||
"display",
|
||||
payload.model_dump(mode="json"),
|
||||
job.device_id,
|
||||
idempotency_key,
|
||||
)
|
||||
|
||||
@app.post(
|
||||
"/api/v1/jobs/{job_id}/preview",
|
||||
response_model=Accepted,
|
||||
status_code=202,
|
||||
tags=["Automation"],
|
||||
dependencies=[Depends(require("render:write"))],
|
||||
)
|
||||
def preview_job(
|
||||
job_id: str,
|
||||
request: Request,
|
||||
idempotency_key: str | None = Header(default=None),
|
||||
):
|
||||
source = get_job(job_id)["source"]
|
||||
if source["type"] != "template":
|
||||
raise FlowError(
|
||||
E.UNSUPPORTED_CAPABILITY, "Job preview requires a template source", 422
|
||||
)
|
||||
return submit(
|
||||
request, "render", source, get_job(job_id)["device_id"], idempotency_key
|
||||
)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/settings/image-url",
|
||||
response_model=ImageURLConfig,
|
||||
tags=["Settings"],
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def image_url_config():
|
||||
return ImageURLConfig(allowed_hosts=settings.image_url_hosts)
|
||||
|
||||
@app.put(
|
||||
"/api/v1/settings/image-url",
|
||||
response_model=ImageURLConfig,
|
||||
tags=["Settings"],
|
||||
dependencies=[Depends(require("settings:write"))],
|
||||
)
|
||||
def set_image_url_config(payload: ImageURLConfig):
|
||||
settings.image_url_hosts = payload.allowed_hosts
|
||||
db.execute(
|
||||
"INSERT OR REPLACE INTO app_config VALUES('image_url_hosts',?)",
|
||||
(encode(payload.model_dump()),),
|
||||
)
|
||||
return payload
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
def api_docs():
|
||||
return get_swagger_ui_html(
|
||||
@@ -764,6 +913,7 @@ def create_app(settings=None, start_worker=True):
|
||||
def static(filename: str):
|
||||
if filename not in [
|
||||
"app.js",
|
||||
"features.js",
|
||||
"style.css",
|
||||
"i18n.js",
|
||||
"swagger-ui-bundle.js",
|
||||
|
||||
Reference in New Issue
Block a user