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, so
mypy, Pyright, and IDE completion see real signatures rather than Any.
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. The opaque field
store and the anchor-preserving content lane are WASM-only by scope, as are the
render session and canvas preview; Python renders in one shot via
engine.render. Names follow snake_case, and the shared model (the Document
/ Card shapes, Diagnostics, the storage DTO) is identical to
@quillmark/wasm's.
A Quill is portable, declarative config data, and quill.metadata a pure,
infallible snapshot of the quill: section. The engine resolves the declared
backend, so only the format probe (supported_formats) and render raise
engine::backend_not_found.
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"
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.card(0).set("author", "Issa") # a CardWriter: .index, .kind, .set, .set_all, .revise_body, .revise_field
Reader: quill.reader(doc)
The read twin of Writer. get reads each field by its declared type: a
richtext field to its markdown projection, a plaintext field to its literal text,
every other type verbatim. 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 → its value; absent → 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.card(0).kind # the composable card's $kind
v.card(0).get("author") # a card field, interpreted by its $kind schema
v.card(0).body_markdown()
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.storage_version_of(blob) # raw tag (incl. unknown futures)
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") # 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.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(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_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, declared by
must_fill: and deriving from default:'s absence when left unset. 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
Every failure raises QuillmarkError, carrying a non-empty .diagnostics list
of Diagnostic objects.
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,
…. 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
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
uv run python -m mypy.stubtest --ignore-disjoint-bases quillmark
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.107.0.tar.gz.
File metadata
- Download URL: quillmark-0.107.0.tar.gz
- Upload date:
- Size: 2.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a71061bda03af653e90ba26a6e08271e72909b66686141ffe5e09b80d41c2940
|
|
| MD5 |
c657f647d29add8b66d422c3d89f2318
|
|
| BLAKE2b-256 |
d71a3bff3214ae57a06d39e8d80b1a121d0a93f4abd559df2d12368e66987317
|
Provenance
The following attestation bundles were made for quillmark-0.107.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.107.0.tar.gz -
Subject digest:
a71061bda03af653e90ba26a6e08271e72909b66686141ffe5e09b80d41c2940 - Sigstore transparency entry: 2499686165
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
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@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.107.0-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: quillmark-0.107.0-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 16.4 MB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14837613bacf4b412a31b417019cd0d3f11e8c477cfdd291b416b321d535d8ac
|
|
| MD5 |
e92c7e6935c70ca81e500961fe85df58
|
|
| BLAKE2b-256 |
7566975970a941c73127f2ea17138ade89d6004f9c38155485a64fb472f603ec
|
Provenance
The following attestation bundles were made for quillmark-0.107.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.107.0-cp310-abi3-win_amd64.whl -
Subject digest:
14837613bacf4b412a31b417019cd0d3f11e8c477cfdd291b416b321d535d8ac - Sigstore transparency entry: 2499686171
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
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@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.107.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: quillmark-0.107.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 17.5 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a079096e1e458aecb51593f176f7fd165c78fd3c85529770f687083674f3d6e1
|
|
| MD5 |
81df53bb7287dbf356ab92fc88083b36
|
|
| BLAKE2b-256 |
65735e093e88532a9ee47ce45707c0875ee163c02cc35e75d6ce2f945612d27b
|
Provenance
The following attestation bundles were made for quillmark-0.107.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.107.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
a079096e1e458aecb51593f176f7fd165c78fd3c85529770f687083674f3d6e1 - Sigstore transparency entry: 2499686167
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
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@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.107.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: quillmark-0.107.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 16.8 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3a00c9bcbab5ac94022d104f294c4b35ff2ec00b95121de3b32d0763e242d3c
|
|
| MD5 |
56d806c7246c3733b5ed2b746e9b9f7b
|
|
| BLAKE2b-256 |
24cc5f9f4433d665cb9f4904cf05c2f089f07d6437e105d4600d45ef67363f05
|
Provenance
The following attestation bundles were made for quillmark-0.107.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.107.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
c3a00c9bcbab5ac94022d104f294c4b35ff2ec00b95121de3b32d0763e242d3c - Sigstore transparency entry: 2499686177
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
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@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file quillmark-0.107.0-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: quillmark-0.107.0-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 15.4 MB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3071e5060fb0a67bc9760857acbd124e662aac04e41df4a3c5a1aff811455542
|
|
| MD5 |
f95c2ff61fa57c45d3f1c8b6f7ba0902
|
|
| BLAKE2b-256 |
da960dfbb54dc1ebc8e03fdaaf1f573f1faaf570b7ca2657b9552ba6d0c56a65
|
Provenance
The following attestation bundles were made for quillmark-0.107.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.107.0-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
3071e5060fb0a67bc9760857acbd124e662aac04e41df4a3c5a1aff811455542 - Sigstore transparency entry: 2499686179
- Sigstore integration time:
-
Permalink:
borb-sh/quillmark@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
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@f3d72352c6c9233485feea1d9f38c0f951c50a27 -
Trigger Event:
pull_request
-
Statement type: