Quillmark — Python bindings
Python bindings for Quillmark, a schema-driven document engine.
Maintained by TTQ.
Installation
pip install quillmark
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
Python is a Tier-1 binding: 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. There is no opaque field store and no anchor-preserving content
lane (install / revise / apply_change + the import_markdown /
export_markdown / rebase / map_pos codec); those are WASM-only by scope,
serving live editors that Python does not target. See
prose/canon/BINDINGS.md.
Names follow snake_case; the shared model (the Document / Card shapes,
Diagnostics, the storage DTO) is identical to the
@quillmark/wasm package's. Python renders in one shot via
engine.render; the iterative render-session and canvas-preview surface is
WASM-only (see prose/canon/PREVIEW.md).
Capability principle: a Quill is portable, declarative config data —
quill.metadata is a pure, infallible snapshot of the quill: section.
The format probe (supported_formats) and rendering (render) are resolved
by the engine, against a quill; they raise QuillmarkError
(code engine::backend_not_found) only if the declared backend isn't
registered.
Quillmark
engine = Quillmark()
engine.registered_backends() # ['typst', 'pdfform'] (order not guaranteed)
engine.render(quill, parsed, OutputFormat.PDF) # ppi=, pages=, producer= 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 # pure config snapshot of the quill: section (never raises)
quill.quill_ref # "name@version"
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)
The typed write front door. 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 — ephemeral by convention: 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.set_body("A **taro** essay.") # typed body write (edit semantics)
w.revise_field("bio", "make it **bold**") # typed *and* anchor-preserving richtext write
w.add_card("quotes", {"author": "Basho"}, "…", at=None) # make + typed commit + insert (at appends/inserts)
w.remove_card(0)
w.card(0).set("author", "Issa") # a CardWriter: .index, .kind, .set, .set_all, .set_body, .revise_field
Reader — quill.reader(doc)
The interpreted read front door and the read twin of Writer. One get reads
each field by its declared type: a richtext field to its markdown projection,
every other type its canonical value verbatim.
v = quill.reader(doc)
v.get("bio") # richtext → markdown str; scalar → its value; absent → None
# undeclared name raises UnknownField; undecodable content raises FieldRichtextDecode
v.get_body() # the main body markdown (quill-free body read)
v.card(0).kind # the composable card's $kind
v.card(0).get("author") # a card field, interpreted by its $kind schema
v.card(0).get_body()
RenderResult / Artifact
result.artifacts # [Artifact, ...]
result.warnings # [Diagnostic, ...]
result.format # OutputFormat
result.render_time_ms # float
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_json()
restored = Document.from_json(stored)
maybe = Document.try_from_json(blob) # None when not a DTO
Document.schema_version_of(blob) # raw tag (incl. unknown futures)
Document.current_schema_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") # LLM/MCP blueprint header for a quill
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.set_quill_ref("other@1.0")
# Structure (quill-free — a card kind is a name, not a schema fact):
doc.insert_card(Document.make_card("note", {"x": 1}, "..."), at=None) # at appends/inserts
doc.remove_card(0) # returns the Card dict, or None
doc.move_card(2, 0); doc.set_card_kind(0, "summary")
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_namespace("agent", {"n": 1}) # one slot, siblings preserved; card=i too
doc.remove_ext_namespace("agent"); doc.remove_ext()
doc.store_seed_namespace("note", {"tag": "T"}) # per-kind $seed overlay; new cards spawn with it
doc.remove_seed_namespace("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's cell is inferred from whether the schema declares a default::
- Unendorsed (no
default:) — the blueprint renders the!must_fillmarker (carrying the field'sexampleas a suggested value when one exists). An absent Unendorsed field zero-fills silently. A!must_fillmarker left in the document is non-fatal: it emits thevalidation::must_fillwarning and still renders. Partial documents are accepted;engine.render(quill, doc)only raises for malformed input. - Endorsed (with
default:) — the blueprint renders the default value with a type-only# <type>annotation (shippable as-is), and the default is used when the document omits the field.
There is no required: axis on FieldSchema.
Error contract
A single exception type — QuillmarkError — is raised for every failure
mode. Every raised exception carries a non-empty .diagnostics list of
Diagnostic objects. This matches the WASM binding's contract.
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_conform, … —
the same taxonomy WASM uses. Route on diagnostics[0].code, never on message
text.
Changelog
See the changelog and the GitHub Releases page for release notes and version history.
Development
uv venv
uv pip install -e ".[dev]"
uv run pytest
License
Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quillmark-0.96.0.tar.gz.
File metadata
- Download URL: quillmark-0.96.0.tar.gz
- Upload date:
- Size: 2.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58ef5d2fad25da7d4eb7cc12a9206bd38080158e00e9cdfdabe77ec103b0ff8f
|
|
| MD5 |
e3172fd3eb788fc72debee71069b529c
|
|
| BLAKE2b-256 |
3f23a4042d16ccc8468b02b74c01cc8d646e0df8c706ebcab398609faceb361a
|
Provenance
The following attestation bundles were made for quillmark-0.96.0.tar.gz:
Publisher:
release.yml on borb-sh/quillmark
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quillmark-0.96.0.tar.gz -
Subject digest:
58ef5d2fad25da7d4eb7cc12a9206bd38080158e00e9cdfdabe77ec103b0ff8f - Sigstore transparency entry: 2222403915
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/borb-sh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.96.0-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: quillmark-0.96.0-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 16.1 MB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f63c1406e0e23c990608cf1700e21e2ce797706d68671083249d86355d7ae6c
|
|
| MD5 |
640603fb87de9707e1a9be35b8df8bd7
|
|
| BLAKE2b-256 |
995a23d64bc07e321ade17522e8e223cb881c3ac06c8085e093f14277d89a978
|
Provenance
The following attestation bundles were made for quillmark-0.96.0-cp310-abi3-win_amd64.whl:
Publisher:
release.yml on borb-sh/quillmark
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quillmark-0.96.0-cp310-abi3-win_amd64.whl -
Subject digest:
3f63c1406e0e23c990608cf1700e21e2ce797706d68671083249d86355d7ae6c - Sigstore transparency entry: 2222404331
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/borb-sh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.96.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: quillmark-0.96.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 17.2 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5db50be1ef31895fddcabef1aed44772eb6cd2e8150222c3be2d5238b025c13d
|
|
| MD5 |
a624a0c919766cdfcb11e3ec493a0d4d
|
|
| BLAKE2b-256 |
353674e290848732e9cb9527a8183fbc545a3b2c40583c16c43d1765aa91d6eb
|
Provenance
The following attestation bundles were made for quillmark-0.96.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on borb-sh/quillmark
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quillmark-0.96.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
5db50be1ef31895fddcabef1aed44772eb6cd2e8150222c3be2d5238b025c13d - Sigstore transparency entry: 2222404971
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/borb-sh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.96.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: quillmark-0.96.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 16.5 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2ae1bf3780fc5e9845c1ac1afd25da975ee63b1b3f210c39585a1f1b35e54f6
|
|
| MD5 |
259a88d190dc396245466465d7ca2eed
|
|
| BLAKE2b-256 |
f88eb4a9d86d06dc90e8535f5257671ba33c6033bd14e30923972c7a03d2aed3
|
Provenance
The following attestation bundles were made for quillmark-0.96.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on borb-sh/quillmark
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quillmark-0.96.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
d2ae1bf3780fc5e9845c1ac1afd25da975ee63b1b3f210c39585a1f1b35e54f6 - Sigstore transparency entry: 2222404719
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/borb-sh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.96.0-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: quillmark-0.96.0-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 15.1 MB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80afa25c3aedccee38d0805253064d8aa8e8c6ef5d89471008d0acb4aaec3581
|
|
| MD5 |
ec54d4d994a86176f108757900b45b03
|
|
| BLAKE2b-256 |
7c71505083b7efaa7cc8ad0bc87505f3fac8014b578e7706b57af02fcc5994dc
|
Provenance
The following attestation bundles were made for quillmark-0.96.0-cp310-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on borb-sh/quillmark
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quillmark-0.96.0-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
80afa25c3aedccee38d0805253064d8aa8e8c6ef5d89471008d0acb4aaec3581 - Sigstore transparency entry: 2222405260
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/borb-sh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@725c022e6f8d1b619bff53bf483a0ebd1f09e2a8 -
Trigger Event:
pull_request
-
Statement type: