dsviper-ge

PySide6 desktop application that exercises the whole DevKit value chain from a single DSM model: code generation with Kibo, persistence and versioning through dsviper, and a Qt Widgets UI assembled from the shared dsviper-components library.

What it demonstrates

dsviper-ge is a worked example of every layer the documentation introduces in isolation:

Layer

Where in dsviper-ge

DevKit doc

DSM model

DSM definitions of the Graph

DSM

Code-gen

gei/ package (Kibo output)

Kibo, Python SDK

Runtime

dsviper.CommitStore usage

dsviper

Shared UI

dsviper_components/

dsviper-components

A single Python application reads its data model from one DSM source and ships a versioned, undo-aware editor — the value-chain end-to-end.

Architecture

Pure-Python applications collapse the C++ 6-layer architecture to five layers, because business logic lives in the same language as the application — function pools are no longer needed to cross a language boundary.

┌─────────────────────────────────────────────────────────────┐
│  1. UI Layer                                                │
│     PySide6 widgets (graph_editor.py, components/, render/) │
├─────────────────────────────────────────────────────────────┤
│  2. CommitStore Facade                                      │
│     ge/context.py — singleton wrapping CommitStore          │
├─────────────────────────────────────────────────────────────┤
│  3. Business Logic (hand-written Python)                    │
│     ge/*.py — vertex.py, graph.py, selection_*.py, …        │
├─────────────────────────────────────────────────────────────┤
│  4. Generated Data (Kibo output)                            │
│     gei/ — the graph unit, containers, definitions()        │
├─────────────────────────────────────────────────────────────┤
│  5. dsviper Runtime                                         │
│     CommitDatabase, CommitStore, CommitMutableState, Value  │
└─────────────────────────────────────────────────────────────┘

Repository layout

dsviper-ge/
├── graph_editor.py          # Application entry point (QApplication, MainWindow)
├── kibo.toml                # How gei/ is generated from the DSM model
├── gei/                     # Kibo-generated infrastructure, never edited by hand
│   ├── __init__.py          # definitions(), the units, AnyConceptKey, Key, …
│   ├── containers.py        # One class per container shape the model uses
│   ├── graph/               # The DSM namespace Graph
│   │   ├── data.py          # Concept keys, structures, their runtime ids
│   │   ├── commit_attachments.py  # The attachments over a Commit state: one class per concept
│   │   └── fields.py, paths.py    # Field names and paths, for the field operations
│   ├── resources.py         # Embedded DSM definitions
│   └── _codegen/            # The runtime the generated package carries
├── ge/                      # Hand-written business logic
│   ├── context.py           # Singleton: store + graph_key + facade
│   ├── graph.py             # Graph creation
│   ├── vertex.py            # Vertex creation
│   ├── edge.py              # Edge creation
│   ├── graph_topology.py    # Topology operations
│   ├── selection_*.py       # Selection management
│   ├── random.py            # Random-data generators
│   └── script_*.py          # Reusable scripts
├── components/              # Project-specific widgets (vertex panel,
│                            #   list panel, render panel, comments, tags…)
├── dsviper_components/      # Vendored copy of the shared dsviper-components
│                            #   library (synced with dev/sync_dsviper_components.py)
├── render/                  # 2-D canvas (paint, hit-testing)
└── list/                    # List-view items

The Context singleton

ge/context.py is the single point of truth for the running database, the active graph, and the CommitStore that drives undo/redo and UI notifications. The UI never talks to a CommitDatabase directly.

from dsviper import CommitStore, CommitDatabase, CommitStateBuilder, CommitState, CommitMutableState, ValueCommitId

from gei import definitions, graph
from gei.graph import attachments
from ge import graph as ge_graph


class Context:

    @classmethod
    def instance(cls) -> Context:
        if not hasattr(cls, "_instance"):
            setattr(cls, "_instance", cls())
        return getattr(cls, "_instance")

    def __init__(self):
        self.store = CommitStore()
        self.graph_key = graph.GraphKey.create()

    def create_database(self, file_path: str) -> CommitDatabase:
        result = CommitDatabase.create(file_path, documentation="A Graph Editor Commit Database")
        result.extend_definitions(definitions())
        return result

    def use(self, database: CommitDatabase):
        if not database.commit_ids():
            self._create_initial_commit(database, CommitStateBuilder.initial_state(database))

        commit_id = database.last_commit_id()
        self.store.set_state(CommitStateBuilder.state(database, commit_id))
        self.store.set_database(database)
        self.store.notify_database_did_open()

        self.load()

The generated package is imported twice over: graph, the module of the DSM namespace Graph (its keys and structures), and attachments, the attachments of that namespace, grouped by the concept they are keyed on. The application’s own module ge.graph has the same short name as the unit, so it is imported under an alias.

The Context exposes the store directly (context.store.dispatch(...)) and adds a small facade for scripting (context.dispatch, context.undo, context.redo).

See dsviper-ge/ge/context.py for the full implementation.

The dispatch pattern

Every UI action that mutates the model goes through one entry point: store.dispatch(label, callable). The callable receives a CommitAttachmentMutating and applies all mutations for this transaction. After the lambda returns, the store commits the mutations and notifies the UI.

# graph_editor.py — "Random Vertex" menu action
def _random_vertex_triggered(self):
    context = Context.instance()
    size = self._render_component.render_widget().size()
    rect = graph.Rectangle()
    rect.x, rect.y, rect.w, rect.h = 0, 0, size.width(), size.height()
    context.store.dispatch("Random Vertex",
                           lambda m: model_random.add_vertex(m, context.graph_key, rect))

graph.Rectangle is a structure of the generated unit, and model_random is the business module ge.random, imported under that name. The body of the lambda is plain Python that calls into ge/. Since business logic is already in Python, no function pool is needed — the lambda calls ge.random.add_vertex() directly. This is the central simplification that distinguishes dsviper-ge (and dsviper-ge-qml) from the C++ equivalent.

Tip

The dispatch label ("Random Vertex" above) is what appears in the undo stack and in the commit history. Pick labels that describe user intent, not implementation steps.

Business logic — ge/

ge/ is hand-written Python organised by functional domain. Each module exposes pure functions taking a CommitAttachmentMutating plus the keys/values they need to operate on, and returns the keys they create. They are trivially unit-testable in isolation.

# ge/vertex.py
from dsviper import CommitAttachmentMutating

from gei.graph import attachments
from gei import containers, graph


def add(attachment_mutating: CommitAttachmentMutating,
        graph_key: graph.GraphKey,
        value: int,
        position: graph.Position,
        color: graph.Color) -> graph.VertexKey:

    vertex_key = create(attachment_mutating, value, position, color)

    vertex_keys = containers.Set_of_Graph_VertexKey()
    vertex_keys.add(vertex_key)
    attachments.Graph.topology.union_vertex_keys(attachment_mutating, graph_key, vertex_keys)

    return vertex_key

ge/ modules import from gei/ (the generated layer) — never the other way around.

Generated data — gei/

gei/ is the Kibo output for the Python target, generated with kibo 2 and the kibo-template-viper 2.1 pack. kibo.toml at the root of the repository declares it — the model’s definitions, the infrastructure name gei, the pack’s line 2 and the Base and CommitAttachments features, the usage of an editor whose document is a Commit database — and kibo-project regenerates it after a model change:

python3 ../kibo-project/kibo_project.py generate

It is never edited by hand. What the rest of the app consumes:

Name

What it is

gei.graph

The DSM namespace Graph: keys (graph.VertexKey), structures (graph.Position)

gei.graph.commit_attachments

The attachments over a Commit state, by concept: attachments.Vertex.visual_attributes.set(…), imported as attachments

gei.containers

One class per container shape: containers.Set_of_Graph_VertexKey

gei.definitions()

The model’s definitions, loaded into the database

The mapping from DSM names to Python identifiers, and the surface of each of these, is described in Using the generated SDK — Python.

Notification flow

The CommitStore exposes signals that the UI subscribes to. The two most load-bearing for dsviper-ge:

# graph_editor.py — _setup_connections
notifier.database_did_open.connect(self._store_database_did_open)
notifier.state_did_change.connect(self._store_state_did_change)

Their handlers refresh every panel that reads from the store. This is the only path by which the UI updates: a dispatch returns, the store fires state_did_change, and the connected slots redraw.

UI action  ──►  store.dispatch(label, λ)
                       │
                       ▼
                 λ(mutating)            ← business logic runs here
                       │
                       ▼
            store.commit_mutations()    ← persisted to the DAG
                       │
                       ▼
       store.notify_state_did_change()  ← Qt signal
                       │
                       ▼
              UI handlers redraw

What dsviper-components ships with the application

Beyond the panels dsviper-ge builds for its own domain (components/), the application inherits a complete suite of administration, sync, and scripting features by importing from dsviper-components. None of this code lives in dsviper-ge — the application only instantiates the widgets and wires them into its menus.

This is the load-bearing reason every Commit-based application in the ecosystem is built on dsviper-components: the moment you adopt the shared library, your application gets a database inspector, a commit-DAG browser, an undo-stack viewer, a remote-server sync pipeline, and an embedded Python REPL — none of which are domain-specific.

The Admin menu — database introspection and history

The Admin menu of dsviper-ge is assembled from eight pre-built dialogs:

# graph_editor.py — _setup_dialog
store = Context.instance().store
self._inspect_dialog = DSInspectDialog()
self._commits_dialog = DSCommitsDialog(store)
self._commit_documents_dialog = DSCommitDocumentsDialog(store)
self._commit_program_dialog = DSCommitProgramDialog(store)
self._commit_undo_dialog = DSCommitUndoDialog(store)
self._commit_blobs_dialog = DSCommitBlobsDialog(store)
self._commit_actions_dialog = DSCommitActionsDialog(store)
self._commit_sync_log_dialog = DSCommitSyncLogDialog()

Each dialog is constructed with the running CommitStore and reacts on its own to state_did_change notifications — the application has nothing to do beyond toggling visibility.

Dialog

What it shows

DSInspectDialog

Live tree of every attachment / structure in the state

DSCommitsDialog

The full mutation DAG (browse, jump to, compare commits)

DSCommitDocumentsDialog

Documents + attachments grouped by commit

DSCommitProgramDialog

Python program embedded in a commit (replayable)

DSCommitSettingsDialog

Per-commit settings

DSCommitUndoDialog

The undo stack as a navigable list

DSCommitBlobsDialog

Every blob referenced across commits

DSCommitActionsDialog

Toggle commit visibility in batch (each toggle appends an Enable / Disable commit)

Adopting these dialogs is what turns a domain editor into a fully-introspectable Commit application. The same set is consumed by every Commit-based editor in the ecosystem.

Sync — fetch / push over a commit server

dsviper-ge’s File menu and toolbar expose three actions — Fetch, Push, Sync — that operate on a remote commit server. The wiring is again fully in dsviper-components:

# graph_editor.py
from dsviper_components.ds_commit_synchronizer_thread import DSCommitSynchronizerThread
from dsviper_components.ds_connect_to_server_dialog import DSConnectToServerDialog
from dsviper_components.ds_commit_sync_log_dialog import DSCommitSyncLogDialog

self._sync_logger = DSLogger(Logging.LEVEL_ALL)
self._sync_logging = Logging.create(self._sync_logger)

DSConnectToServerDialog collects the server connection details (host, port, socket path). DSCommitSynchronizerThread runs the sync I/O off the UI thread and emits progress signals consumed by DSCommitSyncLogDialog (the sync log visible from the Admin menu). Three menu actions drive the workflow:

  • Fetch — pull remote commits into the local DAG without applying them.

  • Push — send local commits that the server does not yet have.

  • Sync — fetch + push in one step.

The shared DAG itself is a property of the CommitDatabase — there is no separate “merge” step in the application. Deterministic reduction is what the commit DAG is for.

Embedded Python scripting — DSCodeEditorDialog

dsviper-ge embeds a fully functional Python editor that runs scripts in the same interpreter as the running application — with the application’s Context and CommitStore exposed as globals:

# graph_editor.py — _setup_dialog
from dsviper_components.python_editor_model import PythonEditorModel

scripts_folder = str(Path(__file__).parent / "scripts")
self._python_editor_model = PythonEditorModel(
    scripts_folder,
    namespace_vars={
        "ctx": Context.instance(),
        "store": store,
        "render_model": self._render_component,
        "_documents_panel": self._commit_documents_dialog,
    },
)
self._code_editor_dialog = DSCodeEditorDialog(self._python_editor_model)
self._python_editor_model.run_init_script()

The scripts a user writes from the Editor have direct access to:

  • ctx.dispatch("label", lambda m: …) — the same dispatch path the UI uses.

  • store.state(), store.attachment_getting() — read-only access to the current state.

  • domain modules (from ge import …) — the same business logic exposed to the menu actions.

The editor itself contributes a pre-wired Editor menu (open / save / run / show description / refresh syntax). dsviper-ge’s _setup_menu simply appends those actions:

# graph_editor.py — _setup_menu
editor = self._code_editor_dialog.editor
editor_menu = self.menuBar().addMenu(self.tr("&Editor"))
editor_menu.addAction(editor.open_script_action)
editor_menu.addAction(editor.save_script_action)
# ...
editor_menu.addAction(editor.run_script_action)
editor_menu.addAction(editor.show_description_action)

Tip

Scripts run in the live interpreter — they can build complex sequences of mutations that go through the same dispatch path as menu actions, which means every script ends up as a single commit in the DAG, replayable and undoable like any other operation.

For the catalogue of what dsviper-components exposes and the conventions for instantiating each widget, see dsviper-components. The model-agnostic limit case — generic database editors with no DSM model of their own — is walked through in cdbe.

Where to read first

  1. graph_editor.py lines around _setup_connections — how the UI wires itself to the store’s signals.

  2. ge/context.py — the singleton facade and database lifecycle.

  3. ge/vertex.py, ge/graph.py — the smallest, most readable examples of model functions.

  4. graph_editor.py lines around _random_vertex_triggered — a complete round trip: UI action → dispatch → model → notification → redraw.

  5. components/list.py — a panel built on dsviper-components that both reads the state and dispatches selection mutations back.

Reference