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 |
|---|---|---|
|
a concept |
|
|
a struct and its field |
|
|
an enum case |
|
|
an attachment verb |
|
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→ typeLogininnamespace Tuto→data.py, and the DSM linestruct Login { … }.tuto_user_login_set→<namespace>_<key-concept>_<attachment>_<verb>→ namespaceTuto, key conceptUser, attachmentlogin, verbset→ the DSM lineattachment<User, Login> login;, and the function inattachments.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:
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.
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.