IndieK

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

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

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

Enumerations

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.