The Python API
The Python API
Kinogaki Core speaks Python. Run:
pip install kinogaki
The wheel ships a prebuilt native library and a ctypes binding over it. There is no compiler, no build step, no C++ toolchain. Platform wheels cover macOS (universal2, Apple silicon and Intel) and Linux (manylinux x86_64). Import it and you have the whole model:
import kinogaki as kg
doc = kg.Document()
doc.append("/world", "group")
doc.append("/world/mat", "material").set("out", (0.9, 0.15, 0.15))
doc.append("/world/ball", "object") \
.set("radius", 1.5) \
.set("albedo", (1, 1, 1))
doc.connect("/world/mat.out", "/world/ball.albedo") # source output drives target input
text = kg.serialize(doc) # the .prisma ASCII
again = kg.deserialize(text) # bytes back to a Document
print(again.eval("/world/ball.albedo")) # (0.9, 0.15, 0.15) — resolved through the connection
The model mirrors the C++ API one to one: a Document is a tree of typed Elements you shape with append and read back with edit. The difference is the values. C++ wraps them in Float, Float3, Str; Python uses native values and infers the Prism type from them.
The idiom: Document plus chainable Handle
Document.append(path, type) adds an Element and returns a Handle — a chainable cursor at that path. set writes a typed property and returns the Handle again, so edits chain. edit(path) returns a Handle to an Element that already exists.
doc = kg.Document()
doc.append("/scene", "group")
doc.append("/scene/cam", "camera") \
.set("fov", 35.0) \
.set("position", (0.0, 1.6, 5.0)) \
.set_meta("note", "hero shot") # string metadata, separate from typed properties
The Prism type follows the native value: a bool is a bool, an int is an int, a float is a float, a str is a string. A sequence of 2, 3, 6, or 32 numbers becomes a float2, float3, matrix, or spectrum. Any other length raises TypeError, loudly, at the call site.
A Handle re-resolves its Element by path on every call, so it stays valid across renames and further edits — exactly like the C++ handle.
A worked example
Build a small document, set typed properties, wire a slot, animate, and write it out:
import kinogaki as kg
doc = kg.Document()
doc.append("/world", "group")
# A material whose output drives the ball's colour.
doc.append("/world/red", "material").set("out", (0.9, 0.15, 0.15))
ball = doc.append("/world/ball", "object")
ball.set("radius", 1.5)
ball.set("albedo", (1, 1, 1)) # a default, until the connection overrides it
doc.connect("/world/red.out", "/world/ball.albedo")
# Keyframe the radius: it grows over four seconds.
ball.animate("radius", {0.0: 1.0, 4.0: 2.5}, interp="linear")
# Read back through connections and time.
print(doc.eval("/world/ball.albedo")) # (0.9, 0.15, 0.15)
print(doc.eval("/world/ball.radius", time=2.0)) # 1.75 — halfway
# Serialize, then load again.
text = kg.serialize(doc)
print(text)
clone = kg.deserialize(text)
assert clone == doc # Documents compare by value
animate(key, samples, interp) takes a {time: value} mapping (or an iterable of (time, value) pairs). Each value is a float or a 3-tuple. interp is "linear" (the default), "held", or "bezier". The time axis lives on every numeric property; scalars and float3s interpolate component-wise.
Codec I/O
A Document reads and writes any format the codecs cover, straight off the object. Codec.AUTO picks the codec from a file's extension.
doc = kg.Document()
doc.load("scene.prisma") # native ASCII, by extension
doc.load("notes.md") # Markdown, normalized into the model
html = doc.to_string(kg.Codec.HTML) # render to a string
doc.save("out.html", kg.Codec.HTML) # write a file
doc.save("scene.prism") # binary native, by extension
doc.load_string(md_text, kg.Codec.MARKDOWN) # parse in-memory bytes
load and load_string replace the document's contents and return False on a read or parse failure. to_string renders to a str for text codecs and bytes for PRISM_BINARY and BLOB. The Codec enum is AUTO, PRISM (.prisma ASCII), PRISM_BINARY (.prism binary), JSON, MARKDOWN, HTML, SVG, TEXT, BLOB.
For the canonical native bytes, the module-level serialize and deserialize are the shortest path:
ascii_bytes = kg.serialize(doc) # str (.prisma)
binary_bytes = kg.serialize(doc, binary=True) # bytes (.prism)
doc = kg.deserialize(binary_bytes) # format auto-detected
Reading values back
Every Handle carries two read families. The permissive get_* getters return a default when the value is absent or the wrong type. The strict require_* getters raise PrismError — a located, loud failure — so a consumer never silently gets the wrong shape.
h = doc.edit("/world/ball")
r = h.get_float("radius", 1.0) # permissive, with a default
rgb = h.require_float3("albedo") # raises PrismError if absent or mistyped
note = h.get_meta("note", "") # string metadata
r_at_2 = h.get_float("radius", time=2.0) # any getter takes a time
The getters are get_float, get_int, get_bool, get_float2, get_float3, get_str, and get_meta; the strict pair shipped today is require_float, require_float3, and require_str. These read the stored value at a slot. To resolve a value through connections and time samples, use the evaluator — doc.eval(slot, time) or the module-level kg.evaluate(doc, slot, time) — which returns a native value or None when the slot is unresolvable.
Walk the document as plain data:
for el in doc.elements(): # Element views, in document order
print(el.path, el.type)
for el in doc.find("/world/**/camera"): # glob: * one segment, ** any depth
print(el.path)
for c in doc.connections(): # Connection views
print(c.source, "→", c.target)
Composition
A document can reference another file's subtree; composition flattens the whole graph into one Document, resolving references relative to each file's directory.
# car.prisma references wheel.prisma four times.
car = kg.compose_file("car.prisma") # one flat Document, every reference inlined
def group "car" {
def object "wheel_fl" { reference "wheel.prisma" }
def object "wheel_fr" { reference "wheel.prisma" }
}
compose_file raises PrismError on a missing or malformed file or a reference cycle; it is for trusted document sets. To author a reference, use the Handle: doc.edit("/car/wheel_fl").reference("wheel.prisma"), or point at one Element inside the file with .reference("parts.prisma", "/parts/bolt"). Local properties override the referenced ones. See composition for the full model.
What is not yet bound
The Python surface covers authoring, codec I/O, evaluation through connections and time, and composition. A few corners of the C++ API are not bound yet — each is a tracked issue, not a dead end:
- The node-registry Evaluator (custom node types that compute outputs).
doc.evalresolves connections and time samples; it does not yet run a registered node graph. - Geometry-array accessors — the bulk read and write paths for
float2_array,int_array, andfloat_arrayproperties. - Matrix and spectrum getters — you can
seta 6- or 32-number value, but the typed read-back for matrix and spectrum is not exposed. - The derived reads
world_matrixandis_visiblethat the C++ API computes from the transform hierarchy.
Reach for the C ABI or the C++ API if you need one of these today.
Reference
This page is the tour. For the exhaustive, method-by-method detail, see the per-class reference: Document and Element.