Developer Documentation

This is a gathering point for the notes and thoughts of the primary developer as of April 2026. I hope to capture some of the thoughts behind the design decisions made, not to convince others of their correctness but rather to (hopefully) provide helpful guidance for future developers.

I apologize in advance.

The Architecture

The application is built around operations using the Client. In order to perform actions in the application, the Client holds references to a “Control Layer” and a “Backend”

  • “Control Layer”: Used for communication with the controls system.

    • Currently this include CA, but could be trivially extended to PVA.

  • “Backend”: Used for communication with the data storage system.

    • Currently we feature two file-based backends, and one in-memory test backend.

By abstracting these interfaces, we make it possible to replace the backing technologies without redesigning the application.

The GUI communicates entirely with the client, and should not hold any business logic of its own. Of course, this line may have been inadvertently blurred during development in some places, but future developers should strive to maintain this boundary.

To help build a responsive GUI, we have added callbacks to the Client. These callbacks fire when specific events happen. These callbacks should not be needed for programmatic or interactive use of the Client.

digraph "classes" { rankdir=BT charset="utf-8" bgcolor=transparent "superscore.client.CallbackType" [color="black", fontcolor="black", label=<<table border="0" align="left" tooltip="CallbackType" width="0" cellpadding="0"> <tr><td border="1" href="./superscore.client.html#superscore.client.CallbackType" tooltip="CallbackType" target="_top"><b>CallbackType</b></td></tr> <tr><td align="left" tooltip=""><b>Attributes:</b></td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.CallbackType.name" target="_top" tooltip="CallbackType.name">name</td></tr></table>>, shape="record", style="solid", URL="./superscore.client.html#superscore.client.CallbackType"]; "superscore.client.Client" [color="black", fontcolor="black", label=<<table border="0" align="left" tooltip="Client" width="0" cellpadding="0"> <tr><td border="1" href="./superscore.client.html#superscore.client.Client" tooltip="Client" target="_top"><b>Client</b></td></tr> <tr><td align="left" tooltip=""><b>Attributes:</b></td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.backend " target="_top" tooltip="Client.backend : _Backend">backend : _Backend</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.cl " target="_top" tooltip="Client.cl : ControlLayer">cl : ControlLayer</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.enable_editing_past " target="_top" tooltip="Client.enable_editing_past : bool">enable_editing_past : bool</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.recent_entry_cache " target="_top" tooltip="Client.recent_entry_cache : set[UUID]">recent_entry_cache : set[UUID]</td></tr> <tr><td align="left" tooltip=""><b>Methods:</b></td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.apply" target="_top" tooltip="Client.apply">apply(entry: Union[Setpoint, Snapshot], sequential: bool): Optional[List[TaskStatus]]</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.compare" target="_top" tooltip="Client.compare">compare(entry_l: Entry, entry_r: Entry): EntryDiff</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.convert_to_template" target="_top" tooltip="Client.convert_to_template">convert_to_template(collection: Collection): Template</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.delete" target="_top" tooltip="Client.delete">delete(entry: Entry): None</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.fill" target="_top" tooltip="Client.fill">fill(entry: Union[Entry, UUID], fill_depth: Optional[int]): None</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.fill_template" target="_top" tooltip="Client.fill_template">fill_template(template: Template, substitutions: Dict[str, str]): Collection</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.find_config" target="_top" tooltip="Client.find_config">find_config(): Path</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.find_origin_collection" target="_top" tooltip="Client.find_origin_collection">find_origin_collection(entry: Union[Collection, Snapshot]): Collection</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.from_config" target="_top" tooltip="Client.from_config">from_config(cfg: Optional[Path])</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.from_parsed_config" target="_top" tooltip="Client.from_parsed_config">from_parsed_config(cfg_parser: configparser.ConfigParser, cfg_path)</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.get_entry" target="_top" tooltip="Client.get_entry">get_entry(uuid: UUID, fill: bool, force_reload: bool): Entry</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.get_user" target="_top" tooltip="Client.get_user">get_user(): str</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.is_editable" target="_top" tooltip="Client.is_editable">is_editable(entry: Entry): bool</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.is_user_authorized" target="_top" tooltip="Client.is_user_authorized">is_user_authorized(user: str): bool</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.register_callback" target="_top" tooltip="Client.register_callback">register_callback(cb_type: CallbackType, cb: Callable[[UUID], None])</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.run_callbacks" target="_top" tooltip="Client.run_callbacks">run_callbacks(cb_type: CallbackType)</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.save" target="_top" tooltip="Client.save">save(entry: Entry)</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.search" target="_top" tooltip="Client.search">search(): Generator[Entry, None, None]</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.snap" target="_top" tooltip="Client.snap">snap(entry: Collection, dest: Optional[Snapshot]): Snapshot</td></tr> <tr><td align="left" href="./superscore.client.html#superscore.client.Client.validate" target="_top" tooltip="Client.validate">validate(entry: Entry): ValidationResult</td></tr></table>>, shape="record", style="solid", URL="./superscore.client.html#superscore.client.Client"]; }

UML Diagram describing the superscore Client

Open in a new tab

Backend types

`FilestoreBackend`: Stores Entries in a single file as a JSON blob. The JSON is generated via apischema deserialization. This is the easiest backend to work with for local development, as changes to the database are easily observed. This backend obviously scales poorly with database size.

`DirectoryBackend`: Stores Entries in directories based on their uuid. The directory structure is a Radix Tree with a default depth of 3. This schema keeps Entry data organized and spread across multiple files for safety.

Control Layer types

`AiocaShim`: An EPICS Channel Access communication layer that supports asyncio access of PVs. The asyncio operation is abstracted away from users of the shim.

How to use the Dataclasses

If one wants to use superscore in other applications, one can manipulate the data model dataclasses directly in Python, and use the Client to communicate with the database.

One can simply import the dataclasses and construct the Entry you’d like.

from superscore.model import Collection, Parameter, Snapshot

origin_coll = Collection(
    description="origin of various types",
    children=[
        Parameter(pv_name="MY:FLOAT"),
        Parameter(pv_name="MY:INT"),
        Parameter(pv_name="MY:ENUM"),
    ]
)
snap = Snapshot(description='various types', title='types collection',
                origin_collection=origin_coll)
snap.children.append(Setpoint(pv_name="MY:FLOAT"))
snap.children.append(Setpoint(pv_name="MY:INT"))
snap.children.append(Setpoint(pv_name="MY:ENUM"))

Once the Entry has been created, Client methods can be used to verify and save the data to the database.

A Primer on the GUI

This section is not about how to use the GUI, but rather about the organization of the code that comprises the GUI.

Main Window Singleton, WindowLinker

The GUI centered around the “main window”, which holds a tree-view and a tab widget. The tab widget holds pages that display and edit Entry data. This Window is a singleton that we expose to every widget in the application via the WindowLinker mixin. This mixin provides methods for accessing the central Client and a method for opening a page (Window.open_page_slot). This mixin should be added to any page widget opened by the application.

QDataclassBridge

In GUI applications that display the same data across different views, one problem is keeping the views synchronized when data changes. While one might expect the qt MVC framework to help with this, qt models are restricted to the views they service.

One way this is approached in superscore is via the QDataclassBridge. This is a simple QObject that emits signals when attributes on the dataclass are modified. This requires changes to the dataclass to be made through the bridge, rather than directly on the object itself. (This is a bit obfuscated, and one of the main drawbacks of the construct).

These bridges are constructed automatically in DataTracker

DataTracker and DataWidget

DataWidget and DataTracker are the the primary classes that widgets in the application inherit from. Widgets inheriting from DataWidget should be initialized with the dataclass the widget is responsible for. The QDataclassBridge will be constructed automatically, based on the is_independent flag. By default, each DataWidget will create a unique bridge for the dataclass it receives. This is because the application relies on the Client as the single source of truth, and widgets should only update their view if data from the backend has changed. If is_independent is False, the QDataclassBridge will be created for the exact instance of the dataclass provided. This option would be chosen if multiple DataWidget s are being used to construct a single view.