Python SDK reference
The indiek package is the reference implementation of the IndieK archive format. Install it with uv add indiek (or pip install indiek); it needs Python 3.14 or newer and depends only on pydantic and pynacl. The examples below form one story: each builds on the previous ones.
Quickstart
from indiek import Archive, Author
author = Author.generate() # keep author.seed_hex secret: it is your signing key
ik = Archive.open("my-archive", author=author) # the folder is created on first write
theorem = ik.create_node("Gödel's first incompleteness theorem", title="Gödel I")
statement = ik.create_node(
"Any consistent formal system able to express arithmetic is incomplete."
)
ik.create_edge(statement, theorem, "states")
tag = ik.create_tag("theorem", "A proven statement", kind="node")
ik.set_tag(tag, theorem)
print(ik.index.alias_of(theorem)) # e1
seal = ik.seal()
print(seal.hash) # sha256:… — the one string that commits to the whole archive
The public API is everything exported by indiek (indiek.__all__). The
modules under indiek.core and indiek.ops are the building blocks behind
it; they are documented in their docstrings but are not part of the stable
API.
from indiek import (
Archive, Author, # entry points
ArchiveIndex, IndexedItem, ItemKind, TranscriptionRecord, # the projection
Seal, FsckReport, IngestResult, # results
VerificationLevel, MediaType, ResourceType, EventType, MetaEventType,
IndieKError, IntegrityError, UnknownItemError, AuthorshipError, ArchiveRuleError,
__version__,
)
Author
from indiek import Author
author = Author.generate() # a new random key
same = Author.from_seed_hex(author.seed_hex) # load from 64 hex characters
assert same.public_key_hex == author.public_key_hex # what events record as author
| Member | Description |
|---|---|
Author.generate() |
New random Ed25519 identity. |
Author.from_seed_hex(seed_hex) |
Load from a 32-byte seed as 64 hex characters; ValueError if malformed. |
public_key_hex |
Lowercase hex public key (64 characters). |
seed_hex |
Lowercase hex seed — the secret. |
sign(message: bytes) -> bytes |
Raw 64-byte Ed25519 signature. |
repr(author) shows only the public key.
Archive.open
Archive.open(
path, # str | Path; "~" is expanded; may not exist yet
author=None, # Author | None; None opens read-only
*,
verify="incremental", # "none" | "incremental" | "full" (or VerificationLevel)
trusted_seal=None, # a seal hash you obtained separately
cache_dir=None, # verification cache; default: IK_CACHE_DIR or the OS cache dir
) -> Archive
Opening reads and verifies both event logs, the blob references and the seal
log (see Verification levels),
then builds the in-memory index. A folder that does not exist is an empty
archive; it is created on the first write. Raises IntegrityError if the
archive is corrupted or tampered with, or if trusted_seal is not in its
seal log; ValueError for an unknown verify level.
from indiek import Archive, VerificationLevel
reader = Archive.open("my-archive") # read-only, incremental
everything = Archive.open("my-archive", verify="full") # checks every signature
raw = Archive.open("my-archive", verify=VerificationLevel.NONE) # parse only, read-only
trusted = Archive.open("my-archive", trusted_seal=seal.hash)
Reading
| Member | Description |
|---|---|
path |
The archive folder. |
author |
The Author used for writes, or None. |
verification |
The VerificationLevel the archive was opened with. |
head / meta_head |
Hash (sha256:…) of the last base / meta event, or None. |
index |
The ArchiveIndex: current state of every item. |
events(network="base") |
Iterate the raw events (dicts) of the "base" or "meta" log, in order. |
seals() |
Every verified Seal, oldest first. |
refresh(level=None) |
Re-read and re-verify from disk (e.g. after another process wrote). |
Items are looked up by alias or UUID; their body is the JSON body of their
latest event. Content is read from the blob store, which re-hashes every blob
it returns:
from indiek import ItemKind
item = reader.index.get("e1")
print(item.kind, item.alias, item.body["title"]) # node e1 Gödel I
print(reader.index.store.blobs.get_text(item.body["blob_hash"]))
for item in reader.index.items.values():
if item.kind is ItemKind.EDGE:
source = reader.index.get(item.body["source_node"])
target = reader.index.get(item.body["target_node"])
label = reader.index.store.blobs.get_text(item.body["blob_hash"])
print(f"{source.alias} --{label}--> {target.alias}") # e2 --states--> e1
for event in reader.events("meta"):
print(event["body"]["event_type"]) # create_node_tag, create_node_representation, …
Use blobs.get(hash) for binary content (bytes). Both raise
IntegrityError("File corrupted or missing! (blob …)") if the file is gone
or altered.
Writing nodes and edges
Every write needs an author (AuthorshipError: An author is required to write
to an archive) and an archive not opened with verify="none"
(ArchiveRuleError: Writes need verification: …). Each write appends signed
events and refreshes index, so the new item’s alias is usable immediately.
from pathlib import Path
from indiek import MediaType, ResourceType
note = ik.create_node("# Proof sketch\n\nBy diagonalisation.", media_type=MediaType.TEXT_MARKDOWN)
diagram = ik.create_node(
Path("figures/diagram.png").read_bytes(), media_type=MediaType.IMAGE_PNG, title="Diagram"
)
paper = ik.create_node(
resource_type=ResourceType.URI,
uri="https://arxiv.org/abs/1234.5678",
title="Survey",
media_type=MediaType.APPLICATION_PDF_A, # optional hint
)
scan = ik.create_node(resource_type=ResourceType.LOCAL_FILE, path="scans/page.tiff")
proves = ik.create_edge(note, statement, "sketches a proof of")
create_node(content=None, *, media_type=None, title=None, resource_type=ResourceType.MANAGED, uri=None, path=None) -> UUID
managed(default):contentis stored in the blob store. Astris always taken literally — never read as a file path — and defaults totext/plain;bytesrequiremedia_type. Errors:ArchiveRuleErrorcontent is required for managed nodes,media_type is required for bytes content.uri: recordsuri, never fetched;media_typeis an optional hint. Error:uri is required for uri nodes.local_file: records the absolute resolvedpath, the file size, its SHA-256 and the time; the media type is detected from the file name (.txt,.md,.png,.jpg/.jpeg,.tif/.tiff) and the title defaults to the file stem. Errors:path is required for local_file nodes,Local file does not exist or is not a regular file: <path>,Unsupported archival media type for <path>.
create_edge(source, target, content) -> UUID — a directed edge between two
existing base nodes, carrying content as text/plain (stored
verbatim). Endpoints are checked before anything is written:
UnknownItemError Unknown ID or alias '<ref>',
SOURCE NODE <uuid> NOT FOUND IN ARCHIVE, TARGET NODE <uuid> NOT FOUND IN ARCHIVE
(edges and tags can never be endpoints). Self-loops are allowed.
Revising: supersede
ik.supersede_node(theorem, "Gödel's *first* incompleteness theorem (1931)")
ik.supersede_node(theorem, "Gödel's first incompleteness theorem", title="Gödel 1931")
ik.supersede_edge("e3", "states, in modern terms")
supersede_node(node, content, *, title=None, media_type=MediaType.TEXT_MARKDOWN) -> UUID
replaces a node’s content with a managed blob; title=None keeps the current
title. supersede_edge(edge, content) -> UUID replaces an edge’s
text/plain content; endpoints and title are kept. Both return the item’s
UUID, which does not change. Errors: UnknownItemError
Node '<ref>' does not exist in this archive,
ID '<ref>' does not reference a base-network node (and the edge
equivalents); AuthorshipError Only the author of Node '<ref>' may supersede it
(checked before anything is written).
Tags
proof = ik.create_tag("proof", "A proof relation", kind="edge")
ik.set_tag(proof, proves)
print(ik.index.items[proves].tags == {proof}) # True
create_tag(name, description, *, kind) -> UUID defines a tag for
kind="node" or kind="edge" (ArchiveRuleError otherwise). Names are not
unique.
set_tag(tag, target) -> UUID assigns a tag to a base node or edge and
returns the UUID of the assignment (a meta edge). The first assignment to a
target also appends a representation of it. Errors:
| Error | Message |
|---|---|
UnknownItemError |
Unknown ID or alias '<ref>'; UUID is not part of any network (neither base nor meta) |
ArchiveRuleError |
<uuid> is not a tag node; UUID should not be an item from the meta network |
AuthorshipError |
Only the tag's author may use this operation |
ArchiveRuleError |
A node tag should point to a node representation; An edge tag should point to an edge representation |
When the kind check fails on a first assignment, the representation already written stays in the meta log (harmless, reused later) and a WARNING says so. Assigning the same tag twice appends a second assignment; the item’s tag set is unchanged.
Bulk import, export and the manifest
result = ik.ingest(input_folder="more") # one managed node per folder entry
cites = result.node_ids[0]
Path("edges.csv").write_text(f"{cites},{theorem},cites\n", encoding="utf-8")
ik.ingest(edges_file="edges.csv") # one edge per line: source,target,content
ik.export_graphml("theorems.graphml", tags=[tag]) # base network as GraphML
ik.write_manifest() # MANIFEST.json, SPEC.md, test_vectors.json
ingest(*, input_folder=None, edges_file=None) -> IngestResult— every entry ofinput_folder(sorted, not recursive) becomes a managed node titled by its stem (text files as text, images as bytes); then every linesource,target,contentofedges_filebecomes an edge (split on the first two commas; endpoints may be UUIDs or aliases). Stops at the first error; earlier writes are kept. See Known limitations.export_graphml(output_path, tags=()) -> Path— writes the base network as pretty-printed, directed GraphML (overwritten). Withtags, keeps items carrying at least one of them.write_manifest() -> Path— writesMANIFEST.json(layout, rules, JSON Schemas of every log line, algorithms used, the current author’s key) and copiesSPEC.mdandtest_vectors.jsonat the archive root; returns the manifest path. Works without an author.
These three, like seal(), verify() and fsck(), run a full
verification first.
Attesting: transcriptions
new_head = ik.transcribe(note="re-verified before publication")
record = ik.index.transcriptions["base"][-1]
print(record.position, record.body["verified_count"], new_head == ik.head)
transcribe(network="base", *, note=None) -> str appends to the "base" (or
"meta") log a transcription event recording that log’s verified head,
length and algorithms, and returns the new head. Any author may transcribe.
ArchiveRuleError: Nothing to transcribe: the <network> log is empty.
Seals, verification and health
second = ik.seal()
assert ik.verify(expect=seal.hash) == seal # the first seal is still in the log
assert ik.verify() == second # without expect: the latest seal
report = ik.fsck()
print(report.base_count, report.seal_count, report.unreferenced_blobs)
| Method | Description |
|---|---|
seal() -> Seal |
Fully verify, then append a seal of both logs and their blobs. Any author may seal. ArchiveRuleError: Nothing to seal: the archive is empty, Nothing to seal: the archive is unchanged since seal <n>, Cannot seal: <n> unreferenced blob(s) … Delete them and seal again: <paths>. |
verify(*, expect=None) -> Seal \| None |
Re-read and fully re-verify from disk. Returns the seal whose hash is expect (else IntegrityError: Expected seal not found in seal log) or, without expect, the latest seal (None if there is none). |
fsck() -> FsckReport |
Fully verify and report on the archive. |
unreferenced_blobs() -> list[Path] |
Files in the blob store that no event references (safe to delete). |
seals() -> list[Seal] |
Every verified seal, oldest first. |
The index
ArchiveIndex (archive.index) is the replayed state of the archive:
| Member | Description |
|---|---|
items: dict[UUID, IndexedItem] |
Every node, edge, tag, representation and tag assignment. |
aliases: dict[str, UUID] |
e{n} / m{n} → UUID. |
resolve(ref) -> UUID |
Alias, UUID string or UUID → UUID (UnknownItemError for garbage; a well-formed UUID is not checked). |
get(ref) -> IndexedItem |
The item (UnknownItemError if absent). |
alias_of(uuid) -> str |
The item’s alias. |
representation_of(uuid) -> UUID \| None / underlying_of(uuid) -> UUID |
Base item ↔ its meta representation. |
transcriptions: dict[str, list[TranscriptionRecord]] |
Per network, author, position (1-based) and body. |
head / meta_head |
As on Archive. |
store |
The verified on-disk store; store.blobs reads content (get, get_text). |
IndexedItem fields: id, alias, kind (an ItemKind), author (the
creator’s public key), body (dict of the latest event body), tags (set of
tag UUIDs, base items only); properties is_meta, is_edge, is_tag.
ItemKind: NODE, EDGE, NODE_TAG, EDGE_TAG, NODE_REPRESENTATION,
EDGE_REPRESENTATION, NODE_TAG_ASSIGNMENT, EDGE_TAG_ASSIGNMENT.
Result types
Seal:hash,seq,prev_seal_hash,base_head,base_count,meta_head,meta_count,blob_root,time,key.FsckReport:path,base_count,base_head,meta_count,meta_head,seal_count,latest_seal,transcriptions({"base": n, "meta": m}),blob_files,referenced_blobs,unreferenced_blobs(paths),untagged_representations.IngestResult:node_ids,edge_ids(UUIDs in creation order).
Enumerations
VerificationLevel:NONE,INCREMENTAL,FULL(values"none", …).ResourceType:MANAGED,URI,LOCAL_FILE.MediaType:TEXT_PLAIN,TEXT_MARKDOWN,IMAGE_TIFF,IMAGE_PNG,IMAGE_JPEG,APPLICATION_PDF_A(values are the media type strings).EventType(base):create_node,create_edge,supersede_node,supersede_edge,transcription.MetaEventType:create_node_tag,create_edge_tag,create_node_representation,create_edge_representation,set_node_tag,set_edge_tag,transcription.
Errors
IndieKError
├── IntegrityError the archive on disk is corrupted, tampered with or inconsistent
├── UnknownItemError an ID or alias does not resolve (also a ValueError)
├── AuthorshipError the current author may not do this (also a ValueError)
└── ArchiveRuleError the write would break a rule of the format (also a ValueError)
from indiek import IndieKError, UnknownItemError
try:
ik.create_edge("e1", "e99", "dangling")
except UnknownItemError as exc:
print(exc) # Unknown ID or alias 'e99'
except IndieKError:
raise
Every IntegrityError message is listed in
Integrity verification and in SPEC.md.