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:

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.