plesty.server.view.window

The console’s main window: menu bar, device table, detail pane.

Layout (designed for a bench, not docked):

┌ Bench  Device  View  Help ──────────────────────────────────────────┐
│ ┌ Devices ─────────────────────────────────────────────────────────┐ │
│ │ NAME    SOURCE                 STATE    PORT  PID    STARTED    │ │
│ │ pm100d  plesty-pm100d==0.2.1   running  5551  4242   10:21:03   │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ┌ pm100d ── [Log] [Jobs] [Reports] [Terminal] [Traffic] ───────────┐ │
│ │ === plesty-server start … :: python -m plesty.pm100d …           │ │
│ │ [SERVER] Running at 127.0.0.1:5551                               │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ~/.plesty/server · fleet.yaml · 2 devices · refreshed 10:21:05      │
└────────────────────────────────────────────────────────────────────┘

The window owns a Bench and a Worker; every menu action calls one presenter method on the worker and refreshes when it returns.

Attributes

COLUMNS

CHECK_COLUMN

NAME_COLUMN

HOST_COLUMN

SOURCE_COLUMN

STATE_COLUMN

LOCAL_HOST

TAB_TITLES

LOCAL_TABS

EXPERIMENT_COLUMNS

EXP_RUN_COLUMN

EXP_NAME_COLUMN

EXP_SOURCE_COLUMN

EXP_STATE_COLUMN

EXP_PROGRESS_COLUMN

EXP_NOTE_COLUMN

EXPERIMENT_TABS

RESUMABLE_STATES

LIVE_STATES

DATA_ROWS

VIZ_PLOTS

DATA_DEPTH

DATA_BRANCHES

REFRESH_MS

NOTICE_MS

ICON_SIZE

SOURCE_WIDTH

CLOCK_FORMAT

Classes

MainWindow

The console window.

_StayOpen

Trigger a checkable menu entry on release without closing the menu.

Functions

_card(→ PySide6.QtWidgets.QWidget)

A titled card with optional buttons: the console's layout primitive.

_button(→ PySide6.QtWidgets.QPushButton)

A card-header button wired to slot.

_iconify(→ None)

Turn a card's text buttons into icons, each named in its tooltip (#41).

_relabel(→ None)

Name an icon button label, drawn as icon; does is what it does, kept if None.

_describe(→ None)

Set what button does — or why it cannot — under its name, in the tooltip.

_icon_colour(→ str)

The stroke of an icon on a button of role, as the stylesheet colours its text.

_theme_tokens(→ dict[str, str])

_icon(→ PySide6.QtGui.QIcon)

The Lucide icon name (assets/icons), stroked colour, faint when disabled.

_shown(→ str)

What the device table shows, for the status line: all fleets, or each by name.

_short(→ str)

A path with the home directory abbreviated to ~.

_source_column(→ None)

Size a Source column by hand, never by its contents (#25).

_clock(→ str)

YY-MM-DD,HH:MM:SS of an ISO timestamp (local time), or "".

_opened_paths() → set[tuple[str, Ellipsis]])

Every expanded row of a tree, as the path of labels leading to it.

_reopen() → None)

Open again every row whose path is in opened.

_embedded_view() → Any)

Build an experiment's own views as a widget, to dock in the console.

_own_view(→ Any)

Build a view of a run out of what the console itself can read.

_plottable(→ list[str])

The fields of a run's records that are worth drawing against the step.

_written(→ str)

When a report was written and how big it is, as one readable phrase.

_unwrap(→ tuple[str, str, dict[str, Any]])

Return how one record reads: its step, its summary, and what to open.

_fields_of(→ str)

One line naming what a document holds, for the row that opens it.

_fill_tree(→ None)

Add value under parent, a level per level of the document.

_leaf(→ str)

How one value reads on its own row.

_experiment_state(→ str)

The run state as the table shows it.

_state_tone(→ str)

Which palette tone a run state is coloured with.

_progress_text(→ str)

One line saying how far a run has got, and what it is doing now.

_asked_text(→ str)

What a run has been told to do and has not done yet, or "".

_tone_brush(→ Any)

The palette colour for a state, looked up from the theme on the window.

Module Contents

plesty.server.view.window.COLUMNS = ('Fleet', 'Name', 'Host', 'Source', 'State', 'Port', 'PID', 'Started')
plesty.server.view.window.CHECK_COLUMN = 0
plesty.server.view.window.NAME_COLUMN = 1
plesty.server.view.window.HOST_COLUMN = 2
plesty.server.view.window.SOURCE_COLUMN = 3
plesty.server.view.window.STATE_COLUMN = 4
plesty.server.view.window.LOCAL_HOST = 'this bench'
plesty.server.view.window.TAB_TITLES = ('Log', 'Jobs', 'Reports', 'Terminal', 'Traffic')
plesty.server.view.window.LOCAL_TABS = ('Log', 'Jobs', 'Reports', 'Terminal', 'Traffic')
plesty.server.view.window.EXPERIMENT_COLUMNS = ('Run', 'Experiment', 'Source', 'State', 'Progress', 'Started', 'Ended', 'Operator', 'Note')
plesty.server.view.window.EXP_RUN_COLUMN = 0
plesty.server.view.window.EXP_NAME_COLUMN = 1
plesty.server.view.window.EXP_SOURCE_COLUMN = 2
plesty.server.view.window.EXP_STATE_COLUMN = 3
plesty.server.view.window.EXP_PROGRESS_COLUMN = 4
plesty.server.view.window.EXP_NOTE_COLUMN = 8
plesty.server.view.window.EXPERIMENT_TABS = ('Log', 'Data', 'Report', 'Visualization')
plesty.server.view.window.RESUMABLE_STATES = ('canceled', 'aborted')
plesty.server.view.window.LIVE_STATES = ('running', 'paused')
plesty.server.view.window.DATA_ROWS = 50
plesty.server.view.window.VIZ_PLOTS = 6
plesty.server.view.window.DATA_DEPTH = 6
plesty.server.view.window.DATA_BRANCHES = 32
plesty.server.view.window.REFRESH_MS = 1000
plesty.server.view.window.NOTICE_MS = 20000
class plesty.server.view.window.MainWindow(bench: plesty.server.presenter.Bench, title: str = 'PLESTY bench', version: str = '')

Bases: PySide6.QtWidgets.QMainWindow

The console window.

Build the window around bench.

Parameters:
notice
bench
version = ''
worker
_views: list[plesty.server.presenter.DeviceView] = []
_starting: set[str]
_pending_select: str | None = None
_control: Any = None
_device_controls: dict[str, Any]
_update_to: str | None = None
_timer
_build_menu() → None
Return type:

None

_build_terminal() → PySide6.QtWidgets.QWidget

The Terminal tab: run a command against the selected device’s own venv.

Which interpreter a command runs against is the whole point — plesty check, plesty init and plesty field-test have to be the device environment’s, and finding that path by hand in a system terminal is what this replaces. The venv is named above the output so it is never in doubt which one is about to run.

Return type:

PySide6.QtWidgets.QWidget

_build_experiment() → PySide6.QtWidgets.QWidget

The Experiment tab: what is declared, and what the selected one is doing.

The same two-card shape as the Devices tab — a table of what this bench holds above, the detail of the selected one below — so an operator who has learned one half of the window has learned the other.

Return type:

PySide6.QtWidgets.QWidget

_build_data() → PySide6.QtWidgets.QWidget

The Data pane: what each step committed, as a tree.

A record is a document, not a line — a spectrum step commits an angle, a file name and whatever the analysis added, and one of those may be a list of fitted lines. Flattened to one JSON line per record it is unreadable at exactly the moment it matters. Each step is a row that opens, and so is every document inside it.

Return type:

PySide6.QtWidgets.QWidget

_build_report() → PySide6.QtWidgets.QWidget

The Report pane: what was written about this run, and writing more.

An operator’s note belongs to the run it was written on, so it is read where the run is read rather than behind a button on the toolbar. What produced the run is here too — which plesty distributions at which versions — because that is the other thing a reader asks of a run they did not make themselves.

Return type:

PySide6.QtWidgets.QWidget

_build_visualization() → PySide6.QtWidgets.QWidget

The Visualization pane: the experiment’s own views, drawn here.

A module declares its viewer in its manifest, and when that module is installed beside this console the views are imported and docked into this pane. When it is not — the usual case, since an experiment is installed in an environment of its own — they cannot be drawn here at all, and the pane offers to open them in the module’s environment instead. It says which of the two it is doing, because an empty half-tab reads as something broken.

Return type:

PySide6.QtWidgets.QWidget

_add(menu: PySide6.QtWidgets.QMenu, text: str, slot: collections.abc.Callable[[], Any], shortcut: Any = None) → PySide6.QtGui.QAction
Parameters:
  • menu (PySide6.QtWidgets.QMenu)

  • text (str)

  • slot (collections.abc.Callable[[], Any])

  • shortcut (Any)

Return type:

PySide6.QtGui.QAction

_build_body() → None
Return type:

None

property selected_view: plesty.server.presenter.DeviceView | None

The selected device, or None.

Keyed by the row rather than by the name: two benches may each declare a pm100d, and the row is what says which of them is meant. Every repaint fills the table from self._views in order, so the index is the same key on both sides.

Return type:

plesty.server.presenter.DeviceView | None

property selected: str | None

Name of the selected device, or None.

Return type:

str | None

property selected_host: str

The node the selected device runs on, empty for this bench’s own.

Return type:

str

_select_after_repaint(name: str) → None

Select name once the table has been rebuilt.

A renamed device has no row under its new name yet — the refresh that follows the call is what creates it — so the selection is remembered instead of applied.

Parameters:

name (str)

Return type:

None

select(name: str, host: str = '') → None

Select the row of name on host (no-op when absent).

Parameters:
  • name (str)

  • host (str)

Return type:

None

refresh() → None

Re-read the devices — this bench’s and its nodes’ — and repaint them.

Return type:

None

refresh_experiments() → None

Re-read what every declared experiment is doing, and repaint it.

A status is a read of this bench’s own run directory, so this costs a few small file reads however long a measurement has been going and cannot disturb one.

Return type:

None

_read_experiments() → list[dict[str, Any]]

Read every run of every declared experiment, off the GUI thread.

Return type:

list[dict[str, Any]]

_show_experiments(rows: list[dict[str, Any]]) → None

Repaint the run table, keeping the selection where it was.

A run just created takes the selection instead, once a repaint that holds it lands: selecting it straight after asking for the refresh looked for a row that was not drawn yet (#31).

Parameters:

rows (list[dict[str, Any]])

Return type:

None

_show_banner() → None

Say what the bench is doing: each run going on, how far, and its devices (#41).

Read from what the two tables last drew — no read of its own — so it is as current as they are and costs the refresh nothing.

Return type:

None

_select_run(name: str, run: str | None) → None

Put the selection back on one run, when it still has a row.

Parameters:
  • name (str)

  • run (str | None)

Return type:

None

property _selected_row: dict[str, Any]

The table row that is selected, as it was last read; empty when none.

Return type:

dict[str, Any]

property selected_run: tuple[str, str | None] | None

The selected experiment and its run id, or None.

Return type:

tuple[str, str | None] | None

property selected_experiment: str | None

The name of the experiment whose run is selected, or None.

Return type:

str | None

property selected_experiment_status: dict[str, Any]

The last-read status of the selected run; empty when none.

Return type:

dict[str, Any]

property _selected_is_loaded: bool

Whether the selected run was loaded from elsewhere rather than run here.

Return type:

bool

_experiment_selected() → None

A different run was picked: the panes follow it.

Return type:

None

_update_experiment_buttons() → None

Enable each control for what the selected experiment is actually doing.

Return type:

None

_gate_experiment_buttons() → None

Enable each button for what the selected experiment is actually doing.

Return type:

None

_refresh_experiment_detail() → None

Repaint whichever experiment pane is in front.

Return type:

None

_show_not_started(name: str, run: str) → None

Empty every pane, for a run that has produced nothing yet.

A created run has a name, a configuration and a place to write, and nothing else — no log, no records, no report, no views. Saying that in each pane is the honest answer; showing the previous run’s is not.

Parameters:
  • name (str) – The experiment.

  • run (str) – The run’s id, or "" when the experiment has never run.

Return type:

None

_tail_experiment_log(name: str) → None

Follow the log of the job that ran the selected run.

The experiment’s newest job is not the selected run’s: picking a run created a minute ago, or an older one from the list, showed whatever the last measurement had printed — a log that belongs to a different run and says so nowhere.

Parameters:

name (str)

Return type:

None

_read_experiment_log(key: tuple[str, str, str], ended: bool) → Any

Read the next part of the shown log, off the GUI thread.

Reads what the pane is at when the read runs, not when it was asked for: a read asked for during the last one starts where that one ended.

Returns:

("end", key, text, start, end) for the first read of a log, ("more", key, text, offset, ended) after it, or None when the pane has moved to another log meanwhile.

Parameters:
  • key (tuple[str, str, str])

  • ended (bool)

Return type:

Any

_show_experiment_log(read: Any) → None

Put a read of the log on screen, without moving what is being read.

The first read opens on the end of the log, as it stood when the run finished or as far as a running one has got. After it, what a running run adds is appended, and the view follows only when it was already at the end. An ended run, once read to its end, is not read again (#28).

Parameters:

read (Any)

Return type:

None

_log_scrolled(value: int) → None

At the top of the shown part of a long log: load the part before it.

Parameters:

value (int)

Return type:

None

_prepend_experiment_log(read: tuple[tuple[str, str, str], str, int]) → None

Put an earlier part of the log above the shown one, keeping the view still.

Parameters:

read (tuple[tuple[str, str, str], str, int])

Return type:

None

_show_experiment_data(name: str) → None

Show the newest committed records — the raw data the run has produced.

Parameters:

name (str)

Return type:

None

_paint_experiment_data(records: list[dict[str, Any]]) → None

Render each record as a row that opens, keeping what was open open.

The tree is rebuilt on every refresh, so what the operator had open has to be reopened — at every level. Restoring only the top level made a nested row fall shut about a second after it was opened, which is worse than not remembering at all.

Parameters:

records (list[dict[str, Any]])

Return type:

None

_show_experiment_report(name: str, status: dict[str, Any]) → None

Show what was written about this run, and what produced it.

Parameters:
  • name (str)

  • status (dict[str, Any])

Return type:

None

_show_visualization(name: str, status: dict[str, Any]) → None

Draw the experiment’s own views here, or offer to open them elsewhere.

Parameters:
  • name (str)

  • status (dict[str, Any])

Return type:

None

_embed_views(name: str, status: dict[str, Any], manifest: Any) → None

Dock the module’s views in this pane when they can be imported here.

An experiment is installed in an environment of its own, so its viewer usually cannot be imported into this console at all — that is the ordinary case, not a failure, and the pane then offers to open the views in the environment that does have them.

Parameters:
  • name (str)

  • status (dict[str, Any])

  • manifest (Any)

Return type:

None

_add_window_menu(shell: Any, title: str) → None

Put “open in a window” on the views’ own menu bar.

Where the shell already keeps Panels and Layout, rather than a row of console chrome above the plots — that was tried and only took height from them. The shell is a QMainWindow, so this is its menu bar and the action sits beside the ones it adds itself.

Parameters:
  • shell (Any)

  • title (str)

Return type:

None

popout_views() → None

Float the whole viewer out of this pane into a window of its own.

The console’s pane is sized for the console, and a plot worth reading closely wants a screen. This takes the whole shell out — every panel it has, its menus and its layout — rather than floating panels one by one, so what lands in the window is the viewer as the module built it.

Return type:

None

dock_views() → None

Put the views back in this pane, wherever their window ended up.

Return type:

None

_offer_viewer(name: str, status: dict[str, Any], module: str, failure: str = '') → None

Say the views cannot be drawn here, and why; offer the module’s own window.

Parameters:
  • name (str) – The experiment.

  • status (dict[str, Any]) – The run’s status.

  • module (str) – The viewer the module declares; "" when none.

  • failure (str) – Why loading it here failed, when it was tried.

Return type:

None

_clear_embedded_views() → None

Take down any docked views and show the offer again.

Views out in a window of their own go with them: they are this run’s, and the pane is being cleared because the run they belong to is not the one shown any more.

Return type:

None

run_experiment() → None

Start a run of the selected experiment, or continue the one that stopped.

Return type:

None

_start_run(name: str, resume: str | None, start: str | None) → None

Start a run of name — continuing resume, or under the created name start.

Parameters:
  • name (str)

  • resume (str | None)

  • start (str | None)

Return type:

None

_experiment_started(name: str, job: plesty.server.model.Job) → None

Note the run and follow its log.

Parameters:
Return type:

None

property selected_run_id: str | None

The run the table has selected, for the controls that drive one.

Asking for a run by name and getting whichever is newest is the same bug three times over: pause, stop and abort each wrote their request into the newest run directory on disk rather than into the run the operator was looking at. With one run they are the same directory, which is why it held together until a second run existed — and then Pause silently paused something that had finished hours ago.

Return type:

str | None

pause_experiment() → None

Hold the run before its next step, or let a held one go on.

Return type:

None

stop_experiment() → None

Ask the run to finish the step in flight and then stop.

Return type:

None

abort_experiment() → None

Ask the run to stop without finishing the step in flight.

Return type:

None

add_run_note() → None

Experiment → Add note…: one more note on the selected run, open or ended (#40).

Return type:

None

report_experiment() → None

Record something on the running measurement.

Return type:

None

create_run() → None

Name a run of a declared experiment, without starting it.

Creating and starting are separate: a measurement takes hours and moves the rig, so the name, the configuration and the plan are worth seeing in the table before anything moves. Run starts it.

Return type:

None

_declared_then_create(name: str, run_id: str, config: str, operator: str = '', note: str = '') → None

Repaint what was just declared, then create the run that was asked for.

Parameters:
  • name (str)

  • run_id (str)

  • config (str)

  • operator (str)

  • note (str)

Return type:

None

_create_experiment_run(name: str, run_id: str, config: str, operator: str = '', note: str = '') → None

Create the run and put it on the table.

Parameters:
  • name (str) – The experiment as this bench declares it.

  • run_id (str) – What to call the run; the bench names it when this is empty.

  • config (str) – The configuration file, relative to the checkout.

  • operator (str) – Who measures it (#40).

  • note (str) – Why; its first note.

Return type:

None

duplicate_run() → None

Create a run repeating the selected one, with the settings it used.

Return type:

None

_run_created(row: dict[str, Any]) → None

Repaint, and put the selection on the run that was just named.

Parameters:

row (dict[str, Any])

Return type:

None

load_run() → None

Read a run measured elsewhere, from the directory it was written to.

A run carries what is needed to read it — the plan, the journal, the records, and the versions that produced it — so one measured on another bench, or restored from a backup, is openable here without declaring the experiment it came from. It is read, not adopted: nothing about it can be driven from this console.

Return type:

None

_forget_loaded(path: str) → None

Take a loaded run off the table, leaving its directory alone.

Parameters:

path (str)

Return type:

None

_run_loaded(row: dict[str, Any]) → None

Put a loaded run in the table, replacing an earlier read of it.

Parameters:

row (dict[str, Any])

Return type:

None

_experiment_asked(name: str, what: str) → None

Say what was asked of the run, and repaint.

Parameters:
  • name (str)

  • what (str)

Return type:

None

watch_experiment() → None

Open the module’s own viewer on the run being shown.

Return type:

None

configure_experiment() → None

Edit what the selected run is configured with — or, with no run, the experiment.

Configuring changes the selected run alone: a run created and waiting is started with its own copy, so the module’s file and the runs beside it stay as they were. The row of an experiment that has no run yet configures the experiment, which is what Create starts from.

Return type:

None

_open_experiment_config(name: str, manifest: Any, config: str, spec: Any, run: dict[str, Any] | None = None) → None

Open the settings dialog on what the module declared.

Parameters:
  • name (str)

  • manifest (Any)

  • config (str)

  • spec (Any)

  • run (dict[str, Any] | None)

Return type:

None

export_run_report() → None

Compile the selected run into one HTML file, where the operator says.

The run directory is the measurement; this is a reading of it that can be sent to somebody who cannot open a run directory — a supervisor, a collaborator, a paper’s supplementary material.

Return type:

None

report_problem() → None

Describe what went wrong, with what the bench can say about itself.

Always available: a report is wanted most when something is broken, so this asks nothing of the bench being in a good state.

Return type:

None

check_for_updates() → None

Show the installed and the newest version, and update to it when asked (#29).

The update is not run from here: the console is the environment being replaced. Asking for it closes the window — through the ordinary close, which asks before stopping running servers and jobs — and the helper installs once this process has gone, then starts the console again.

Return type:

None

show_about() → None

Help ▸ About: this version, how it was installed, and the last update (#29).

Return type:

None

offer_welcome_at_start() → None

Open the welcome on the first start of a bench that holds nothing (#43).

Return type:

None

open_welcome() → None

Help → Welcome: install the demo, create its first run, and run it (#43).

One PC, no lab hardware: the demo experiment’s setup brings its own stage simulator and a spectrometer that replays measured spectra, all on this bench. The install is a job like any other; when it has ended well, the experiment is declared, the run created and started.

Return type:

None

check_for_updates_at_start() → None

Ask PyPI for a newer bench, off the GUI thread, and say so in one line (#39).

Benches ran old versions for weeks because the only way to learn of a release was to open Help ▸ Check for updates…. At most once a day (startup_update()); never for a source checkout, which cannot update itself and is never “the newest”; and not at all when Server ▸ Configure… turned it off. No network, or a check that fails, says nothing.

Return type:

None

_offer_update(latest: str | None) → None

Show the update line for latest; nothing when there is none.

Parameters:

latest (str | None)

Return type:

None

report_last_update() → None

Say how the last update went, once, on the start that follows it (#29).

Return type:

None

declare_experiment() → None

Declare an experiment on this bench, without creating a run of it.

The menu’s way in, for declaring ahead of time. Create does its own declaring, so this is not on the path from installing a module to running it — it is there for someone setting a bench up.

The installed modules that may be experiments are offered to choose between, as the device dialog does: the bench knows where it installed each one from, so choosing one fills in the repository, the ref and a proposed name rather than asking for them again.

Return type:

None

_experiment_declared(name: str) → None

Repaint and select what was just declared.

A newly declared experiment has no run, so its row is the one with no run id — which is what _select_run() is asked for.

Parameters:

name (str)

Return type:

None

remove_run() → None

Delete the selected run, or take a loaded one off the table.

A row is a run, so the button beside it removes that run — dropping the whole experiment from here would take every run with it, which is what the Experiment menu is for.

A run loaded from elsewhere is only forgotten: its directory belongs to whoever measured it, and this console is reading it, not keeping it.

Return type:

None

open_run() → None

Open the selected run’s directory in the desktop’s file manager.

The run directory is the measurement — the records, the journal, the analyses and now the report — and everything this pane shows is a reading of it. An operator who wants the files themselves, to copy one out or to attach it to something, should not have to retype a path the console already knows.

Return type:

None

remove_experiment() → None

Drop the selected experiment from the fleet, leaving its runs.

Return type:

None

install_experiment() → None

Install the selected experiment’s module as a job.

Return type:

None

update_experiment() → None

Pull and reinstall the selected experiment’s module as a job.

Return type:

None

_experiment_install(update: bool) → None

Install or update the module of the selected experiment.

The installer is keyed by the package and knows nothing of devices or experiments, so this is the device path unchanged.

Parameters:

update (bool)

Return type:

None

_show_devices(views: list[plesty.server.presenter.DeviceView]) → None
Parameters:

views (list[plesty.server.presenter.DeviceView])

Return type:

None

_selection_changed() → None
Return type:

None

_update_buttons() → None

Enable the device buttons for the selected row and its state.

Return type:

None

_refresh_detail() → None
Return type:

None

_set_tabs_live(live: collections.abc.Sequence[str]) → None

Enable the detail tabs that have something to read, disable the rest.

A disabled tab and its pane, which says which bench to read it on, are between them enough. A marker on every tab name was a second thing to learn for a state the tab already shows.

Parameters:

live (collections.abc.Sequence[str])

Return type:

None

_show_log(text: str) → None
Parameters:

text (str)

Return type:

None

_show_jobs(jobs: list[plesty.server.model.Job]) → None
Parameters:

jobs (list[plesty.server.model.Job])

Return type:

None

_build_traffic() → PySide6.QtWidgets.QWidget

The Traffic tab: what this device’s server has served.

The console cannot watch the wire — the server is another process — so this reads the records plesty-lib writes to the server log. Metadata only: what was asked, of what, how long it took and how it ended. A result never appears here, because a result can be a spectrum.

Return type:

PySide6.QtWidgets.QWidget

_show_traffic(result: tuple[str, list[Any]]) → None

Fill the Traffic tab, newest last, and colour what went wrong.

An empty pane is ambiguous — a server that has served nothing looks exactly like one whose plesty-lib is too old to write records — so the caption says both rather than leaving a blank table to read as broken.

A result for a device that is no longer selected is dropped: it was read before the selection moved, and painting it here would put one device’s traffic under another one’s name.

Parameters:

result (tuple[str, list[Any]])

Return type:

None

_show_reports(reports: list[Any]) → None
Parameters:

reports (list[Any])

Return type:

None

_note(line: str) → None

Show a progress line on its own row, and keep it.

Its own row, because a failure is standing information the operator has to act on and progress is not: neither may hide the other, which is also why this is not QStatusBar.showMessage — that hides every non-permanent widget, the standing failure included.

It clears itself after NOTICE_MS, because “installing …” an hour later is a lie about what the bench is doing. Nothing is lost — View ▸ Messages… has every line of the session.

Parameters:

line (str)

Return type:

None

clear_notice() → None

Drop the progress line (the timer, and any action that finishes).

Return type:

None

property notice_text: str

The progress line currently shown (empty when none).

Return type:

str

_view_choices() → tuple[list[str], list[str]]

What View → Fleet offers: this bench’s fleets, and the other benches.

Return type:

tuple[list[str], list[str]]

_fill_fleet_view_menu() → None

View → Fleet: All, then every fleet of this bench and every other bench.

Each is ticked on its own, so any mix shows; All ticks or clears them all. Filled when it opens, since fleets and benches come and go under it.

Return type:

None

_sync_view_checks() → None

Tick the View → Fleet entries that are shown.

Return type:

None

_toggle_view(data: tuple[str, str], checked: bool) → None

One View → Fleet entry ticked or cleared.

Parameters:
  • data (tuple[str, str])

  • checked (bool)

Return type:

None

set_view(fleets: frozenset[str] | None, benches: frozenset[str] | None) → None

Show the devices of fleets of this bench and of the other benches.

None is every one, including those added later; an empty set is none.

Parameters:
  • fleets (frozenset[str] | None)

  • benches (frozenset[str] | None)

Return type:

None

show_messages() → None

View → Messages…: every progress line this session, newest last.

Return type:

None

_show_message(text: str, tone: str) → None
Parameters:
  • text (str)

  • tone (str)

Return type:

None

show_error(message: str) → None

Report a failure in the window, where it costs the operator nothing to read.

Not a modal. What fails here fails asynchronously — an install takes minutes and the operator is often elsewhere — and a modal blocks the Qt event loop: the device table stops refreshing, an open job log stops tailing, and the console is frozen until somebody clicks it away. The message stays until it is dismissed or another replaces it.

Parameters:

message (str)

Return type:

None

show_outcome(message: str) → None

Report something that ended as it should, where a failure would be reported.

Not a modal, for the reason show_error() gives, and not a progress line (_note()), which clears itself: an install that ended while the operator was away is still news when they are back. The next message replaces it.

Parameters:

message (str)

Return type:

None

clear_error() → None

Hide the standing message (the Dismiss button, and any action that succeeds).

Return type:

None

property error_text: str

What the window is currently reporting as failed (empty when nothing).

Return type:

str

_call(action: collections.abc.Callable[[], Any], then: collections.abc.Callable[[Any], None] | None = None) → None

Run a presenter call on the worker; refresh afterwards, report on error.

Parameters:
  • action (collections.abc.Callable[[], Any])

  • then (collections.abc.Callable[[Any], None] | None)

Return type:

None

_require() → str | None
Return type:

str | None

_item_changed(item: PySide6.QtWidgets.QTableWidgetItem) → None

A check box toggled: include/exclude the device from the working fleet.

Parameters:

item (PySide6.QtWidgets.QTableWidgetItem)

Return type:

None

_fleets_dir() → pathlib.Path

Where the fleet file dialogs start: <home>/fleets/ (created on demand).

Return type:

pathlib.Path

new_fleet(path: str | None = None) → None

File → New fleet…: an empty fleet file, made current (devices are added to it).

Parameters:

path (str | None)

Return type:

None

open_fleet(path: str | None = None) → None

File → Open fleet…: replace the declarations with another fleet file.

Parameters:

path (str | None)

Return type:

None

_swap_fleet(path: str, create: bool) → None

Stop the running devices, then make path the current fleet.

The servers of the fleet being left behind would otherwise keep running out of reach of the console, holding their ports against the devices of the fleet being opened. They are named before they are stopped, and Cancel leaves the current fleet as it is. With nothing running there is nothing to say, and the fleet opens straight away.

Parameters:
  • path (str)

  • create (bool)

Return type:

None

save_fleet_as(path: str | None = None) → None

File → Save working fleet as…: the checked devices to a new fleet file.

Parameters:

path (str | None)

Return type:

None

_fleet_changed() → None
Return type:

None

edit_device_config() → None

Edit → Device config…: declaration + .env of the highlighted device.

The catalogue is fetched on the worker first, so the module’s repository URL and latest tag are in the drop-downs when the operator switches a release declaration to a repository one.

Return type:

None

edit_fleet_file() → None

Edit → Fleet file…: the YAML of the whole fleet.

Return type:

None

run_in_venv() → None

Terminal → Run: start the typed command in the selected device’s environment.

One at a time: the pane shows one command’s output, and a second command started underneath it would interleave into the same view with no way to tell them apart.

Return type:

None

stop_venv_command() → None

Terminal → Stop: cancel the command running in the pane.

Return type:

None

_refresh_terminal() → None

Name the environment, and append whatever the running command has written.

Return type:

None

_show_venv(label: str) → None

Say which environment the next command runs against, or why there is none.

Parameters:

label (str)

Return type:

None

configure_agent() → None

Server → Configure…: the endpoint, token and workers this bench serves on.

Saving does not move a running agent: its clients connected to the endpoint it bound, with the token it was started with. The operator is told to restart it rather than have it change underneath them.

Return type:

None

toggle_agent() → None

Server → Start/Stop agent: serve plesty-bench clients from this console, or stop.

The console is the long-running process a lab PC already has open, so the agent runs on a thread of it — the same service fleet run --agent starts, with no second process to supervise.

Return type:

None

stop_orphans() → None

Server → Stop servers outside this fleet: end what no fleet row can reach (#24).

Return type:

None

_show_agent() → None

Name the action for what it would now do, and repaint the status line.

Return type:

None

_show_role() → None

Offer the bench list only on a master, and say why when it is not offered.

Return type:

None

edit_peers() → None

Server → Benches…: the other benches this master shows and drives.

Return type:

None

open_catalogue() → None

Hub → Browse…: fetch the Hub’s list of modules, install one.

Only the module: the fleet is not touched here. Declaring an instance of it is Device ▸ Add device…, which offers what the bench holds.

Return type:

None

load_local_module() → None

Hub → Load from…: a module’s repository on this machine, loaded where it lives.

Like an install, it stocks the bench and declares nothing; unlike one, the folder stays where it is and only its path and what it is are kept.

Return type:

None

open_installed_modules() → None

Hub → Installed modules…: every module on the bench, declared or not, to uninstall.

Return type:

None

declare_device() → None

Device → Add device…: declare an instance of an installed module.

With no device module installed the user is sent to the Hub instead. An installed experiment is not offered: declared here, it would be served as a device.

Return type:

None

remove_device() → None

Edit → Remove from fleet (also the Remove button).

Return type:

None

start_device() → None

Device → Start (one start at a time per device).

Return type:

None

stop_device() → None

Device → Stop.

Return type:

None

restart_device() → None

Device → Restart.

Return type:

None

_run_then_reread(action: collections.abc.Callable[[], Any], name: str, host: str) → None

Run a stop or restart, then let the held panels of name read again.

On failure as well as success: a panel left held would refuse every call for good.

Parameters:
  • action (collections.abc.Callable[[], Any])

  • name (str)

  • host (str)

Return type:

None

_open_panel_windows(name: str, host: str) → list[Any]

The control windows that may hold a panel of name, open ones only.

Parameters:
  • name (str)

  • host (str)

Return type:

list[Any]

_hold_panels(name: str, host: str = '') → None

Have every open panel of name stop calling a server that is going away (#22).

Parameters:
  • name (str)

  • host (str)

Return type:

None

_reread_panels(name: str, host: str = '') → None

Have every open panel of name read the device it now runs as (#22).

A device can be docked in its own control window and on the wall at once; both read again.

Parameters:
  • name (str)

  • host (str)

Return type:

None

install_device() → None

Device → Install: starts the job and opens its dialog.

A node’s install is started there and left to run: this console keeps no job list for a device it does not host, so what comes back is the job’s identifier in a status line rather than a dialog following it.

Return type:

None

update_device() → None

Device → Update: starts the job and opens its dialog.

Return type:

None

uninstall_device() → None

Device → Uninstall.

Return type:

None

clear_device_log() → None

Clear log: empty the selected device’s log after confirming.

Asked first, because it is not undoable and it takes the Traffic tab with it — the traffic history is read back out of this same file, and an operator who wanted only one of the two would be surprised by the other. A running server keeps writing into the emptied file.

Return type:

None

control_device() → None

Device → Control…: open this device’s controls, and only this device’s.

A window of its own, named for it. The bench’s Control window is a wall that keeps every panel docked in it, so opening one device’s controls there showed the last device’s beside them — Control on the table is what opens the wall.

Return type:

None

device_control_window(host: str, name: str) → Any

Return the window for one device’s controls, creating it once.

One device, one window. The bench’s own Control window is a shell that keeps every panel docked in it, which is what makes it a wall of instruments — and what made Control on the detail card open the last device’s panel beside this one’s. Asking for a single device’s controls gets that device and nothing else.

Parameters:
  • host (str) – The bench it runs on, empty for this one.

  • name (str) – The device.

Returns:

The ControlWindow for it.

Return type:

Any

control_window() → Any

Return the bench’s Control window, creating it the first time.

This one is the wall: every running device docked together, which is what Control on the device table opens.

Returns:

The ControlWindow.

Return type:

Any

show_control_window() → None

View → Control window: bring it up, showing every running device.

It used to open whatever was already there, which on first use was nothing: a Shell with no panels has no central widget, so an operator with two running devices opened the window and was shown an empty frame. Opening it now means “control this bench” — every device whose server is answering gets a panel, and the ones that cannot are named in the status bar rather than silently missing.

A device already open keeps its panel, so this can be used to pick up devices started since, without disturbing the arrangement.

Return type:

None

_close_control_window() → None

Close every control window, releasing the connections they hold.

The bench’s own, and one per device opened from a detail card: each holds a link to a device server, and a socket released after its server has gone waits out its own timeout on the way down.

Return type:

None

field_test_device() → None

Device → Field test…: configure, start the job, open its dialog.

Return type:

None

fleet_up() → None

Device → Fleet up (the working fleet).

Return type:

None

fleet_down() → None

Device → Fleet down (the working fleet).

Return type:

None

show_jobs() → None

View → Jobs…: the Jobs tab of the detail pane.

Return type:

None

_open_job(job: plesty.server.model.Job, then: collections.abc.Callable[[plesty.server.model.Job], None] | None = None, close_when_done: bool = False) → None

Follow job in a window; then is called with it once it has ended.

close_when_done closes the window once the job has ended well — for a job that is a step on the way to something else, like the welcome’s install (#43). One that failed stays open, its log the reason.

Parameters:
Return type:

None

_announce(job: plesty.server.model.Job) → None

Say how an install or update ended, in the window and not only in its log.

Parameters:

job (plesty.server.model.Job)

Return type:

None

_open_job_row(item: PySide6.QtWidgets.QTableWidgetItem) → None
Parameters:

item (PySide6.QtWidgets.QTableWidgetItem)

Return type:

None

_context_menu(pos: Any) → None
Parameters:

pos (Any)

Return type:

None

closeEvent(event: Any) → None

Closing the console closes what it manages: servers and running jobs.

Asks first when something is running — an accidental close must not take a bench down — then stops everything synchronously so nothing is left unmanaged behind a window that is gone.

Parameters:

event (Any)

Return type:

None

_hand_over_update(version: str) → None

Start the helper that installs version once this console has exited.

Parameters:

version (str)

Return type:

None

plesty.server.view.window._card(title: str | PySide6.QtWidgets.QLabel, body: PySide6.QtWidgets.QWidget, buttons: list[PySide6.QtWidgets.QWidget] | None = None, own_row: bool = False) → PySide6.QtWidgets.QWidget

A titled card with optional buttons: the console’s layout primitive.

Parameters:
  • title (str | PySide6.QtWidgets.QLabel) – Card title (a label is reused as-is).

  • body (PySide6.QtWidgets.QWidget) – The card’s content.

  • buttons (list[PySide6.QtWidgets.QWidget] | None) – Buttons shown on the title line (few) or, with own_row, on a bar of their own between the title and the body (many).

  • own_row (bool) – Put the buttons on their own row.

Return type:

PySide6.QtWidgets.QWidget

plesty.server.view.window._button(text: str, slot: collections.abc.Callable[[], Any], tooltip: str = '', primary: bool = False, danger: bool = False) → PySide6.QtWidgets.QPushButton

A card-header button wired to slot.

Parameters:
  • text (str)

  • slot (collections.abc.Callable[[], Any])

  • tooltip (str)

  • primary (bool)

  • danger (bool)

Return type:

PySide6.QtWidgets.QPushButton

plesty.server.view.window.ICON_SIZE = 16
plesty.server.view.window._iconify(buttons: collections.abc.Mapping[str, PySide6.QtWidgets.QPushButton], icons: collections.abc.Mapping[str, str]) → None

Turn a card’s text buttons into icons, each named in its tooltip (#41).

The label stays the button’s accessible name and its label property, so it is still what the button is called — to a screen reader, and to the code that renames it (_relabel()).

Parameters:
  • buttons (collections.abc.Mapping[str, PySide6.QtWidgets.QPushButton])

  • icons (collections.abc.Mapping[str, str])

Return type:

None

plesty.server.view.window._relabel(button: PySide6.QtWidgets.QPushButton, label: str, icon: str, does: str | None = None) → None

Name an icon button label, drawn as icon; does is what it does, kept if None.

Parameters:
  • button (PySide6.QtWidgets.QPushButton)

  • label (str)

  • icon (str)

  • does (str | None)

Return type:

None

plesty.server.view.window._describe(button: PySide6.QtWidgets.QPushButton, does: str) → None

Set what button does — or why it cannot — under its name, in the tooltip.

Parameters:
  • button (PySide6.QtWidgets.QPushButton)

  • does (str)

Return type:

None

plesty.server.view.window._icon_colour(role: str) → str

The stroke of an icon on a button of role, as the stylesheet colours its text.

Parameters:

role (str)

Return type:

str

plesty.server.view.window._theme_tokens() → dict[str, str]
Return type:

dict[str, str]

plesty.server.view.window._icon(name: str, colour: str) → PySide6.QtGui.QIcon

The Lucide icon name (assets/icons), stroked colour, faint when disabled.

Lucide draws in currentColor, which Qt reads as black on a dark console, so the colour is written into the SVG before it is rendered.

Parameters:
  • name (str)

  • colour (str)

Return type:

PySide6.QtGui.QIcon

class plesty.server.view.window._StayOpen

Bases: PySide6.QtCore.QObject

Trigger a checkable menu entry on release without closing the menu.

eventFilter(watched: PySide6.QtCore.QObject, event: PySide6.QtCore.QEvent) → bool
Parameters:
  • watched (PySide6.QtCore.QObject)

  • event (PySide6.QtCore.QEvent)

Return type:

bool

plesty.server.view.window._shown(fleets: frozenset[str] | None, benches: frozenset[str] | None, others: bool = True) → str

What the device table shows, for the status line: all fleets, or each by name.

others says whether any other bench is shown at all; without one, “all other benches” is not said.

Parameters:
  • fleets (frozenset[str] | None)

  • benches (frozenset[str] | None)

  • others (bool)

Return type:

str

plesty.server.view.window._short(path: Any) → str

A path with the home directory abbreviated to ~.

Parameters:

path (Any)

Return type:

str

plesty.server.view.window.SOURCE_WIDTH = 240
plesty.server.view.window._source_column(table: PySide6.QtWidgets.QTableWidget, column: int) → None

Size a Source column by hand, never by its contents (#25).

Source is the longest cell in the window — a distribution, a version and a commit. Sized to its contents it pushed the window wider than the screen; as Stretch it took what the other columns left and could not be dragged at all. It starts at SOURCE_WIDTH, the operator drags it from there, the cell elides and its tooltip carries the full text, and the last column takes up whatever width is spare.

Parameters:
  • table (PySide6.QtWidgets.QTableWidget)

  • column (int)

Return type:

None

plesty.server.view.window.CLOCK_FORMAT = '%y-%m-%d,%H:%M:%S'
plesty.server.view.window._clock(stamp: str | None) → str

YY-MM-DD,HH:MM:SS of an ISO timestamp (local time), or "".

Parameters:

stamp (str | None)

Return type:

str

plesty.server.view.window._opened_paths(root: PySide6.QtWidgets.QTreeWidgetItem, prefix: tuple[str, Ellipsis] = ()) → set[tuple[str, Ellipsis]]

Every expanded row of a tree, as the path of labels leading to it.

The path rather than the label, because labels repeat: every step of a sweep has a params and a value, and remembering “params is open” would open it under every step at once.

Parameters:
  • root (PySide6.QtWidgets.QTreeWidgetItem) – The item whose children to walk.

  • prefix (tuple[str, Ellipsis]) – The labels leading to root.

Returns:

One path per expanded row.

Return type:

set[tuple[str, Ellipsis]]

plesty.server.view.window._reopen(root: PySide6.QtWidgets.QTreeWidgetItem, opened: set[tuple[str, Ellipsis]], prefix: tuple[str, Ellipsis] = ()) → None

Open again every row whose path is in opened.

Parameters:
  • root (PySide6.QtWidgets.QTreeWidgetItem) – The item whose children to walk.

  • opened (set[tuple[str, Ellipsis]]) – Paths from _opened_paths().

  • prefix (tuple[str, Ellipsis]) – The labels leading to root.

Return type:

None

plesty.server.view.window._embedded_view(module: str, run_dir: Any, site_dirs: collections.abc.Sequence[Any] = ()) → Any

Build an experiment’s own views as a widget, to dock in the console.

The viewer is loaded the way plesty-lib’s viewer contract loads one (plesty.lib.monitor.load_viz()): by importing it. An experiment is installed in an environment of its own, so it is usually not importable here as it stands; site_dirs — that environment’s packages — are then added behind this console’s own on the import path, so the module and what only it depends on are found there while everything the console already has stays the console’s. plesty is a namespace package, so the module’s part of it joins this one. It also needs a plesty-lib carrying the viewer contract; an older one raises, and the caller says so.

Parameters:
  • module (str) – Import path of the experiment’s viewer, from its manifest.

  • run_dir (Any) – The run to watch.

  • site_dirs (collections.abc.Sequence[Any]) – The module environment’s site-packages, to import it from when it is not installed here.

Returns:

A shell holding the panels, not yet started.

Raises:

Exception – Anything the import or the composition raises — the caller treats every one of them the same way.

Return type:

Any

plesty.server.view.window._own_view(run_dir: Any, title: str = 'Run') → Any

Build a view of a run out of what the console itself can read.

An experiment is installed in an environment of its own, so its viewer usually cannot be imported here and the pane would offer a button and nothing else. But the console has plesty-lib, and a run is a directory of committed records — so the numbers a run recorded can be drawn here whatever produced them, and every run gets a plot instead of almost none.

What this cannot do is the experiment’s own views: a spectrum lives in a file the record only names, and knowing that data_file is a spectrum is exactly the experiment-specific knowledge the console does not have. Those stay in the module’s window, which is offered alongside.

Parameters:
  • run_dir (Any) – The run to draw.

  • title (str) – What to call the view.

Returns:

A shell holding one plot per numeric field, not yet started.

Raises:

Exception – When plesty-lib cannot build a view, or the run carries no number worth plotting — the caller falls back to the offer.

Return type:

Any

plesty.server.view.window._plottable(rows: list[dict[str, Any]]) → list[str]

The fields of a run’s records that are worth drawing against the step.

A number that never changes is a setting, not a measurement, and a run’s records carry plenty of those — the exposure, the sample, the frame count. Drawing them would fill the pane with flat lines and push the one field that moves off the bottom.

Parameters:

rows (list[dict[str, Any]]) – The committed records, as read.

Returns:

The field names, in the order first seen.

Return type:

list[str]

plesty.server.view.window._written(report: collections.abc.Mapping[str, Any]) → str

When a report was written and how big it is, as one readable phrase.

The size is what says whether a report is worth sending as it stands or wants trimming first, and the time is what says whether it is the one for the run as it finished or a stale one from an earlier export.

Parameters:

report (collections.abc.Mapping[str, Any])

Return type:

str

plesty.server.view.window._unwrap(record: collections.abc.Mapping[str, Any], offset: int) → tuple[str, str, dict[str, Any]]

Return how one record reads: its step, its summary, and what to open.

A committed record is an envelope — index, saved_at, provenance, type, value — around what the step actually produced. Shown as it stands, every row of a sweep carries the same five field names and the step is not named at all, which is a tree that sorts nothing. So the envelope is opened here: the row is the step the provenance names, and its children are the measurement.

Parameters:
  • record (collections.abc.Mapping[str, Any]) – One committed record.

  • offset (int) – Its place among the records shown, for a record that carries no step of its own.

Returns:

The row’s label, the line beside it, and the document under it.

Return type:

tuple[str, str, dict[str, Any]]

plesty.server.view.window._fields_of(document: collections.abc.Mapping[str, Any]) → str

One line naming what a document holds, for the row that opens it.

A closed row still has to be worth reading, so this names the fields rather than their values: which are there is what tells one step from another, and the values are one click away.

Parameters:

document (collections.abc.Mapping[str, Any])

Return type:

str

plesty.server.view.window._fill_tree(parent: PySide6.QtWidgets.QTreeWidgetItem, value: Any, depth: int = 0) → None

Add value under parent, a level per level of the document.

Parameters:
  • parent (PySide6.QtWidgets.QTreeWidgetItem) – The row the value belongs to.

  • value (Any) – A mapping, a sequence, or a leaf.

  • depth (int) – How deep this is, so a document that nests without end — or one that refers to itself — cannot take the window down with it.

Return type:

None

plesty.server.view.window._leaf(value: Any) → str

How one value reads on its own row.

Parameters:

value (Any)

Return type:

str

plesty.server.view.window._experiment_state(state: str) → str

The run state as the table shows it.

not_started is the one worth rewording: it is the ordinary state of an experiment that is declared and ready, not a failure to start.

Parameters:

state (str)

Return type:

str

plesty.server.view.window._state_tone(state: str) → str

Which palette tone a run state is coloured with.

Parameters:

state (str)

Return type:

str

plesty.server.view.window._progress_text(status: collections.abc.Mapping[str, Any]) → str

One line saying how far a run has got, and what it is doing now.

The step in flight is named when it reports itself: a long exposure is otherwise indistinguishable from a run that has stopped answering.

Parameters:

status (collections.abc.Mapping[str, Any])

Return type:

str

plesty.server.view.window._asked_text(status: collections.abc.Mapping[str, Any]) → str

What a run has been told to do and has not done yet, or "".

A request lands at a step boundary, so there is a stretch — a whole exposure — where the operator has asked for something and nothing has visibly happened. Saying so is the difference between waiting and wondering whether the click registered.

Parameters:

status (collections.abc.Mapping[str, Any])

Return type:

str

plesty.server.view.window._tone_brush(widget: PySide6.QtWidgets.QWidget, state: str) → Any

The palette colour for a state, looked up from the theme on the window.

Parameters:
  • widget (PySide6.QtWidgets.QWidget)

  • state (str)

Return type:

Any