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.110.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.110.0-cp310-abi3-win_amd64.whl (16.5 MB view details)

Uploaded CPython 3.10+Windows x86-64

quillmark-0.110.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.7 MB view details)

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

quillmark-0.110.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (16.9 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

quillmark-0.110.0-cp310-abi3-macosx_11_0_arm64.whl (15.5 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

File details

Details for the file quillmark-0.110.0.tar.gz.

File metadata

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

Hashes for quillmark-0.110.0.tar.gz
Algorithm Hash digest
SHA256 fb9693e6b74e1350c8b4ea2e0ef5e3a0edf909199e40a2d5c5aef638bc3f0eaa
MD5 cd6ea63ac3c38c3834ec2d8c8b98f0e6
BLAKE2b-256 a34b3233e17af1e7db63118bc41a1cc3fe77795afb51814a31fa9ed398dda562

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: quillmark-0.110.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 16.5 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.110.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 fefb8888f866abc2d5edbd069dacfc1e74a91afb1aa7e58c31668aac1baae306
MD5 31012cc9cea05c1b283e5725216e1fcf
BLAKE2b-256 cf6ddf994160c83d55b875b930f622105b63c23f9b3dc77a3f3122d10d164e7a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for quillmark-0.110.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 6f062ec63b7da903cb490b92488f70e32f681f403d035db36067e80905bba313
MD5 af7810d6d4c74d640b161af7734af7be
BLAKE2b-256 316e62111836ba0066d9bc8e1279e0a4f06116b475e3638ea8193346f535dc2a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for quillmark-0.110.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 a78a064cac9e63a0263226826ce4e2f0364108de7cc2335278c48d2ec3cfd88f
MD5 0966c55f33a6bb54bda1b3ff65a2df48
BLAKE2b-256 9b4fca83e92041dff5d19f36bcdb9430e1f5e641cb01d8745c0f471e1093f3fb

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for quillmark-0.110.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 5601b1b2b635347135f6f6a40418a579ef970a30ee6e6b9fb2d9ac76cd47ba72
MD5 7f2c22529856c75edd600b3c09a89e21
BLAKE2b-256 33044d867173230211b245b527bdc50739657adbfcc72e33f7f875f7ed2833d6

See more details on using hashes here.

Provenance

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

This release

0.110.0 This release

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

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