sen::db Python bindings¶
The sen_db_python module exposes the sen::db cursor API to Python.
Scripts can open a recording, walk its contents, and inspect every value, type, snapshot,
and annotation without the original C++ binaries.
Importing¶
The conventional alias is sen. The rest of this document assumes it.
Opening a recording¶
inp = sen.Input("/path/to/recording_directory")
print(inp.path) # str -- the recording directory
summary = inp.summary # Summary object
Summary carries:
| Attribute | Type | Meaning |
|---|---|---|
firstTime |
datetime.timedelta |
time of the first sample, since epoch |
lastTime |
datetime.timedelta |
time of the last sample, since epoch |
keyframeCount |
int |
number of keyframes |
objectCount |
int |
number of distinct objects |
typeCount |
int |
number of distinct types |
annotationCount |
int |
number of annotations |
indexedObjectCount |
int |
number of objects with random access |
The cursor model¶
A DataCursor iterates over the runtime data. Three constructors:
| API | Cursor over |
|---|---|
inp.begin() |
the whole recording, from the start |
inp.at(keyframeIndex) |
the whole recording, from a chosen keyframe |
inp.makeCursor(objectIndexDef) |
a single object's entries only |
Walk pattern:
cursor = inp.begin()
cursor.advance() # move to the first entry
while not cursor.atEnd:
entry = cursor.entry
# entry.time -> datetime.timedelta since epoch
# entry.payload -> one of PropertyChange / Event / Keyframe /
# Creation / Deletion / End
...
cursor.advance()
cursor.atStart is True before any advance(); in that state entry.payload is None.
cursor.atEnd is True once the cursor reaches the recording's end.
The payload accessor returns the active variant alternative directly, so isinstance()
dispatches against concrete classes:
if isinstance(entry.payload, sen.PropertyChange):
...
elif isinstance(entry.payload, sen.Event):
...
Payload classes¶
PropertyChange¶
| Attribute | Type |
|---|---|
.objectId |
int |
.name |
str -- property name |
.value |
converted Python value |
Event¶
| Attribute | Type |
|---|---|
.objectId |
int |
.name |
str -- event name |
.args |
list[object] -- one converted value per argument |
Keyframe¶
| Attribute | Type |
|---|---|
.snapshots |
sequence of Snapshot |
Creation¶
Delegates attribute lookup to the embedded Snapshot. Read object fields directly:
creation.name, creation.className, creation.<propertyName>, etc. See
Snapshot reflection.
Deletion¶
| Attribute | Type |
|---|---|
.objectId |
int |
End¶
Sentinel marking the end of the cursor. The idiomatic check is cursor.atEnd.
Snapshot reflection¶
Snapshot exposes attributes via reflection against the object's class. Built-in
attributes return identity and class metadata; any other name resolves as a property
defined on the class (walking parents).
| Attribute | Returns |
|---|---|
.name |
str -- object name |
.busName |
str |
.sessionName |
str |
.objectId |
int -- object id |
.className |
str -- qualified class name |
.propertyNames |
list[str] -- every property, including inherited |
.<propertyName> |
the property's converted Python value |
Reading an attribute that is neither built-in nor a known property raises
AttributeError.
snap = keyframe.snapshots[0]
print(snap.className) # 'school.Student'
print(snap.propertyNames) # ['firstName', 'surName', 'focusLevel', ...]
print(snap.focusLevel) # 0.42
Value conversion¶
Sen values are converted to native Python equivalents. The mapping is uniform across
PropertyChange.value, Event.args, Annotation.value, and every
Snapshot.<propertyName> accessor.
| Sen value | Python value |
|---|---|
bool, integers, floats |
matching Python builtin |
string |
str |
TimeStamp, Duration |
datetime.timedelta |
VarList (sequence) |
list |
VarMap (named fields) |
dict |
KeyedVar (tagged variant alternative) |
{"type": "<qualifiedName>", "value": <converted>} -- same shape as the type registry uses |
| structs | dict (via VarMap) |
Type registry¶
inp.getTypes() returns a TypeRegistry snapshot describing every class, struct,
variant, alias, enum, and quantity present in the recording.
| API | Returns |
|---|---|
len(registry) |
int -- number of registered types |
name in registry |
bool |
.classNames |
list[str] -- every qualified name |
.getTypeSpec(qualifiedName) |
dict describing the type, or None if absent |
.getAllTypeSpecs() |
dict[str, dict] -- every type, keyed by qualified name |
The dict shape is the kernel's external CustomTypeSpec encoding. Top level:
| Key | Type | Meaning |
|---|---|---|
qualifiedName |
str |
fully qualified type name |
name |
str |
short name (without the package prefix) |
description |
str |
description from the STL definition |
data |
{type: str, value: dict} |
tagged union -- shape depends on type |
data.type is one of:
| Tag | data.value shape |
|---|---|
sen.kernel.ClassTypeSpec |
{properties, methods, events, constructor, parents, isInterface} |
sen.kernel.StructTypeSpec |
{fields, parent} -- each field has name, description, type; parent empty for none |
sen.kernel.VariantTypeSpec |
{fields} -- each field has key, description, type |
sen.kernel.EnumTypeSpec |
{enums, storageType} -- each enum has name, key, description |
sen.kernel.AliasTypeSpec |
{aliasedType} |
sen.kernel.QuantityTypeSpec |
{elementType, unit, minValue, maxValue} |
sen.kernel.SequenceTypeSpec |
{elementType, maxSize, fixedSize} |
sen.kernel.OptionalTypeSpec |
{type} |
ClassTypeSpec members:
- properties[i]:
name,description,category(staticRO/staticRW/dynamicRO/dynamicRW),type,transportMode,tags,checkedSet. - methods[i]:
name,description,args(list of{name, description, type}),transportMode,constness,deferred,returnType,propertyRelation,localOnly. - events[i]:
name,description,args(same shape as method args),transportMode. - constructor: same shape as
methods[i]. - parents: list of qualified parent class names.
- isInterface:
bool.
Inheritance is walked via parents. To find an event defined on an ancestor:
def find_event(reg, class_name, event_name):
spec = reg.getTypeSpec(class_name)
if spec is None:
return None
data = spec["data"]["value"]
for event in data.get("events", []):
if event["name"] == event_name:
return event
for parent in data.get("parents", []):
found = find_event(reg, parent, event_name)
if found is not None:
return found
return None
Indexes¶
| API | Returns |
|---|---|
inp.getObjectIndexDefinitions() |
sequence of ObjectIndexDef |
inp.getAllKeyframeIndexes() |
sequence of KeyframeIndex |
inp.getKeyframeIndex(time) |
KeyframeIndex or None -- closest keyframe at or before time |
inp.at(keyframeIndex) |
new DataCursor starting at that keyframe |
inp.makeCursor(objectIndexDef) |
new DataCursor iterating one object only |
ObjectIndexDef carries objectId, name, session, bus, indexId, and type
(qualified class name). KeyframeIndex carries offset and time.
All three accessors read the same indexes file on first call. If the recording was written
with a writer version whose index format the current reader doesn't support -- or the file
is otherwise malformed -- the first call (whichever of the three it is) raises; subsequent
calls re-raise. The same per-object metadata is reachable by walking the cursor and
collecting Creation payloads:
inp = sen.Input("/path/to/recording")
cursor = inp.begin()
cursor.advance()
objects = {}
while not cursor.atEnd:
p = cursor.entry.payload
if isinstance(p, sen.Creation):
objects[p.objectId] = {"name": p.name, "className": p.className, "bus": p.busName}
cursor.advance()
Annotations¶
inp.annotationsBegin() returns an AnnotationCursor with the same shape as
DataCursor -- atStart, atEnd, entry, advance(). The payload is either
Annotation or End.
Annotation attribute |
Type |
|---|---|
.type |
str -- qualified name for custom types; basic type name (e.g. f64) for built-ins |
.value |
converted Python value |
Time¶
All times are datetime.timedelta since the Unix epoch. Convert to a wall-clock
datetime:
Output discipline¶
Calling code (CLI / MCP gateway / etc.) typically caps script output. The MCP gateway caps stdout at 64 KiB, stderr at 16 KiB, and wall-clock duration at 60 s. Print summaries (not raw rows), and bound the walk by time-window or sample count when the recording is large.
Counts by category¶
from collections import defaultdict
counts = defaultdict(int)
# ... walk ...
counts[key] += 1
print(dict(counts))
Per-key stats¶
samples = defaultdict(list)
# ... walk: samples[key].append(value) ...
for key, values in samples.items():
print(f"{key}: n={len(values)} min={min(values):.3f} "
f"mean={sum(values)/len(values):.3f} max={max(values):.3f}")
Top-K¶
top = sorted(counts.items(), key=lambda kv: -kv[1])[:10]
for key, count in top:
print(f"{key}: {count}")
Time-window bins¶
entry.time is a datetime.timedelta; dividing two timedeltas gives a float, so
binning is straightforward:
from datetime import timedelta
window = timedelta(seconds=5)
bins = defaultdict(int)
# ... walk: bins[int(entry.time / window)] += 1 ...
for idx in sorted(bins):
print(f"t={idx*5:>4}s n={bins[idx]}")
Sampling, not enumeration¶
When showing examples, cap how many you keep:
EXAMPLES_PER_KEY = 3
examples = defaultdict(list)
# ... walk: keep only the first few per key ...
if len(examples[key]) < EXAMPLES_PER_KEY:
examples[key].append(value)
End-to-end example¶
Compute per-student focus statistics and rank events by frequency across a school recording, in a single cursor walk:
import sen_db_python as sen
from collections import defaultdict
from datetime import datetime
EPOCH = datetime(1970, 1, 1)
inp = sen.Input("/data/recordings/school_run_42")
# 1. Sanity-check the schema via the type registry.
reg = inp.getTypes()
student_spec = reg.getTypeSpec("school.Student")
assert student_spec is not None
property_names = [p["name"] for p in student_spec["data"]["value"]["properties"]]
assert "focusLevel" in property_names
# 2. Walk the recording once, collecting samples per student and counting events.
samples = defaultdict(list)
names_by_id = {}
event_counts = defaultdict(int)
cursor = inp.begin()
cursor.advance()
while not cursor.atEnd:
p = cursor.entry.payload
if isinstance(p, sen.Creation):
names_by_id[p.objectId] = p.name
elif isinstance(p, sen.PropertyChange) and p.name == "focusLevel":
samples[p.objectId].append(p.value)
elif isinstance(p, sen.Event):
event_counts[p.name] += 1
cursor.advance()
# 3. Aggregate and report.
print(f"Window: {EPOCH + inp.summary.firstTime} .. {EPOCH + inp.summary.lastTime}")
print("\nFocus statistics:")
for object_id, values in samples.items():
name = names_by_id.get(object_id, f"<id {object_id}>")
mean = sum(values) / len(values)
print(f" {name:<20} n={len(values):>4} mean={mean:.3f}")
print("\nEvent counts:")
for name, count in sorted(event_counts.items(), key=lambda kv: -kv[1]):
print(f" {name:<24} {count}")