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, 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

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"

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)

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.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 interpreted read front door and 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 its canonical value 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's cell is inferred from whether the schema declares a default::

  • Unendorsed (no default:): the blueprint renders the !must_fill marker (carrying the field's example as a suggested value when one exists). An absent Unendorsed field zero-fills silently. A !must_fill marker left in the document is non-fatal: it emits the validation::must_fill warning 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_coercion_failed, …: 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

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 Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

quillmark-0.102.0-cp310-abi3-win_amd64.whl (16.3 MB view details)

Uploaded CPython 3.10+Windows x86-64

quillmark-0.102.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.5 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

quillmark-0.102.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (16.7 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

quillmark-0.102.0-cp310-abi3-macosx_11_0_arm64.whl (15.4 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

File details

Details for the file quillmark-0.102.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: quillmark-0.102.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 16.3 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

Hashes for quillmark-0.102.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 302638055b7c21108a216e1eceef6a41556c9058a7ed0829bfc0fd0357492151
MD5 0437197a192ddf8ed39828fd02491dc6
BLAKE2b-256 7ad1d115a9ae2654d6910ad03bef9db5e7ceb74cbad43869d44b0a39eae71d34

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.102.0-cp310-abi3-win_amd64.whl:

Publisher: release.yml on borb-sh/quillmark

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file quillmark-0.102.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for quillmark-0.102.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 eba86370595c14ad3bf0d04e58d3873a3130af73b609451b83920a0217ff5f8d
MD5 bc8caabf4ff80d7862514b3355ba0a05
BLAKE2b-256 8da799cf4c884a9b894057e320c0b9976afbb72105e058fe50445d42b4799bbe

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.102.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on borb-sh/quillmark

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file quillmark-0.102.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for quillmark-0.102.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 0feefa3be83f00fc31a2f542134d68e94b38d218d45a80c49ff6cb3d8194d714
MD5 5e5dd64055ead38b3bcfbcce28e0f12a
BLAKE2b-256 3add9fd457e8836baa0d5bc506bf37c416af07268f81f5b2f656cd1272d16d3d

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.102.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on borb-sh/quillmark

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file quillmark-0.102.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for quillmark-0.102.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e47edb5c5fee60ff2b6af62aa5d636758042814908bc78c10410963a9f03ef1b
MD5 8ea6072a6e0ab47e1e66269877bf9da0
BLAKE2b-256 ed05822f854226ea5e94d26e0b2c23c9e9b25a10f8b105fab46a9c57fb9aed54

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.102.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on borb-sh/quillmark

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.112.0

5 files

0.111.0

5 files

0.110.0

5 files

0.109.1

5 files

0.109.0

5 files

0.108.3

5 files

0.108.2

5 files

0.108.1

5 files

0.108.0

5 files

0.107.0

5 files

0.106.0

5 files

0.105.0

4 files

0.104.0

4 files

0.103.0

4 files

This release

0.102.0 This release

4 files

0.101.0

4 files

0.100.0

4 files

0.99.0

4 files

0.98.0

5 files

0.97.0

5 files

0.96.0

5 files

0.95.1

5 files

0.94.0

5 files

0.92.1

5 files

0.92.0

5 files

0.91.0

5 files

0.90.0

5 files

0.89.1

5 files

0.88.0

5 files

0.87.3

5 files

0.87.2

5 files

0.87.1

5 files

0.87.0

5 files

0.86.0

5 files

0.85.0

5 files

0.84.0

5 files

0.83.0

5 files

0.82.0

5 files

0.81.0

5 files

0.80.0

5 files

0.79.0

5 files

0.78.0

5 files

0.77.0

5 files

0.76.0

5 files

0.75.0

5 files

0.74.1

5 files

0.74.0

5 files

0.73.0

5 files

0.72.0

5 files

0.71.1

5 files

0.71.0

5 files

0.70.0

5 files

0.69.1

5 files

0.69.0

5 files

0.68.0

5 files

0.67.0

5 files

0.66.2

5 files

0.66.1

5 files

0.66.0

5 files

0.65.1

5 files

0.64.0

5 files

0.63.0

5 files

0.62.0

5 files

0.61.0

5 files

0.60.0

5 files

0.59.0

5 files

0.58.0

5 files

0.57.0

5 files

0.56.0

5 files

0.55.0

5 files

0.54.1

5 files

0.54.0

5 files

0.53.1

5 files

0.53.0

5 files

0.52.0

5 files

0.51.1

5 files

0.51.0

5 files

0.50.0

5 files

0.49.1

5 files

0.49.0

5 files

0.48.0

5 files

0.47.0

5 files

0.46.0

5 files

0.45.0

5 files

0.44.0

5 files

0.43.0

5 files

0.42.2

5 files

0.42.0

5 files

0.41.2

5 files

0.41.1

5 files

0.41.0

5 files

0.40.3

5 files

0.39.0

5 files

0.38.0

5 files

0.37.1

5 files

0.37.0

5 files

0.36.0

5 files

0.35.1

5 files

0.35.0

5 files

0.34.0

5 files

0.33.0

5 files

0.32.2

5 files

0.32.1

5 files

0.32.0

5 files

0.28.0

5 files

0.27.0

5 files

0.26.0

5 files

0.25.0

5 files

0.24.3

5 files

0.24.2

5 files

0.24.1

5 files

0.24.0

5 files

0.23.1

5 files

0.22.2

5 files

0.22.1

5 files

0.22.0

5 files

0.21.3

5 files

0.21.2

5 files

0.21.1

5 files

0.21.0

5 files

0.20.2

5 files

0.20.1

5 files

0.20.0

5 files

0.19.1

5 files

0.19.0

5 files

0.18.0

5 files

0.17.0

5 files

0.16.0

5 files

0.15.1

5 files

0.15.0

5 files

0.14.0

5 files

0.13.3

5 files

0.13.2

5 files

0.13.1

5 files

0.13.0

5 files

0.12.0

5 files

0.11.0

5 files

0.10.0

5 files

0.9.0

5 files

0.8.5

5 files

0.8.4

5 files

0.8.3

5 files

0.8.2

5 files

0.8.1

5 files

0.8.0

5 files

0.7.0

5 files

0.6.14

5 files

0.6.7

5 files

0.6.6

5 files

0.6.5

5 files

0.6.4

5 files

0.6.3

5 files

0.6.2

5 files

0.6.1

5 files

0.6.0

5 files

0.5.1

5 files

0.5.0

5 files

0.4.1

5 files

0.4.0

5 files

0.1.20

5 files

0.1.19

5 files

0.1.18

5 files

0.1.17

5 files

0.1.14

5 files

0.1.13

5 files

0.1.12

6 files

0.1.0

2 files

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