Python

You defined a data model in DSM and ran Kibo over it. What you got back — and what you work against every day — is a small typed Python package named after your model. You almost never call the dsviper runtime directly: you import two of its modules and let them delegate to the runtime for you.

This page is that daily surface, end to end. For why the generated code exists (the adapter/Dual Reality pattern), see kibo-template-viper; for the raw runtime this layer sits on, see the dsviper for Python chapter — starting at Types and Values.

Anatomy of a generated package

A generated package — say model/ — has a handful of modules:

model/
├── data.py                 ← the types: one class per concept, struct, enum, container
├── attachments.py          ← the verbs on a commit state (get / set / enumerate)
├── database_attachments.py ← the same verbs for a standard (non-commit) Database
├── definitions.py          ← the runtime type catalog (plumbing — you rarely import this)
├── value_type.py           ← Viper type handles used by data.py / attachments.py (plumbing)
├── path.py                 ← field paths used by the fine-grained setters (plumbing)
├── resources.py            ← the packed definitions blob (plumbing)
└── __init__.py             ← re-exports data.py

A model that declares function pools also gets function_pools.py (and RPC variants). This guide uses the versioned commit database throughout; database_attachments.py is the twin of attachments.py for the plain, non-versioned Database store — the same verbs, called directly on a Database instead of a commit state (see Database). Every file carries this header — it is generated, and regenerated on every model change:

# Generated by kibo from <namespace>. Do not edit by hand.

Treat the package as a build artifact: never hand-edit it, never commit a local tweak to it — the next generation overwrites you. If something in the generated code is wrong, the fix belongs in the DSM model or the template, not the output.

The two modules you actually import

import model.data as md          # the types
import model.attachments as ma    # the verbs

Everything else is plumbing these two lean on. (attachments re-exports data, so a symbol like Tuto_UserKey is reachable through either — but importing both under md / ma keeps types and verbs legible at the call site.) A first program touches nothing but md, ma, and a CommitDatabase:

from dsviper import CommitDatabase, CommitStateBuilder, CommitMutableState
import model.data as md
import model.attachments as ma

db = CommitDatabase.open("model.cdb")

# a typed key + a typed struct — from `data`, never a raw dsviper.Value
user = md.Tuto_UserKey.create()
login = md.Tuto_Login()
login.nickname = "alice"

# a mutation verb — from `attachments`, applied to the mutating state
state = CommitMutableState(CommitStateBuilder.initial_state(db))
ma.tuto_user_login_set(state.attachment_mutating(), user, login)
db.commit_mutations("Add user", state)

That is the whole shape of daily work: types come from data, changes go through attachments, the commit loop lives on CommitDatabase. The rest of this chapter is those three ideas in detail.

Where each name comes from

The generated names are mechanical — which is what makes a stack trace legible (see Reading a repro):

You see

It came from

DSM

md.Tuto_UserKey

a concept

concept User; in namespace Tuto

md.Tuto_Login, .nickname

a struct and its field

struct Login { string nickname; }

md.Tuto_Status.ACTIVE

an enum case

enum Status { …, active, }

ma.tuto_user_login_set

an attachment verb

attachment<User, Login> login;

The prefix is always <Namespace>_ for types and <namespace>_<key-concept>_<attachment>_ for verbs — so a symbol in an error message points straight back to a line of DSM.

The proxy posture

The generated classes — Tuto_UserKey, Tuto_Login, Set_Tuto_UserKey — are proxies. It is tempting to treat them as ordinary Python objects and drop them into ordinary Python containers. Don’t. Understanding why is the difference between code that fails at the mistake and code that fails three layers away.

One value, two realities

Viper is a strongly-typed data model. The type catalog, built from your DSM at runtime, is the single source of truth — a value either conforms to its type or it is rejected, at the moment it is built. That safety lives in the runtime.

Python is a permissive environment. set[Tuto_UserKey] is a hint: at runtime it is set[object], and nothing stops you adding a string to it.

A proxy is how the runtime’s strong typing is expressed inside Python’s permissive one — the same value seen two ways (the Dual Reality). The proxy does not add type safety; the safety already lives in the runtime value. The proxy makes it visible and ergonomic at edit time.

What vpr_value is

Every proxy holds exactly one runtime value in an attribute named vpr_value:

user = md.Tuto_UserKey.create()
user.vpr_value          # the underlying dsviper.ValueKey — the runtime reality

vpr_value is the seam between the two realities. The generated code reaches through it to delegate every operation to the runtime — that is its job. It is an implementation detail of the proxy, not an interchange format for your code. When you find yourself writing .vpr_value in your own logic, it is a signal that you dropped the proxy posture somewhere upstream and are now patching the boundary by hand.

Native container vs. proxy container

The two look interchangeable and are not:

>>> a = md.Tuto_UserKey.create()
>>> raw = {a}                      # to the type system this is set[object]…
>>> raw.add("this is a bug")       # …so this is silently accepted — a deferred bug

>>> strict = md.Set_Tuto_UserKey()  # a set of Tuto_UserKey, enforced at insertion
>>> strict.add(a)
>>> strict.add("this is a bug")    # rejected here, at the mistake — fail-fast
Traceback (most recent call last):
    ...
AttributeError: ...

The native set carries anything; the annotation is a comment. The proxy set delegates each add to the runtime through vpr_value, so a wrong element is rejected at the line that inserts it, with a real stack frame. Fail-fast is not a side effect — it is the whole point of holding the proxy.

The request that looks reasonable and isn’t

A recurring ask: “let the container constructor accept a native set of proxies, so I can skip the unwrapping.” Given attachment<A, set<B>> bbb;, the wish is:

def generate() -> set[Foo_BKey]:          # really set[object]
    return {Foo_BKey.create(), Foo_BKey.create()}

data = md.Set_Foo_BKey(generate())        # convenience wanted
# instead of today's:
data = md.Set_Foo_BKey({b.vpr_value for b in generate()})

It will not be added, on purpose. The constructor already accepts native scalars and PODs — that is Viper’s seamless decode, a genuine input boundary (md.Set_uint32({1, 2, 3}) works because integers are their own runtime form). But a set of proxies is not a bag of PODs; it is a bag of Developer-Reality objects whose only runtime-meaningful content is hidden behind vpr_value. Teaching the constructor to peek into those objects would bless set[object]-of-proxies as an interchange format — exactly the permissive, deferred-bug posture the proxies exist to replace. The manual {b.vpr_value for b in …} is not a missing convenience; it is the friction that tells you the value was produced in the wrong reality.

The rule: fix the output, not the boundary

The problem is never the input — it is your output. Don’t produce a native container and reconcile it at the edge; produce the typed container where the value is created:

def generate() -> md.Set_Foo_BKey:        # a set of Foo_BKey, enforced
    result = md.Set_Foo_BKey()
    result.add(Foo_BKey.create())
    result.add(Foo_BKey.create())
    return result

data = generate()                          # no unwrapping, no boundary

Pick the all-proxy posture and stay in it. The generated SDK expresses the runtime’s type discipline; native Python objects suspend it. Mixing the two does not buy convenience — it defers the moment a wrong value is caught from the line that created it to some later line that cannot explain it.

Living in the proxies

Everything in data follows a few shapes. Once you know them, a new model reads itself.

Keys — concepts and clubs

A concept or club becomes a …Key proxy. A key is an identity, not data:

>>> user = md.Tuto_UserKey.create()   # a fresh identity
>>> user.description()                # "<uuid>:Tuto::UserKey" — human-readable
'...:Tuto::UserKey'
>>> user == user                      # identity comparison, via the runtime
True

Keys are hashable and orderable (through vpr_value), so a Set_Tuto_UserKey or a Map_Tuto_UserKey_… keyed by them behaves as you expect.

Structs — fields as properties

A struct becomes a proxy whose fields are properties: read and assign them directly, and the runtime type-checks each assignment:

>>> login = md.Tuto_Login()
>>> login.nickname = "alice"
>>> login.password = "s3cret"
>>> login.nickname
'alice'
>>> login.nickname = 42               # rejected by the runtime — wrong type
Traceback (most recent call last):
    ...
dsviper.ViperError

(login.copy() returns a deep, independent copy.)

Enums — cases as attributes

An enum becomes a proxy with one attribute per case (upper-cased):

>>> account = md.Tuto_Account()
>>> account.state = md.Tuto_Status.ACTIVE
>>> account.state.name()
'active'
>>> md.Tuto_Status.from_str("pending").name()   # parse from a name
'pending'

Containers — Pythonic, but still typed

vector / set / map / xarray / optional become container proxies with the idiomatic Python surface — iteration, in, indexing, len, set operators — all delegating to the runtime:

>>> keys = md.Set_Tuto_UserKey()
>>> keys.add(md.Tuto_UserKey.create())
>>> keys.add(md.Tuto_UserKey.create())
>>> len(keys)
2
>>> all(isinstance(k, md.Tuto_UserKey) for k in keys)   # iteration yields proxies
True

The one rule from the proxy posture applies throughout: elements are proxies too. Build and pass the typed container; never assemble a native set/list of proxies and unwrap it at the boundary.

Mutating: calling an attachment verb

Data does not live in a key — it hangs off it as attachments. You read and write attachments through the generated free functions in attachments, against a state. A state is a point in the commit DAG (see Commit for the model); here is the loop.

Open a state

db = CommitDatabase.open("model.cdb")

# a fresh, empty state:
state = CommitMutableState(CommitStateBuilder.initial_state(db))

# …or the latest committed state of an existing database:
state = CommitMutableState(CommitStateBuilder.state(db, db.last_commit_id()))

Write — through attachment_mutating(), then commit

Setters take an AttachmentMutating; nothing is persisted until you commit the state. Here is the full loop against a fresh in-memory database — a real app opens a .cdb instead, but the shape is identical (the generated definitions module seeds the schema):

>>> from dsviper import CommitDatabase, CommitStateBuilder, CommitMutableState
>>> import model.definitions as mdef
>>> db = CommitDatabase.create_in_memory()
>>> _ = db.extend_definitions(mdef.definitions())

>>> user = md.Tuto_UserKey.create()
>>> login = md.Tuto_Login()
>>> login.nickname = "alice"

>>> state = CommitMutableState(CommitStateBuilder.initial_state(db))
>>> ma.tuto_user_login_set(state.attachment_mutating(), user, login)
>>> ma.tuto_user_login_set_nickname(state.attachment_mutating(), user, "alice2")   # fine-grained field set
>>> _ = db.commit_mutations("Register alice", state)   # one typed commit

Read — through attachment_getting()

Every attachment gives you …_get, …_keys, …_enumerate, each taking an AttachmentGetting drawn from a state:

>>> read = CommitMutableState(CommitStateBuilder.state(db, db.last_commit_id()))
>>> getting = read.attachment_getting()
>>> len(ma.tuto_user_login_keys(getting))              # Set_Tuto_UserKey
1
>>> ma.tuto_user_login_get(getting, user).unwrap().nickname   # Optional_Tuto_Login
'alice2'
>>> [login.nickname for _key, login in ma.tuto_user_login_enumerate(getting)]
['alice2']

The mental model is uniform: a verb is a free function taking a getting/mutating handle, a proxy key, and (for setters) a proxy value. Collection-valued attachments add the expected verbs — …_union_<field>, …_subtract_<field>, …_insert_<field>, …_remove_<field> — each mutating in place through the same state. The commit itself, and everything about heads, history and merges, is the Commit subsystem’s job, not the SDK’s.

Reading a repro

Because every generated name is mechanical, an error or a bug report written in generated symbols maps straight back to your model — no guessing.

Given a traceback that mentions ma.tuto_user_login_set or md.Tuto_LoginKey, read the name:

  • Tuto_Login → type Login in namespace Tutodata.py, and the DSM line struct Login { }.

  • tuto_user_login_set<namespace>_<key-concept>_<attachment>_<verb> → namespace Tuto, key concept User, attachment login, verb set → the DSM line attachment<User, Login> login;, and the function in attachments.py.

So a report like tuto_user_login_set raises on this input” localises itself: the type is Tuto_Login, the attachment is login<User, Login>, the code is the generated attachments.py. From there:

  1. The generated file says “Do not edit by hand.” The defect is upstream — in the DSM model, or in the template that produced the file — never a patch to the output.

  2. Regenerate after any model change (see dsm_util.py); the package is a build artifact and a stale one will lie about the model.

That legibility is the payoff of the proxy layer: the names you debug are the names you declared.