Markup language for AI and humans, compiles to DOCX and HTML
Project description
dok
Document markup language for AI and humans, compiles to DOCX and HTML.
Write structure. Let AI fill it. Compile to anything.
pip install dok
Zero dependencies. Pure Python stdlib.
pip install dokinstalls exactly one thing.
The problem with AI document generation today
Ask an AI to generate a Word document and one of three things happens:
- It writes python-docx code that silently breaks on anything non-trivial — wrong XML element ordering, missing bidi tags, corrupt output with no error message
- It writes Markdown that loses all formatting the moment it touches a real document converter
- It hallucinates OOXML directly and produces a file Word refuses to open
The root cause is not the AI. It is that there is no clean, compact format that maps directly to a rich document. So AI uses whatever is closest and gets it wrong.
Dok fixes that.
How it works
Natural language prompt
↓
AI generates .dok ← ~50 tokens for a complete invoice
↓
dok compiler
↓
DOCX · HTML · PDF (via LibreOffice)
The AI never touches OOXML. It never manages RTL bidi ordering. It never thinks about twips, half-points, or content type manifests. It outputs clean structure. The compiler handles everything below.
Show me
A complete invoice. AI generates this:
doc(font: Calibri, size: 11) {
page(margin: normal) {
banner(fill: #1F3864, accent: gold, color: white) {
center { bold(size: 16) { "INVOICE" } }
}
table(border: true, striped: true) {
tr { th { "Item" } th { "Qty" } th { "Price" } }
tr { td { "Consulting" } td { "3" } td { "$900" } }
tr { td { "Design" } td { "1" } td { "$400" } }
}
right { bold(size: 14) { "Total: $1,300" } }
}
}
Compile it to any format:
python -m dok invoice.dok invoice.docx # Word document
python -m dok invoice.dok invoice.html # Web / email
Same source. Every format. No reformatting.
Why AI generates dok reliably
The grammar fits in a system prompt. The entire language is five node types and one rule. An AI learns it in ~200 tokens and produces valid output consistently. Compare that to teaching a model the full OOXML specification.
One way to express everything. There is no ambiguity. No five ways to write the same structure. Fewer choices mean fewer hallucinations.
Errors are self-correcting. When AI makes a mistake, the compiler returns a structured error with the source line and an exact fix:
ValidationError at line 4: 'bold' cannot be inside 'page'
Hint: bold is a style node — wrap content in a paragraph first.
p { bold { "your text" } }
The AI reads that, corrects it, retries. No human involved. Convergence in one or two iterations.
RTL and Arabic — first class, not an afterthought
Arabic and Hebrew rendering in OOXML requires three XML elements in strict order. Get the order wrong and the document silently renders left-aligned. There is no error. Word just opens it broken.
In dok, one wrapper node handles everything:
rtl {
h1 { "التقرير السنوي" }
p { "نما الإيراد بنسبة " bold { "42%" } " هذا العام." }
}
Mixed direction works correctly too:
rtl {
p { "نص عربي " ltr { bold { "English term" } } " نص عربي" }
}
No manual bidi tags. No XML ordering bugs. No post-processing. It compiles correctly every time.
What dok replaces
| Tool | What it does | Price | Dok equivalent |
|---|---|---|---|
| DocRaptor | HTML → PDF API | $15–200/month | dok.to_pdf() |
| Carbone | Template → DOCX/PDF | $49–299/month | dok.to_docx() |
| PDFMonkey | Template → PDF | $19–149/month | dok.to_pdf() |
| Docmosis | Enterprise doc generation | $150+/month | All of the above |
Open source. No API key. No per-document pricing. Runs locally or on your own server.
Five-minute integration with any AI
import dok
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT = """
You generate documents in dok syntax. Output only valid dok. No explanation, no markdown fences.
Dok has five layers — each node belongs to exactly one:
doc → document defaults (font, size, spacing)
page → physical space (margin, paper, cols)
layout → arrangement (center, right, rtl, ltr, indent, row, cols)
style → appearance (bold, italic, underline, color, size, font, highlight)
content → atoms (h1-h4, p, code, ul/ol/li, table/tr/th/td, box, banner, callout, badge, line, img, link)
One syntax rule: name(props) { children }
Text is always quoted: "like this"
Multiline text uses triple quotes: \"\"\"like this\"\"\"
"""
def generate_document(prompt: str, output_path: str):
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": prompt}]
)
source = response.content[0].text
node = dok.parse(source)
if output_path.endswith(".docx"):
dok.to_docx(node, output_path)
elif output_path.endswith(".html"):
dok.to_html(node, output_path)
generate_document(
"Invoice for Alice. Consulting 3 days at $300/day. Design 1 day at $400. Show total.",
"invoice.docx"
)
Installation
pip install dok
Quick start
Command line:
python -m dok report.dok report.docx # compile to Word
python -m dok report.dok report.html # compile to HTML
python -m dok --check report.dok # validate only
python -m dok --tree report.dok # print node tree
Python:
import dok
node = dok.parse('doc { h1 { "Hello World" } }')
dok.to_docx(node, "hello.docx")
dok.to_html(node, "hello.html")
The core idea
A document is a stack of contexts.
doc(font: Calibri, size: 11) { // sets document defaults
page(margin: normal) { // sets physical space
center { // sets alignment
bold(color: navy) { // sets run style
h1 { "Quarterly Report" } // content atom
}
}
}
}
Each layer adds one thing to the context. Content at the bottom inherits everything above it. No cascade. No specificity. No global state. Just a tree where context flows inward.
Every feature is a node at one of five layers:
doc → document defaults (font, size, spacing)
page → physical space (margin, paper, cols)
layout → arrangement (center, right, rtl, row, cols, indent)
style → appearance (bold, italic, color, size, font)
content → atoms (h1-h4, p, code, box, line, table, ul/ol, img, link)
What dok deliberately does not do
Dok has no variables. No loops. No conditionals. No expressions.
This is not a limitation to work around — it is the design.
Loops and conditionals live in your programming language, which already does them better than any template engine. Dok's job is to describe what the document looks like. Your language's job is to decide what goes into it.
Dok handles: structure, appearance, shapes, layout, direction, composition, functions.
Your language handles: which sections to include, how many items in a list, what color a status badge should be, whether to show a warning block.
The boundary is clean. The compiler is simple. The output is predictable.
Feature overview
| Category | Features |
|---|---|
| Output | .docx (Word), .html (standalone) |
| Text | Headings (h1–h4), paragraphs, block quotes, code blocks |
| Strings | Single-line "...", multiline """...""" with auto-dedent |
| Styles | Bold, italic, underline, strikethrough, superscript, subscript, color, font, size, highlight |
| Layout | Center, right, justify, RTL/LTR, indent, columns, float |
| Columns | page(cols: 2) for newspaper-style flow, cols(ratio: 2:1) for side-by-side layout |
| Containers | Box (width/height), callout, banner, badge |
| Lists | Bullet (ul/li), numbered (ol/li) |
| Tables | Headers, borders, striped rows, colspan |
| Images | PNG/JPEG embedded in DOCX, inline in HTML |
| Links | Clickable hyperlinks, styled content inside links |
| Shapes | Circle, diamond, chevron with arrow connectors |
| Page control | Page breaks, page numbers, headers, footers, spacers |
| Spacing | Document-wide presets, per-paragraph override, line-height control |
| Code | Syntax-preserving blocks, monospace, background, border |
| Functions | Reusable components with parameters and children |
| Imports | Split documents into modules with import "file.dok" |
| Direction | Full RTL for Arabic/Hebrew with rtl { } |
| Validation | Prop types, structure rules, all errors with source location and fix hints |
Syntax
One rule for every node in the language:
name(props) { children } // node with props and children
name { children } // node with no props
name(props) // self-closing node
"text" // bare text node (single line)
"""text""" // triple-quoted text (multiline, auto-dedent)
-> // arrow connector (inside row only)
--- // page break
// comment // line comment, stripped before parsing
Props are always key: value pairs or bare flags:
fill: navy // named color
fill: #4472C4 // hex color
size: 14 // number
src: "image.png" // string value
rounded // bare flag (boolean true)
Multiline strings use triple quotes with automatic leading-indentation removal:
p {
"""
This paragraph spans multiple lines.
Leading indentation is automatically stripped.
The output is clean flowing text.
"""
}
code {
"""
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
"""
}
The five layers
Layer 1 — doc
Sets document-wide defaults. Everything inside inherits these unless overridden.
doc(font: Calibri, size: 11) {
// all content here
}
| Prop | Values | Default |
|---|---|---|
font |
any system font name | Calibri |
size |
number in points | 11 |
spacing |
compact tight normal relaxed |
normal |
Spacing presets control paragraph spacing and line height across the whole document:
compact— zero paragraph gap, single line spacingtight— small paragraph gap, single line spacingnormal— standard Word spacing (8pt after, 1.15× line height)relaxed— generous spacing (10pt after, 1.3× line height)
doc is optional. If omitted, all defaults apply.
Layer 2 — page
Sets the physical page. Multiple page nodes create multiple sections with different layouts.
doc {
page(margin: normal) {
// standard content
}
page(margin: wide, cols: 2) {
// two-column section
}
}
| Prop | Values | Default |
|---|---|---|
margin |
normal narrow wide none |
normal |
paper |
a4 letter a3 |
a4 |
cols |
1 2 3 |
1 |
cols: 2 creates newspaper-style columns: text flows down the left column then continues at the top of the right. This is native Word section columns — ideal for research papers, newsletters, and academic documents.
// Research paper: full-width title, two-column body
doc(font: "Times New Roman", size: 10, spacing: tight) {
page(margin: normal, cols: 1) {
center { bold(size: 16) { "Paper Title" } }
// abstract...
}
page(margin: normal, cols: 2) {
h2 { "Introduction" }
p { "Body text flows automatically into the second column." }
}
}
page is optional. If omitted, margin: normal and paper: a4 apply.
Layer 3 — layout
Arrangement nodes affect how their children are placed on the page.
center { h1 { "Title" } }
right { p { "Right-aligned" } }
rtl { p { "نص عربي" } }
indent(level: 2) {
p { "Indented two levels." }
}
row {
box(fill: blue, color: white) { "Step 1" } ->
box(fill: orange, color: white) { "Step 2" } ->
box(fill: green, color: white) { "Step 3" }
}
cols(ratio: 2:1) {
col { p { "Wider column." } }
col { p { "Narrower column." } }
}
| Node | What it does |
|---|---|
center |
Centers children horizontally |
right |
Right-aligns children |
left |
Left-aligns children (default, rarely needed) |
justify |
Justifies text in children |
rtl |
Sets right-to-left direction for all children |
ltr |
Sets left-to-right direction (override inside rtl) |
indent(level: N) |
Indents N levels (each level = 0.5 inch) |
row { A -> B } |
Places children side by side with optional arrow connectors |
cols { col col } |
Side-by-side column layout |
float(side: right) |
Floats content right, text wraps around it |
Layer 4 — style
Style wrappers modify how text renders. They nest freely.
p {
"Normal text, then "
bold { "bold" }
", then "
italic(color: navy) { "italic navy" }
"."
}
| Node | Effect |
|---|---|
bold |
Bold text |
italic |
Italic text |
underline |
Underlined text |
strike |
Strikethrough |
sup |
Superscript |
sub |
Subscript |
color(value: red) |
Text color (named or hex) |
size(value: 14) |
Font size in points |
font(value: Georgia) |
Font family |
highlight(value: yellow) |
Highlight color |
span(bold, color: red, size: 14) |
Multiple styles at once |
Style nodes can carry inline props — these are equivalent:
bold { color(value: red) { "hello" } }
bold(color: red) { "hello" }
Layer 5 — content
Content nodes are the atoms. They produce visible output.
Text blocks:
| Node | Description |
|---|---|
h1 to h4 |
Headings |
p |
Paragraph |
quote |
Block quote |
code |
Code block (monospace, multiline via """) |
"text" |
Bare text node |
--- |
Page break |
Per-element spacing and line-height control:
p(spacing: compact) { "No space after this paragraph." }
p(spacing: relaxed) { "Extra space after this one." }
p(line-height: 15) { "1.5× line spacing." }
p(line-height: 20) { "Double-spaced." }
line-height is in tenths: 10 = single, 15 = 1.5×, 20 = double.
Code blocks:
code { "console.log('hello')" }
code {
"""
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
"""
}
- DOCX: Courier New 10pt, light gray background (#F5F5F5), thin border
- HTML: Monospace font, gray background, rounded border, preserved whitespace
Lists:
ul {
li { "First bullet" }
li { "Second with " bold { "inline formatting" } }
}
ol {
li { "Step one" }
li { "Step two" }
}
Produces native Word bullet and numbered lists with correct indentation.
Tables:
table(border: true, striped: true) {
tr {
th { "Name" }
th { "Score" }
}
tr {
td { "Alice" }
td { bold(color: green) { "95" } }
}
}
| Prop | Values | Default |
|---|---|---|
border |
boolean | false |
striped |
boolean | false |
th cells are bold with a shaded background. td(colspan: 2) spans columns.
Images:
img(src: "photo.png", width: 4)
img(src: "logo.jpg", width: 2, height: 1)
| Prop | Description |
|---|---|
src |
File path (required) |
width |
Width in inches |
height |
Height in inches (auto from aspect ratio if omitted) |
alt |
Alt text |
PNG and JPEG supported. Embedded in the DOCX file.
Hyperlinks:
p {
"Visit "
link(href: "https://example.com") { "our website" }
" for more info."
}
// Styled content inside links
link(href: "mailto:contact@example.com") { bold { "Email us" } }
// Multiple links inline
p {
link(href: "https://github.com") { "GitHub" }
" | "
link(href: "https://example.com") { "Website" }
}
Links render as blue (#0563C1) underlined text in both DOCX and HTML. In DOCX they are native Word hyperlinks stored in the relationship table.
Visual elements:
| Node | Description |
|---|---|
box |
Bordered/shaded content block |
callout |
Side-bordered note block |
banner |
Full-width colored block |
badge |
Small inline label |
line |
Horizontal divider |
box(fill: #E8F4FD, stroke: #2196F3, rounded: true) {
bold { "Info" }
p { "Content inside the box." }
}
callout(fill: #FFF3CD, stroke: #FFC107, tail: top-left) {
bold { "Warning:" }
" Check your input data."
}
banner(fill: #1F3864, accent: gold, color: white) {
center { bold(size: 16) { "Company Name" } }
}
p { "Status: " badge(fill: green, color: white) { "ACTIVE" } }
line(stroke: gray, dashed: true)
Shape props:
| Prop | Values | Description |
|---|---|---|
fill |
color | Background color |
stroke |
color | Border color |
color |
color | Text color inside |
rounded |
flag | Rounded corners (box only) |
shadow |
flag | Drop shadow |
accent |
color | Left-edge accent bar (banner only) |
width |
integer | Box width as % of page (1–100), default = full |
height |
integer | Minimum height in points, default = auto |
tail |
top-left top-right bottom-left bottom-right |
Callout pointer |
dashed |
flag | Dashed line |
thick |
flag | Thick line |
box(fill: #E8F4FD, width: 50) { p { "Half-width box." } }
box(fill: #FFF2CC, height: 120) { p { "At least 120pt tall." } }
Page numbers:
footer {
center {
p { "Page " page-number }
}
}
page-number inserts a live Word field that updates automatically.
Headers and footers:
doc {
header {
right { italic(color: gray, size: 9) { "Document Title" } }
}
footer {
center {
p { color(value: gray, size: 9) { "Page " } page-number }
}
}
page { /* content */ }
}
Spacer:
space(size: 24) // 24pt vertical space
Color values:
Named: red orange yellow green blue navy purple gray black white gold silver
Light variants: lightblue lightgreen lightyellow lightgray lightpink
Hex: #4472C4 #1F3864 #FF0000 #ABC
Transparent: none
Functions and reuse
Define reusable components with def:
def alert-box(message) {
box(fill: #FFF3CD, stroke: #FFC107, rounded: true) {
bold { "Warning: " }
message
}
}
def info-row(label, value) {
p {
bold(color: gray) { label }
value
}
}
alert-box(message: "Check your data before submitting.")
info-row(label: "Name: ", value: "Alice Johnson")
Use the children keyword for composable wrappers:
def card(title) {
box(fill: #F5F5F5, rounded: true) {
bold(size: 14) { title }
children
}
}
card(title: "Summary") {
p { "This content goes where 'children' is." }
p { "Multiple children work too." }
}
Imports
Split large documents into reusable modules:
// components.dok
def company-header(title) {
banner(fill: #1F3864, accent: gold, color: white) {
center { bold(size: 16) { title } }
}
}
def signature-block(name) {
center {
line
color(value: gray) { name }
}
}
// report.dok
import "components.dok"
doc {
page {
company-header(title: "Annual Report")
h1 { "Introduction" }
signature-block(name: "CEO")
}
}
Imports resolve relative to the importing file. Circular imports are detected and rejected.
Builder API (Python)
For dynamic content — loops, conditionals, data-driven documents. Your language handles the logic. Dok handles the shape.
Every builder function returns a Node — immutable, composable, no side effects.
import dok
doc = dok.doc(
dok.page(
dok.banner("Acme Corp", fill="#1F3864", accent="gold", color="white"),
dok.h1("Report"),
dok.p("Revenue grew by ", dok.bold("42%", color="green"), "."),
margin="normal",
)
)
dok.to_docx(doc, "report.docx")
dok.to_html(doc, "report.html")
All builder functions:
# Document structure
dok.doc(*children, font=..., size=..., spacing=...)
dok.page(*children, margin=..., paper=..., cols=...)
# Layout
dok.center(*children)
dok.right(*children)
dok.justify(*children)
dok.rtl(*children)
dok.ltr(*children)
dok.indent(*children, level=1)
dok.row(*children)
dok.cols(*children, ratio="1:1")
dok.col(*children)
dok.float_right(*children)
dok.float_left(*children)
# Style
dok.bold(*children, color=..., size=..., font=...)
dok.italic(*children, ...)
dok.underline(*children, ...)
dok.strike(*children, ...)
dok.sup(*children)
dok.sub(*children)
dok.color("red", *children)
dok.size(14, *children)
dok.font("Georgia", *children)
dok.highlight("yellow", *children)
dok.span(*children, bold=True, color=..., size=...)
# Text blocks
dok.h1("Title", ...) # also h2, h3, h4
dok.p(*children, ...)
dok.quote(*children)
dok.code("source code")
# Lists
dok.ul(dok.li("item"), dok.li("item"))
dok.ol(dok.li("first"), dok.li("second"), start=1)
dok.li(*children)
# Tables
dok.table(
dok.tr(dok.th("Header"), dok.th("Header")),
dok.tr(dok.td("Cell"), dok.td("Cell")),
border=True, striped=True,
)
# Visual elements
dok.box(*children, fill=..., stroke=..., rounded=True)
dok.callout(*children, fill=..., stroke=..., tail="top-left")
dok.banner(*children, fill=..., accent=..., color=...)
dok.badge("label", fill=..., color=...)
dok.line(stroke=..., dashed=True)
# Inline
dok.img("photo.png", width=4)
dok.link("https://example.com", "click here")
dok.page_number()
# Meta
dok.header(*children)
dok.footer(*children)
dok.space(size=12)
dok.page_break()
dok.arrow(label=None)
Dynamic content:
# Loops
items = [dok.li(item.name) for item in data]
doc = dok.doc(dok.ul(*items))
# Conditionals — None children are silently dropped
doc = dok.doc(
dok.page(
dok.callout("CONFIDENTIAL", fill="red", color="white") if classified else None,
dok.h1(report.title),
*[dok.p(section.text) for section in report.sections],
)
)
Output:
dok.to_docx(node, "report.docx")
dok.to_html(node, "report.html")
data = dok.to_bytes(node) # bytes for HTTP / S3
node = dok.parse(source_string)
node = dok.parse(source_string, base_dir=Path("./templates"))
Compilation pipeline
source → lex → parse → resolve_imports → resolve_functions → validate → convert → write
- Lexer — tokenizes the source string
- Parser — builds an AST of Node trees
- Import resolver — reads imported files, injects their nodes
- Function resolver — expands function calls by substituting parameters
- Validator — checks structure rules, prop types, printable constraints
- Converter — walks the AST with context inheritance, produces a DocxModel
- Writer — serializes the DocxModel to OOXML inside a ZIP archive
All errors carry source location (line, column) and human-readable hints.
Error handling
Every error includes source location and an exact fix:
ParseError at line 5, col 12: Expected '}' to close block
Hint: Every '{' needs a matching '}'.
ValidationError at line 8, col 3: 'li' must be inside 'ul' or 'ol'
Hint: ul { li { "item" } }
ValidationError at line 12, col 5: Invalid color 'notacolor' for 'bold.color'
Hint: Use a named color (red, navy, gold, ...) or hex (#FF0000, #ABC).
ResolveError at line 3: Missing parameter 'name' in call to 'greeting'
Hint: Usage: greeting(name: ...)
The validator collects all errors in a single pass — you see everything wrong at once, not one error at a time.
Complete example
import "components.dok"
def metric(label, value, trend_color) {
box(fill: #F8F9FA, rounded: true) {
p { color(value: gray) { label } }
bold(size: 20, color: trend_color) { value }
}
}
doc(font: Calibri, size: 11) {
header {
right { italic(color: gray, size: 9) { "Q4 2024 Report" } }
}
footer {
center { p { color(value: gray, size: 9) { "Page " } page-number } }
}
page(margin: normal) {
banner(fill: #1F3864, accent: gold, color: white) {
center { bold(size: 16) { "Acme Corporation" } }
}
center {
h1 { "Q4 2024 Financial Report" }
italic(color: gray) { "For internal distribution only" }
}
row {
metric(label: "Revenue", value: "$4.2M", trend_color: green)
metric(label: "Customers", value: "1,840", trend_color: green)
metric(label: "Churn", value: "2.1%", trend_color: red)
}
h2 { "Regional Breakdown" }
table(border: true, striped: true) {
tr { th { "Region" } th { "Revenue" } th { "Growth" } }
tr { td { "EMEA" } td { "$1.8M" } td { bold(color: green) { "+38%" } } }
tr { td { "Americas" } td { "$1.5M" } td { bold(color: green) { "+22%" } } }
tr { td { "APAC" } td { "$0.9M" } td { bold(color: orange) { "+8%" } } }
}
space(size: 12)
callout(fill: #FFF2CC, stroke: #FFC000, tail: bottom-left) {
bold { "Note:" }
p { "These figures are preliminary and subject to audit." }
}
h2 { "Key Highlights" }
ul {
li { "Enterprise contracts in Germany and UK drove EMEA growth" }
li { "Customer acquisition cost decreased by " bold { "15%" } }
li { "New product line launched in Q3 reaching " bold { "$400K" } " revenue" }
}
space(size: 24)
cols(ratio: 1:1:1) {
col { center { line color(value: gray) { "CEO" } } }
col { center { line color(value: gray) { "CFO" } } }
col { center { line color(value: gray) { "Board Secretary" } } }
}
}
}
File extension
.dok — UTF-8 text. Version-control friendly. Diffs cleanly. Readable by non-developers.
For purely dynamic documents, no .dok file is needed — build the node tree with the builder API and pass it directly to the compiler.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dok-0.1.0.tar.gz.
File metadata
- Download URL: dok-0.1.0.tar.gz
- Upload date:
- Size: 72.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66b2484ad88d30853d7fabfe70a6aa4502a898554af41fae3443e95dec0fc2eb
|
|
| MD5 |
adce0c87e8646971f1fab7b68e7459f7
|
|
| BLAKE2b-256 |
9eca10f903e95b3c3d4b1de8e030f02db9efca213db065404e9af911c7c529a9
|
File details
Details for the file dok-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dok-0.1.0-py3-none-any.whl
- Upload date:
- Size: 72.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b5a44ab29881be9bc106e8f55ccf8f778a61b737a183fc0d9c6f7223ff0f6f2
|
|
| MD5 |
96e921898e153b02b0b3d43e60aab774
|
|
| BLAKE2b-256 |
ba51be156ed836de055204187e89b11afdbb4c865f1e56eb4d8b31fc1f9f637f
|