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

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

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

quillmark-0.108.1.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.108.1-cp310-abi3-win_amd64.whl (16.4 MB view details)

Uploaded CPython 3.10+Windows x86-64

quillmark-0.108.1-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.108.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (16.8 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

quillmark-0.108.1-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.108.1.tar.gz.

File metadata

  • Download URL: quillmark-0.108.1.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

Hashes for quillmark-0.108.1.tar.gz
Algorithm Hash digest
SHA256 1a6b8bee8b2c0e661ce521effdf25b57c1950b788077462840fc55faa8666cb2
MD5 4607dc15b5c83a82c28641f62eebb2ba
BLAKE2b-256 9f15a96c291927ddfbb45b8b2b0381d4b032a2700b0d2dfc9416a92c9aa74c9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.108.1.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.108.1-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: quillmark-0.108.1-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

Hashes for quillmark-0.108.1-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 b5f2a1fee12c7e0cb506be2352c144ec4bfc11a1f76fa6d95f4de1fd6d23ea62
MD5 ee9898f569fb2f5303ad733067873bee
BLAKE2b-256 90b79f16c90d20c9ba75179fd93b2b296c906551009dd78b9da3732386e5aa49

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for quillmark-0.108.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 feabe0bbddcd2157255e0ae71fcbac9c88de0e33055b09871b875c597c9c17de
MD5 14b7ee53d31e02f4cd3ffb3f06e194ea
BLAKE2b-256 d852c09a2df119db2e5f2f6abaf680ac8046e82702a610312ab686ca8c215332

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.108.1-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.108.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for quillmark-0.108.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 7fcb0bdb5e3f347379835d6d660828204dbe9c11ad1e06c3cccdc03b6dbed8e3
MD5 87af7a49817cb87ac7d15e4e95c80b94
BLAKE2b-256 02db2ac5ed888cac0df0df80edf6a87d703f0c686bd40a91cc853a33973a8d8d

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.108.1-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.108.1-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for quillmark-0.108.1-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e0db0a4d5134cb8d0fbe972606139540e547c94530e24ce350ae38be42af5f41
MD5 636da392f5670daeceaded3d5c6297bd
BLAKE2b-256 a3ff020bb2b267e1a4a6596f59a9da80abeadb889c7be6c82cb480b33fea5c90

See more details on using hashes here.

Provenance

The following attestation bundles were made for quillmark-0.108.1-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

This release

0.108.1 This release

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

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