Skip to main content

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

Writerquill.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

Readerquill.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_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_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

quillmark-0.96.0.tar.gz (2.8 MB view details)

Uploaded Source

Built Distributions

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

quillmark-0.96.0-cp310-abi3-win_amd64.whl (16.1 MB view details)

Uploaded CPython 3.10+Windows x86-64

quillmark-0.96.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.2 MB view details)

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

quillmark-0.96.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (16.5 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

quillmark-0.96.0-cp310-abi3-macosx_11_0_arm64.whl (15.1 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

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

Hashes for quillmark-0.96.0.tar.gz
Algorithm Hash digest
SHA256 58ef5d2fad25da7d4eb7cc12a9206bd38080158e00e9cdfdabe77ec103b0ff8f
MD5 e3172fd3eb788fc72debee71069b529c
BLAKE2b-256 3f23a4042d16ccc8468b02b74c01cc8d646e0df8c706ebcab398609faceb361a

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.96.0.tar.gz:

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

Hashes for quillmark-0.96.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 3f63c1406e0e23c990608cf1700e21e2ce797706d68671083249d86355d7ae6c
MD5 640603fb87de9707e1a9be35b8df8bd7
BLAKE2b-256 995a23d64bc07e321ade17522e8e223cb881c3ac06c8085e093f14277d89a978

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.96.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.96.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for quillmark-0.96.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 5db50be1ef31895fddcabef1aed44772eb6cd2e8150222c3be2d5238b025c13d
MD5 a624a0c919766cdfcb11e3ec493a0d4d
BLAKE2b-256 353674e290848732e9cb9527a8183fbc545a3b2c40583c16c43d1765aa91d6eb

See more details on using hashes here.

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

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.96.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for quillmark-0.96.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 d2ae1bf3780fc5e9845c1ac1afd25da975ee63b1b3f210c39585a1f1b35e54f6
MD5 259a88d190dc396245466465d7ca2eed
BLAKE2b-256 f88eb4a9d86d06dc90e8535f5257671ba33c6033bd14e30923972c7a03d2aed3

See more details on using hashes here.

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

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.96.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for quillmark-0.96.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 80afa25c3aedccee38d0805253064d8aa8e8c6ef5d89471008d0acb4aaec3581
MD5 ec54d4d994a86176f108757900b45b03
BLAKE2b-256 7c71505083b7efaa7cc8ad0bc87505f3fac8014b578e7706b57af02fcc5994dc

See more details on using hashes here.

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

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

0.102.0

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

This release

0.96.0 This release

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