Skip to main content

Quillmark: Python bindings

Python bindings for Quillmark, a schema-driven document engine.

Maintained by TTQ.

Installation

pip install quillmark

The package is typed: it ships py.typed and stubs for the whole surface.

Quick Start

from quillmark import Quillmark, Quill, Document, OutputFormat

engine = Quillmark()                       # backend registry + render dispatcher
quill = Quill.from_path("path/to/quill")   # portable, declarative config data

markdown = """~~~
$quill: my_quill
$kind: main
title: Hello World
~~~

# Hello
"""

parsed = Document.from_markdown(markdown)
result = engine.render(quill, parsed, OutputFormat.PDF)
result.artifacts[0].save("output.pdf")

API surface

Field I/O flows through quill.writer(doc) and quill.reader(doc), the schema-bound write/read front doors. Document carries the quill-free surface: parse, storage, structure, $ext / $seed, and remove_field. Names follow snake_case, and the shared model (the Document / Card shapes, Diagnostics, the storage DTO) is identical to @quillmark/wasm's.

Quillmark

engine = Quillmark()
engine.registered_backends()              # ['typst', 'acroform'] (order not guaranteed)
engine.render(quill, parsed, OutputFormat.PDF)   # ppi=, pages= optional
engine.supported_formats(quill)           # [OutputFormat.PDF, ...] (raises if backend unregistered)

Quill

quill = Quill.from_path("path/to/quill")  # pure config load: no backend resolved here

quill.backend_id            # "typst" (declared backend)
quill.blueprint             # auto-generated annotated Markdown blueprint
quill.schema                # structured dict of the quill's document schema
quill.metadata              # `name`, `version`, `backend`, `author`, `description`, extras sorted
quill.quill_ref             # "name@version"

doc     = quill.parse(markdown)           # the bound door: parse + conform, the primary ingestion path
diags   = quill.conform(doc)              # the same walk in place on a transported document ([] = at rest)
diags   = quill.validate(parsed)          # list of validation::* diagnostic dicts ([] = valid)
seed    = quill.seed_document()           # starter Document seeded from `example:` values
main    = quill.seed_main()               # just the $kind: main card (dict, like doc.main)
card    = quill.seed_card("note")         # one starter composable card (dict), None if kind undeclared

writer  = quill.writer(doc)               # schema-bound typed write front door
reader  = quill.reader(doc)                 # schema-bound interpreted read front door

Writer: quill.writer(doc)

Resolves each field's type from the bound quill, so a name the schema does not declare is a typo (UnknownField), not a fallback. Holds both handles by reference and owns neither: bind, write, discard.

w = quill.writer(doc)
w.set("title", "On Taro")                 # typed-commit one field (mismatch raises now)
w.set_all({"title": "T", "author": "A"})  # atomic batch; one diagnostic per bad field
w.revise_body("A **taro** essay.")        # body write (edit semantics; a body has no field schema)
w.revise_field("bio", "make it **bold**") # typed *and* anchor-preserving content write (codec by declared type)
w.add_card("quotes", {"author": "Basho"}, "…", at=None)  # make + typed commit + insert (at appends/inserts)
w.remove_card(0)
w.set("author", "Issa", card=0)           # every verb takes card=: None is main, an int the composable card

Reader: quill.reader(doc)

The read twin of Writer. get reads each field in the values form: every content leaf in its type tree as its codec's text (a richtext leaf to markdown, a plaintext leaf to its literal text), everything else as stored, never coerced. get_content is the same read at the other end of the codec, handing back the field's Content as a dict whichever lane stored it.

v = quill.reader(doc)
v.get("bio")                              # richtext → markdown str; scalar → as stored; absent or present-null → None
                                          # undeclared name raises UnknownField; undecodable content raises FieldDecode
v.get_content("bio")                      # the `Content` dict {text, lines, marks, islands}; absent → None
                                          # a type that is not a content leaf raises FieldNotContent
v.body_markdown()                         # the main body markdown (quill-free body read)
v.get("author", card=0)                   # a card field, interpreted by its $kind schema
v.body_markdown(card=0)
doc.card(0)["kind"]                       # the composable card's $kind

RenderResult / Artifact

result.artifacts            # [Artifact, ...]
result.warnings             # [Diagnostic, ...]
result.format               # OutputFormat

artifact.format             # OutputFormat
artifact.bytes              # bytes
artifact.mime_type          # 'application/pdf', 'image/svg+xml', ...
artifact.save("out.pdf")

Document

doc = Document("my_quill")                       # blank canvas: $quill only, no fields, no cards
doc = Document.from_markdown(markdown)
emitted = doc.to_markdown()

stored   = doc.to_stored()
restored = Document.from_stored(stored)

Document.storage_version_of(blob)                # raw tag (incl. unknown futures); None when not a DTO
Document.current_storage_version()               # what this build writes

Document.format_rules()                          # card-yaml authoring rules (static text)
Document.quill_ref_hint()                        # $quill reference grammar (static text)
Document.blueprint_instruction("taro")           # a quill blueprint's fill obligation (no tool name)

doc.clone()
doc.equals(other)
doc.card_count
doc.main; doc.cards; doc.body; doc.warnings      # total-read snapshots (dicts); body is a content dict
doc.card(0)                                      # one card, same dict shape as main (out of range raises)
doc.seed_overlay("note")                         # one $seed[kind] overlay, or None
doc.set_quill_ref("other@1.0")

# Structure (quill-free, a card kind is a name, not a schema fact):
doc.insert_card({"kind": "note", "body": "..."}, at=None)   # at appends/inserts
doc.insert_card({"kind": "note",                            # fields → payload items
                 "payload_items": [{"type": "field", "key": "x", "value": 1}]})
doc.remove_card(0)                               # returns the Card dict, or None
doc.move_card(2, 0)
doc.remove_field("title")                        # remove has no lane; card=i targets a composable card

# Out-of-band consumer state (never rendered):
doc.store_ext({"agent": {"pinned": True}})       # whole $ext map; card=i for a composable card
doc.store_ext({**doc.main["ext"], "agent": {"n": 1}})   # one namespace: merge, then store
doc.remove_ext()                                 # every namespace at once
doc.store_seed_overlay("note", {"tag": "T"})     # per-kind $seed overlay; new cards spawn with it
doc.remove_seed_overlay("note")

Setting a field's value is the writer's job (quill.writer(doc).set(...)): a field write needs the schema, and Document is quill-free. Reading a field's interpreted value is the reader's (quill.reader(doc).get(...)).

Schema model

A field carries two independent axes, and no required: one on FieldSchema.

Value — what the cell holds. With a default:, the blueprint renders that value under a type-only # <type> annotation and the render path uses it when the document omits the field. Without one, an example takes the cell as a suggested value, and an absent field blank-fills.

Obligation — whether a human must author the field, read off default:'s absence: a defaulted field asks nobody, a defaultless one asks. An obliged field carries the !must_fill marker in the blueprint, and validation emits the non-fatal validation::must_fill warning while the document leaves it unauthored — from either of two triggers, named by the diagnostic's trigger arg: marker for a marker the document still carries, unauthored for a cell the schema obliges and the document never filled. Authoring the field's blank discharges the obligation; clearing the key does not.

Neither axis gates render. Partial documents are accepted, and engine.render(quill, doc) raises only for malformed input.

Error contract

An engine refusal raises QuillmarkError, carrying a non-empty .diagnostics list of Diagnostic objects. An argument the binding cannot read at all — a non-finite float, a value with no JSON form, a malformed path sequence — raises ValueError before the engine is called, described by no diagnostic.

try:
    Document.from_markdown(bad_md)
except QuillmarkError as exc:
    for d in exc.diagnostics:
        print(d.severity, d.code, d.message, d.path)
        print(str(d))   # canonical pretty-printed text (matches CLI / WASM)

Mutator failures (invalid field names, kind names, out-of-range indices) carry a namespaced edit::* code on diagnostics[0]: edit::invalid_field_name, edit::unknown_field, edit::index_out_of_range, edit::field_coercion_failed, …. insert_card refuses a card's contents under those same codes. Route on diagnostics[0].code, never on message text.

Card and page indices count from the front, so a negative one addresses nothing rather than the last card or page: it takes the same answer as an index past the end — edit::index_out_of_range where the verb raises, None where remove_card answers absence.

Each refusal anchors at a rooted document path: main.<field> for a main field, cards.<kind>[<i>].<field> for a card field, cards[<i>] for a structural out-of-range op.

Changelog

See the changelog and the GitHub Releases page for release notes and version history.

Development

uv venv
source .venv/bin/activate
uv pip install maturin pytest
maturin develop
pytest

maturin develop builds the extension in the debug profile. Stay in the activated venv: uv sync, uv run, and pip install -e each sync the project through maturin's PEP 517 backend, a release build that replaces the debug .so.

python/quillmark/_quillmark.pyi is hand-written and must track src/. No CI job gates it, so after changing the surface run the stub against the built module yourself:

uv pip install mypy
python -m mypy.stubtest --ignore-disjoint-bases quillmark

License

Apache-2.0

Metadata

Release files for quillmark 0.113.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for quillmark 0.113.0
File Size Uploaded Provenance
quillmark-0.113.0.tar.gz 1.6 MB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for quillmark 0.113.0
File
quillmark-0.113.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
quillmark-0.113.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
quillmark-0.113.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
quillmark-0.113.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details

Total release size: 68.4 MB

Release files / quillmark-0.113.0.tar.gz

Download URL quillmark-0.113.0.tar.gz
Size 1.6 MB
Tags Source
SHA-256 checksum
How to use checksums
56e951cae4a7aea1c88541759df87572fdbcd2c1b91227e3f10e3ae27ebf51ee
BLAKE2b-256 checksum
How to use checksums
08632738744c76df3163fadc3d8f4aa1c320c119158ea675767e7cb1f41adfb3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / quillmark-0.113.0-cp310-abi3-win_amd64.whl

Download URL quillmark-0.113.0-cp310-abi3-win_amd64.whl
Size 16.6 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
e492462379afeb7ceb08f82337a57ddc011f19fbea9557a189cf5edc59d41341
BLAKE2b-256 checksum
How to use checksums
ca9698875c11d3a1e17ef6ec8e335ed74ed50d350f7410698ca42c29e8874c48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / quillmark-0.113.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL quillmark-0.113.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 17.7 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
e1d903c819f7bf80f575163fa70bb84f78effda2f6b7a0794feb524ddd2b83cc
BLAKE2b-256 checksum
How to use checksums
9382af51fcbeb4518a9391884cf5355690c53d2b7c3751418a51e6fae2b17e77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / quillmark-0.113.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL quillmark-0.113.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 16.9 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
258a38efa304b538431413d33027bb4a3ab6a5e137557bed29b918d8ca376460
BLAKE2b-256 checksum
How to use checksums
43634de878c9dde9963c3a3cd18ce9f2340613ef303f8194d0e6b4745c011b81
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / quillmark-0.113.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL quillmark-0.113.0-cp310-abi3-macosx_11_0_arm64.whl
Size 15.6 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
d020b0e438eb2a22ab73fff42dcd75e9e9eba4942b6f9e35ac02c0aaa339557b
BLAKE2b-256 checksum
How to use checksums
7e0e0451554e98155c1d4ee7977f091c201ceec1331c35093cd377e63533a428
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Security for quillmark 0.113.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 5 files in this release have been uploaded with provenance metadata.

Select from 5 files

Attestation for quillmark-0.113.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.113.0 This release

5 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page