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

Uploaded CPython 3.10+Windows x86-64

quillmark-0.108.3-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.108.3-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.108.3-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.108.3.tar.gz.

File metadata

  • Download URL: quillmark-0.108.3.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.3.tar.gz
Algorithm Hash digest
SHA256 f017765427555d236dc9d3f1c33a4c1328c07e44bc3f8711f125e8ae76a13706
MD5 eea47e659335c4e204895fcb72b1ebdb
BLAKE2b-256 4c14d67adca028480469e100bfc3492be912a4495e995de0e2cb1d12c4e30bfb

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: quillmark-0.108.3-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.108.3-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 76e724b9ea770fd9bbc975de5da10aea876cecb218c424e3544a4cf4b5119464
MD5 2ef877f57a5af941ccf7578b88adf46c
BLAKE2b-256 f801ac3891fe36f0b9ecccc2c276418a3fa064c87cd9cd60e2768ac15234da2d

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for quillmark-0.108.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 34720815d9a02664ac0ba6fdc8f7c04c6d42d2cd0da63747a95e758b1739f1d6
MD5 9e86a77b0d897a893dd5dcb13a2c6197
BLAKE2b-256 3baa7701a97f0e0a87f5451219a51f76921e14449372253166e8e2488eb57aee

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for quillmark-0.108.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 5cedefbb2ead362eeacad5ec205d335d6c6dce9556ee960ea2b4f13a9160403f
MD5 e2b4a479d7679c1dddb06ca6f44f505b
BLAKE2b-256 2b6f6a6dc97e163eda7a88abee3a439cc84b896c6958dc733123583afb2b68fc

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for quillmark-0.108.3-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 95c5f00bb800d0f0aee368964a81fbb790c3d9d14260dbc7dca272bdb90cf2ee
MD5 8be98e18ca038b8da83029cb9d076c3b
BLAKE2b-256 16d63eca3a4beca10cb9d8b7f81c308f3544d8bf1c0448c4c88a05a3717c458c

See more details on using hashes here.

Provenance

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

This release

0.108.3 This release

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