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:
2026-08-21 21:48:05 +02:00
co-authored by Claude Opus 5
parent 97785ee590
commit 60d7ec81c0
170 changed files with 629 additions and 619 deletions
+101
View File
@@ -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