Rename the import package app to fluksio
A wheel whose top-level module is `app` collides with anything else in a user's venv, so the package that is about to be published takes the name it is published under. Only the Python package moves; the repo, the Docker WORKDIR and the compose project keep theirs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Panels: which dashboards a given device shows.
|
||||
|
||||
A wall tablet in the hall and one in the workshop want different dashboards,
|
||||
and the same dashboard may hang on both. Rather than nesting pages inside a
|
||||
dashboard, a panel names an ordered set of whole dashboards — each keeps its
|
||||
own canvas, its own draft and its own version, and the device switches between
|
||||
them through a rail.
|
||||
|
||||
Stored beside the flows rather than in them, like the alerting configuration:
|
||||
which screen hangs where is the deployment's concern, not any one dashboard's.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.flow.dashboards import DashboardNotFound, DashboardStore
|
||||
from fluksio.flow.schemas import _validate_name
|
||||
|
||||
|
||||
class PanelDef(BaseModel):
|
||||
"""One device, and what it shows."""
|
||||
|
||||
id: str
|
||||
title: str = ""
|
||||
#: Ordered. The first one is what the device opens after pairing, and the
|
||||
#: rail follows this order. A name that no longer resolves is simply a
|
||||
#: dashboard someone deleted; the panel skips it.
|
||||
dashboards: list[str] = Field(default_factory=list)
|
||||
|
||||
@field_validator("id")
|
||||
@classmethod
|
||||
def _check_id(cls, value: str) -> str:
|
||||
return _validate_name(value)
|
||||
|
||||
|
||||
class PanelsConfig(BaseModel):
|
||||
"""Every panel this installation knows about."""
|
||||
|
||||
panels: list[PanelDef] = Field(default_factory=list)
|
||||
|
||||
|
||||
def _path() -> Path:
|
||||
return settings.PANELS_FILE
|
||||
|
||||
|
||||
def read_config() -> PanelsConfig:
|
||||
"""The stored panels, or none. Blocking."""
|
||||
path = _path()
|
||||
if not path.exists():
|
||||
return PanelsConfig()
|
||||
try:
|
||||
return PanelsConfig.model_validate_json(path.read_text())
|
||||
except Exception:
|
||||
# A hand-edited file that no longer parses must not lock everyone out.
|
||||
return PanelsConfig()
|
||||
|
||||
|
||||
def write_config(config: PanelsConfig) -> None:
|
||||
"""Blocking."""
|
||||
path = _path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(config.model_dump_json(indent=2))
|
||||
|
||||
|
||||
def find(panel_id: str) -> PanelDef | None:
|
||||
"""The panel by that id, or None if it was removed.
|
||||
|
||||
Read from disk on every call: this is what makes deleting a panel revoke
|
||||
its credential, so it has to see the current file rather than a cache.
|
||||
"""
|
||||
for panel in read_config().panels:
|
||||
if panel.id == panel_id:
|
||||
return panel
|
||||
return None
|
||||
|
||||
|
||||
def messages_for(panel_id: str, store: DashboardStore) -> set[str]:
|
||||
"""Every message the widgets of this panel's dashboards read or write.
|
||||
|
||||
What a screen is entitled to see, as its own dashboards define it. Read
|
||||
from the published documents, since that is what a panel draws, and empty
|
||||
for a panel that is gone — which is the same answer as "nothing".
|
||||
"""
|
||||
panel = find(panel_id)
|
||||
if panel is None:
|
||||
return set()
|
||||
names: set[str] = set()
|
||||
for dashboard in panel.dashboards:
|
||||
try:
|
||||
defn = store.read(dashboard)
|
||||
except DashboardNotFound:
|
||||
continue
|
||||
for widget in defn.widgets:
|
||||
names.update(widget.messages)
|
||||
if widget.target:
|
||||
names.add(widget.target)
|
||||
return names
|
||||
Reference in New Issue
Block a user