docs: replace MkDocs with Zensical (#3073)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
Marcelo Trylesinski
2026-07-10 13:48:46 +02:00
committed by GitHub
parent 74a242ae7f
commit 4fc8882c02
17 changed files with 1183 additions and 610 deletions
+1 -3
View File
@@ -14,6 +14,7 @@ on:
- src/mcp/**
- src/mcp-types/**
- scripts/build-docs.sh
- scripts/docs/**
- pyproject.toml
- uv.lock
- .github/workflows/deploy-docs.yml
@@ -49,9 +50,6 @@ jobs:
- name: Build combined docs (v1.x at /, main at /v2/)
run: bash scripts/build-docs.sh site
env:
# Silence mkdocs-material's MkDocs 2.0 warning banner in CI logs.
NO_MKDOCS_2_WARNING: "1"
- name: Configure Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
+11 -12
View File
@@ -139,11 +139,16 @@ jobs:
- name: Check README snippets are up to date
run: uv run --frozen scripts/update_readme_snippets.py --check
# `mkdocs.yml` sets `strict: true` and `pymdownx.snippets: check_paths: true`,
# but until this job existed the docs were only ever built post-merge by
# `deploy-docs.yml`, so a broken link, a missing nav target, or a deleted
# `docs_src/` include went green on the PR and broke the next deploy of main.
# This is the check path; `deploy-docs.yml` stays the deploy path.
# `scripts/docs/build.sh` is the whole gauntlet: build_config.py fails on
# nav entries without a page and pages without a nav entry, `zensical build
# --strict` fails on broken .md links, `pymdownx.snippets: check_paths:
# true` fails on a deleted `docs_src/` include, and the post-build steps
# fail on unresolved cross-references, inventory download failures, and
# broken non-markdown link targets.
# Until this job existed the docs were only ever built post-merge by
# `deploy-docs.yml`, so those failures went green on the PR and broke the next
# deploy of main. This is the check path; `deploy-docs.yml` stays the deploy
# path.
docs:
runs-on: ubuntu-latest
steps:
@@ -168,11 +173,5 @@ jobs:
enable-cache: true
version: 0.9.5
- name: Install dependencies
run: uv sync --frozen --all-extras --python 3.10
- name: Build the docs in strict mode
run: uv run --frozen --no-sync mkdocs build --strict
env:
# Silence mkdocs-material's MkDocs 2.0 warning banner in CI logs.
NO_MKDOCS_2_WARNING: "1"
run: bash scripts/docs/build.sh
+5 -1
View File
@@ -141,9 +141,13 @@ venv.bak/
# Rope project settings
.ropeproject
# mkdocs documentation
# documentation
/site
/.worktrees/
# Generated at build time by scripts/docs/ (the API reference tree and the
# concrete Zensical config spliced from mkdocs.yml).
/docs/api/
/mkdocs.gen.yml
# mypy
.mypy_cache/
+70 -37
View File
@@ -1,12 +1,42 @@
/* Sidebar hierarchy + density for MkDocs Material 9.7.x.
All rules scoped to the desktop sidebar breakpoint (>= 76.25em), matching
Material's own scoping for navigation.sections, so the mobile drill-down
drawer keeps stock styling. Colors use Material tokens, so the light and
slate schemes both work without extra palette handling. */
/* Sidebar hierarchy + density for Zensical's UI (Material-compatible md-*
DOM, but different stock spacing: nav links are 8px-radius pills with
7px 16px padding). All rules scoped to the desktop sidebar breakpoint
(>= 76.25em) so the mobile drill-down drawer keeps stock styling. Colors
use the md-* tokens, so the light and slate schemes both work without
extra palette handling. */
@media screen and (min-width: 76.25em) {
/* Section labels: smaller, uppercase, letter-spaced, muted. Covers both the
clickable index-page headers and the bare API Reference label. */
/* The sidebar is one coordinate system derived from the pill inset:
every row — page links, group rows, section labels — is a direct
.md-nav__link child of its item with the same 10px horizontal padding,
so all text shares one column, and hover/active pills always paint
10px of breathing room inside the scroll container (never clipped).
The padding lives on the elements Zensical paints hover/active pills
on (.md-nav__link[href] anchors and [for] labels — leaf links, bare
section labels, and the inner anchor of an .md-nav__container
wrapper); wrappers stay geometry-neutral, as stock. The 10px inset
also stays >= the 0.4rem pill radius, so the corner curve never
crowds the text. Vertical rhythm has a single knob: the nav list's
flex gap (stock 0.2rem reads airy; 2px matches the density the site
shipped with on Material, ~30px row pitch). */
.md-sidebar--primary .md-nav__list {
gap: 2px;
}
.md-sidebar--primary .md-nav__item > .md-nav__link:not(.md-nav__container),
.md-sidebar--primary .md-nav__container > .md-nav__link {
padding: 3px 10px;
margin: 0;
}
.md-sidebar--primary .md-nav__item > .md-nav__container {
padding: 0;
margin: 0;
}
/* Section labels: typography only — geometry comes from the row rule
above, so no specificity coordination is needed. */
.md-sidebar--primary .md-nav__item--section {
margin: 0.8em 0;
}
.md-sidebar--primary .md-nav__item--section > .md-nav__link {
font-size: 0.62rem;
font-weight: 700;
@@ -15,39 +45,19 @@
color: var(--md-default-fg-color--light);
}
/* Indent section children and hang a guide line. Material outdents section
children with [dir=ltr] ... margin-left: -0.6rem, which is why they sit
flush under the header; restoring the margin re-exposes the stock 0.6rem
list padding. The logical property ties Material's physical one at
(0,3,0) specificity and wins on source order (extra_css loads last),
while staying direction-agnostic. */
/* Guide lines: 12px from the item box = 2px right of the label text
(which sits at box + 10px pill inset); children indent past them. */
.md-sidebar--primary .md-nav__item--section > .md-nav {
margin-inline-start: 0.1rem;
margin-inline-start: 12px;
border-inline-start: 0.05rem solid var(--md-default-fg-color--lightest);
}
/* Same guide line for collapsible groups inside the API Reference subtree
(section items also carry --nested, so exclude them). */
.md-sidebar--primary .md-nav__item--nested:not(.md-nav__item--section) > .md-nav {
margin-inline-start: 12px;
border-inline-start: 0.05rem solid var(--md-default-fg-color--lightest);
}
/* Tighten vertical rhythm (stock: 0.625em link margins, 1.25em sections).
The child combinator keeps this off anchors inside md-nav__container,
which carry their own margin-top: 0 stock rule. */
.md-sidebar--primary .md-nav__item > .md-nav__link {
margin-top: 0.45em;
}
.md-sidebar--primary .md-nav__item--section {
margin: 1em 0;
}
.md-sidebar--primary .md-nav__item--section > .md-nav__link {
margin-top: 0;
}
/* The current page stands out from its siblings. 700 because Material only
loads Inter at 300/400/700; a 600 would silently substitute the 700 face
anyway, but render lighter on the system-font fallback stack. */
/* The current page stands out from its siblings (on top of the stock
pill highlight). 700 because only Inter 300/400/700 are loaded. */
.md-sidebar--primary .md-nav__link--active {
font-weight: 700;
}
@@ -60,10 +70,33 @@
}
}
/* Headings: Material's 300-weight light-gray defaults read washed out; use
the full foreground color and a solid weight instead. 700, not 600: the
Google Fonts request only carries Inter 300/400/700 (see the nav__link
note above). */
/* Dark scheme: Zensical's slate canvas is near-black (hsla(225,15%,5%)),
harsher than the Material slate this site shipped with; restore that
blue-grey. Code blocks and other surfaces keep Zensical's own tokens. */
@media screen {
[data-md-color-scheme="slate"] {
--md-default-bg-color: #1e2129;
}
}
/* Inline code inside admonitions: the chip token is an absolute dark
surface designed for the page canvas, so on a tinted admonition panel it
sits as an opaque slab (Zensical's own docs share this bug). Re-tint it
tone-on-tone instead — translucent foreground, composited over whatever
the panel color is — the same pattern Starlight and Docusaurus ship for
code inside callouts. Prose chips keep the block-matching dark surface;
block code inside admonitions keeps its own surface too. The first
declaration is the fallback where color-mix is unsupported. */
.md-typeset .admonition :not(pre) > code,
.md-typeset details :not(pre) > code {
background-color: var(--md-default-fg-color--lightest);
background-color: color-mix(in srgb, currentcolor 11%, transparent);
color: inherit;
}
/* Headings: the 300-weight light-gray defaults read washed out; use the
full foreground color and a solid weight instead. 700, not 600: only
Inter 300/400/700 are loaded (see the nav__link note above). */
.md-typeset h1,
.md-typeset h2 {
font-weight: 700;
-40
View File
@@ -1,40 +0,0 @@
"""Generate the code reference pages and navigation."""
from pathlib import Path
import mkdocs_gen_files
nav = mkdocs_gen_files.Nav()
root = Path(__file__).parent.parent.parent
src = root / "src"
# `src/mcp-types` is a distribution directory, not an import package, so each
# package's dotted module path is taken relative to its own parent: deriving it
# from `src/` would emit the unimportable `mcp-types.mcp_types.*`.
for package in (src / "mcp", src / "mcp-types" / "mcp_types"):
base = package.parent
for path in sorted(package.rglob("*.py")):
module_path = path.relative_to(base).with_suffix("")
doc_path = path.relative_to(base).with_suffix(".md")
full_doc_path = Path("api", doc_path)
parts = tuple(module_path.parts)
if parts[-1] == "__init__":
parts = parts[:-1]
doc_path = doc_path.with_name("index.md")
full_doc_path = full_doc_path.with_name("index.md")
elif parts[-1].startswith("_"):
continue
nav[parts] = doc_path.as_posix()
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
ident = ".".join(parts)
fd.write(f"::: {ident}")
mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root))
with mkdocs_gen_files.open("api/SUMMARY.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
-184
View File
@@ -1,184 +0,0 @@
"""Generate llms.txt, llms-full.txt, and per-page markdown (https://llmstxt.org/).
The hook publishes three artifacts into the built site:
- `llms.txt`: a markdown index of the documentation, one link per page,
grouped by nav section.
- a `.md` rendition of every prose page next to its HTML (e.g.
`servers/tools/index.md`), which is what the llms.txt links point at.
- `llms-full.txt`: every prose page concatenated for single-fetch consumption.
Page markdown is the source markdown with `--8<--` snippet includes resolved
(so the `docs_src/` code examples appear inline) and relative links rewritten
to absolute URLs. The API reference pages under `api/` are mkdocstrings stubs
with no markdown source, so they are linked as rendered HTML from an Optional
section instead of being embedded.
Incremental builds (`mkdocs build --dirty`) are rejected: they skip unmodified
pages, which would silently truncate the generated artifacts.
"""
from __future__ import annotations
import posixpath
import re
from dataclasses import dataclass, field
from pathlib import Path
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.exceptions import PluginError
from mkdocs.structure.files import File, Files
from mkdocs.structure.nav import Navigation, Section
from mkdocs.structure.pages import Page
# Pages with no markdown source, linked as HTML under "## Optional".
_OPTIONAL_PAGES = [
("api/mcp/index.md", "mcp API reference", "Auto-generated API reference for the mcp package (rendered HTML)"),
(
"api/mcp_types/index.md",
"mcp-types API reference",
"Auto-generated API reference for the mcp-types package (rendered HTML)",
),
]
_SNIPPET_LINE = re.compile(r'^(?P<indent>[ \t]*)--8<-- "(?P<path>[^"\n]+)"$', flags=re.MULTILINE)
_MD_LINK = re.compile(r'(\]\()([^)\s]+\.md)(#[^)\s]*)?( +"[^"]*")?(\))')
@dataclass
class _State:
page_markdown: dict[str, str] = field(default_factory=dict)
rendition_uris: set[str] = field(default_factory=set)
nav: Navigation | None = None
files: Files | None = None
_state = _State()
def _site_url(config: MkDocsConfig) -> str:
assert config.site_url is not None
return config.site_url.rstrip("/") + "/"
def _md_uri(file: File) -> str:
return re.sub(r"\.html$", ".md", file.dest_uri)
def on_config(config: MkDocsConfig) -> None:
# `mkdocs serve` rebuilds reuse the imported module; start each build clean.
_state.page_markdown.clear()
_state.rendition_uris.clear()
_state.nav = _state.files = None
def on_nav(nav: Navigation, config: MkDocsConfig, files: Files) -> None:
_state.nav = nav
_state.files = files
_state.rendition_uris.update(page.file.src_uri for page in nav.pages if not page.file.src_uri.startswith("api/"))
def on_page_markdown(markdown: str, page: Page, config: MkDocsConfig, files: Files) -> str | None:
if page.file.src_uri not in _state.rendition_uris:
return None
# Same anchor as the pymdownx.snippets `base_path` in mkdocs.yml.
repo_root = Path(config.config_file_path).parent
def include(match: re.Match[str]) -> str:
indent, path = match["indent"], match["path"]
# Mirror the snippets extension's restrict_base_path: reject paths
# that resolve outside the repo root.
resolved_path = (repo_root / path).resolve()
if not resolved_path.is_relative_to(repo_root.resolve()):
raise PluginError(f"llms_txt: snippet path {path!r} in {page.file.src_uri} escapes the repo root")
try:
content = resolved_path.read_text(encoding="utf-8").rstrip("\n")
except OSError as exc:
raise PluginError(f"llms_txt: cannot read snippet {path!r} in {page.file.src_uri}") from exc
# Keep a pointer to the embedded file so readers can find it on disk.
if path.endswith(".py"):
content = f"# {path}\n{content}"
if indent:
content = "\n".join(indent + line if line else line for line in content.split("\n"))
return content
resolved, substitutions = _SNIPPET_LINE.subn(include, markdown)
if substitutions != sum("--8<--" in line for line in markdown.splitlines()):
raise PluginError(f"llms_txt: unresolved snippet include in {page.file.src_uri}")
site_url = _site_url(config)
src_dir = posixpath.dirname(page.file.src_uri)
def rewrite(match: re.Match[str]) -> str:
opening, target, anchor, title, closing = match.groups()
if "://" in target:
return match.group(0)
linked = files.get_file_from_path(posixpath.normpath(posixpath.join(src_dir, target)))
if linked is None:
raise PluginError(f"llms_txt: cannot resolve link target {target!r} in {page.file.src_uri}")
# Pages without a markdown rendition (the api/ stubs) link to their HTML instead.
url = _md_uri(linked) if linked.src_uri in _state.rendition_uris else linked.url
return f"{opening}{site_url}{url}{anchor or ''}{title or ''}{closing}"
_state.page_markdown[page.file.src_uri] = _MD_LINK.sub(rewrite, resolved)
return None
def _section_pages(section: Section) -> list[Page]:
pages: list[Page] = []
for child in section.children:
if isinstance(child, Page) and child.file.src_uri in _state.rendition_uris:
pages.append(child)
elif isinstance(child, Section):
pages.extend(_section_pages(child))
return pages
def on_post_build(config: MkDocsConfig) -> None:
assert _state.nav is not None and _state.files is not None
missing = _state.rendition_uris - _state.page_markdown.keys()
if missing:
raise PluginError(f"llms_txt: pages skipped this build (is this a --dirty build?): {sorted(missing)}")
site_dir = Path(config.site_dir)
site_url = _site_url(config)
top_level = [
item for item in _state.nav.items if isinstance(item, Page) and item.file.src_uri in _state.rendition_uris
]
sections: list[tuple[str, list[Page]]] = [("Docs", top_level)] if top_level else []
for item in _state.nav.items:
if isinstance(item, Section):
pages = _section_pages(item)
if pages:
sections.append((item.title, pages))
index = [f"# {config.site_name}", "", f"> {config.site_description}", ""]
full: list[str] = []
for title, pages in sections:
index += [f"## {title}", ""]
for page in pages:
markdown = _state.page_markdown[page.file.src_uri]
(site_dir / _md_uri(page.file)).write_text(markdown, encoding="utf-8")
description = page.meta.get("description")
tail = f": {description}" if description else ""
index.append(f"- [{page.title}]({site_url}{_md_uri(page.file)}){tail}")
body, h1_found = re.subn(r"\A\s*# .+\n", "", markdown)
if not h1_found:
raise PluginError(f"llms_txt: page {page.file.src_uri} does not start with an H1")
full += [f"# {page.title}", "", f"Source: {page.canonical_url}", "", body.strip(), ""]
index.append("")
index += ["## Optional", ""]
for src_uri, title, description in _OPTIONAL_PAGES:
linked = _state.files.get_file_from_path(src_uri)
if linked is None:
raise PluginError(f"llms_txt: optional page {src_uri} not found")
index.append(f"- [{title}]({site_url}{linked.url}): {description}")
index.append("")
(site_dir / "llms.txt").write_text("\n".join(index), encoding="utf-8")
(site_dir / "llms-full.txt").write_text("\n".join(full), encoding="utf-8")
+29 -32
View File
@@ -1,6 +1,5 @@
site_name: MCP Python SDK
site_description: The official Python SDK for the Model Context Protocol
strict: true
repo_name: modelcontextprotocol/python-sdk
repo_url: https://github.com/modelcontextprotocol/python-sdk
@@ -10,6 +9,9 @@ site_url: https://py.sdk.modelcontextprotocol.io/v2/
# TODO(Marcelo): Add Anthropic copyright?
# copyright: © Model Context Protocol 2025 to present
# The "API Reference" entry is a placeholder: `scripts/docs/gen_ref_pages.py`
# generates the `docs/api/` tree and `scripts/docs/build_config.py` splices the
# real nested nav in before the build. See `scripts/docs/build.sh`.
nav:
- MCP Python SDK: index.md
- "What's new in v2": whats-new.md
@@ -122,45 +124,41 @@ theme:
extra_css:
- extra.css
# https://www.mkdocs.org/user-guide/configuration/#validation
validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
anchors: warn
markdown_extensions:
- tables
- abbr
- admonition
- attr_list
- def_list
- footnotes
- md_in_html
- pymdownx.betterem
- pymdownx.details
- pymdownx.caret
- pymdownx.critic
- pymdownx.mark
- pymdownx.superfences
# Code examples live as complete, importable, tested files under `docs_src/`
# and are included into pages with `--8<-- "docs_src/<chapter>/tutorialNNN.py"`
# (resolved against the repo root regardless of the build's working
# directory; the extension's default base_path is the CWD).
# `check_paths: true` + `strict: true` turn a renamed/deleted example into a
# build failure instead of a silently empty code block.
# (resolved against the repo root, which is the build's working directory).
# `check_paths: true` turns a renamed/deleted example into a build failure
# instead of a silently empty code block.
- pymdownx.snippets:
base_path: !relative $config_dir
base_path: [.]
check_paths: true
- pymdownx.tilde
- pymdownx.inlinehilite
- pymdownx.highlight:
pygments_lang_class: true
- pymdownx.extra:
pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: pymdownx.superfences.fence_code_format
# Zensical re-implements the emoji extension; the generator/index functions
# live under `zensical.extensions.emoji`, not `material.extensions.emoji`.
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
emoji_index: zensical.extensions.emoji.twemoji
emoji_generator: zensical.extensions.emoji.to_svg
options:
custom_icons:
- docs/.overrides/.icons
@@ -170,23 +168,22 @@ markdown_extensions:
custom_checkbox: true
- sane_lists # this means you can start a list from any number
# Zensical natively watches these beyond docs/: page content is assembled
# from src/ (mkdocstrings) and docs_src/ (snippet includes), so serve
# live-reload must react to both.
watch:
- src
- docs_src
hooks:
- docs/hooks/llms_txt.py
# Zensical natively re-implements `search`, `glightbox` and `mkdocstrings`; it
# does not run arbitrary MkDocs plugins or hooks. The former `gen-files`,
# `literate-nav` and `llms_txt` hook are handled by the standalone scripts
# under `scripts/docs/` (see scripts/docs/build.sh). The `social` plugin was
# dropped: Zensical has no social-card support, and the cards were gated on
# ENABLE_SOCIAL_CARDS, which no workflow ever set.
plugins:
- search
- social:
enabled: !ENV [ENABLE_SOCIAL_CARDS, false]
- glightbox
- gen-files:
scripts:
- docs/hooks/gen_ref_pages.py
- literate-nav:
nav_file: SUMMARY.md
- mkdocstrings:
handlers:
python:
+23 -10
View File
@@ -76,16 +76,29 @@ dev = [
"opentelemetry-sdk>=1.39.1",
]
docs = [
# MkDocs 2.0 is a ground-up rewrite (no plugin system) that is incompatible
# with mkdocs-material and every plugin below; stay on the 1.x line.
"mkdocs>=1.6.1,<2",
"mkdocs-gen-files>=0.5.0",
"mkdocs-glightbox>=0.4.0",
"mkdocs-literate-nav>=0.6.1",
# docs/extra.css overrides Material-internal nav selectors; revisit it on a
# major bump before raising this cap.
"mkdocs-material[imaging]>=9.7.0,<10",
"mkdocstrings-python>=2.0.1",
# Zensical is the Material team's successor to MkDocs; it natively
# re-implements search, glightbox and mkdocstrings but runs no arbitrary
# MkDocs plugins or hooks, so the API reference (formerly gen-files +
# literate-nav) and llms.txt (formerly a hook) are generated by the
# standalone scripts under scripts/docs/. See scripts/docs/build.sh.
# 0.0.48 fixed relative/scoped cross-references for mkdocstrings-python
# (which the mkdocstrings config in mkdocs.yml relies on) but broke
# search; 0.0.50 fixes it. The toolchain is pinned exactly: Zensical is
# pre-1.0 and the build guards key on its rendering behavior, so bumps
# should be deliberate.
"zensical==0.0.50",
# Zensical's mkdocstrings compatibility layer targets the mkdocstrings 1.x /
# mkdocstrings-python 2.0.5+ API (griffe 2 / griffelib); the older
# mkdocstrings 0.30 / python 2.0.1 line renders API pages with an
# unregistered-autorefs KeyError under Zensical.
"mkdocstrings==1.0.4",
"mkdocstrings-python==2.0.5",
# scripts/docs/build_config.py and llms_txt.py read mkdocs.yml directly.
"pyyaml>=6.0.2",
# gen_ref_pages.py imports griffe directly. griffelib is not a typo: it is
# griffe's successor distribution (same author) and still imports as
# `griffe`; the old `griffe` distribution is the incompatible 1.x line.
"griffelib==2.1.0",
]
codegen = ["datamodel-code-generator==0.57.0"]
+29 -7
View File
@@ -1,13 +1,17 @@
#!/usr/bin/env bash
#
# Build combined v1 + v2 MkDocs documentation for GitHub Pages.
# Build combined v1 + v2 documentation for GitHub Pages.
#
# v1 docs (from the v1.x branch) are placed at the site root.
# v2 docs (from main) are placed under /v2/.
#
# Both branches are fetched fresh from origin, so the output is identical
# regardless of which branch triggered the workflow. This script is intended
# to run in CI; for local single-branch preview use `uv run mkdocs serve`.
# The two lines use different toolchains: v1.x still builds with MkDocs, while
# main builds with Zensical (which needs a pre-build step to materialise the API
# reference and a post-build step for llms.txt — see scripts/docs/). Each branch
# is fetched fresh from origin and built with its own synced `docs` group, so
# the output is identical regardless of which branch triggered the workflow.
# This script is intended to run in CI; for a local v2 preview use
# `scripts/serve-docs.sh`.
#
# Usage:
# scripts/build-docs.sh [output-dir]
@@ -30,7 +34,21 @@ cleanup() {
}
trap cleanup EXIT
rm -rf "${OUTPUT_DIR:?}"/*
# Build the checked-out worktree into its local `site/`, picking the toolchain
# from the branch's own files rather than hard-coding it here: a branch that
# ships the Zensical build recipe (scripts/docs/build.sh) builds with it,
# otherwise it falls back to MkDocs. This keeps the combined build correct
# regardless of which branch triggered it. Zensical requires site_dir to live
# within the project root, so both paths build to the local `site/` and let
# the caller copy it to its destination.
build_site() {
if [[ -f scripts/docs/build.sh ]]; then
bash scripts/docs/build.sh
else
uv sync --frozen --group docs
NO_MKDOCS_2_WARNING=1 uv run --frozen --no-sync mkdocs build --site-dir site
fi
}
build_branch() {
local branch="$1" worktree="$2" dest="$3"
@@ -43,11 +61,15 @@ build_branch() {
(
cd "$worktree"
uv sync --frozen --group docs
uv run --frozen --no-sync mkdocs build --site-dir "$dest"
rm -rf site
build_site
mkdir -p "$dest"
cp -a site/. "$dest/"
)
}
rm -rf "${OUTPUT_DIR:?}"/*
build_branch v1.x "$V1_WORKTREE" "$OUTPUT_DIR"
build_branch main "$V2_WORKTREE" "$OUTPUT_DIR/v2"
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
#
# Build the v2 documentation site for this checkout into `site/`.
#
# Zensical runs no MkDocs plugins or hooks, so the build is three steps:
# materialise the API reference pages and the concrete config, build the
# site strictly, then generate llms.txt and the per-page markdown
# renditions. This script is the single owner of that recipe, dependency
# sync included — CI (shared.yml, docs-preview.yml) and scripts/build-docs.sh
# all call it. The toolchain detection in docs-preview.yml and build-docs.sh
# keys on this file's path and expects the site under site/.
#
# Usage:
# scripts/docs/build.sh
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Snippet includes (`--8<--`) resolve against the working directory, which
# must therefore be the repo root.
cd "$SCRIPT_DIR/../.."
uv sync --frozen --group docs
# Zensical's incremental cache is unsound: a warm rebuild where only some
# pages re-render silently drops cross-references to cache-hit pages, and
# HTML for since-deleted pages lingers in site/. Build cold so the output
# (and the checks below) are deterministic.
rm -rf .cache site
uv run --frozen --no-sync python scripts/docs/build_config.py
uv run --frozen --no-sync zensical build -f mkdocs.gen.yml --strict
# Zensical stays green even under --strict when a cross-reference fails to
# resolve (rendered as literal bracket text) or an objects.inv inventory
# fails to download (every link through it silently degrades to plain text);
# MkDocs strict mode aborted on both. Validate the built site instead.
uv run --frozen --no-sync python scripts/docs/check_crossrefs.py --site-dir site
uv run --frozen --no-sync python scripts/docs/llms_txt.py --site-dir site
+90
View File
@@ -0,0 +1,90 @@
"""Produce the concrete Zensical build config from `mkdocs.yml`.
Zensical builds from `mkdocs.yml` directly, but it has no equivalent of
mkdocs-literate-nav: the "API Reference" navigation has to be materialised
as explicit entries. This script regenerates the `docs/api/` tree (via
gen_ref_pages) and writes `mkdocs.gen.yml` with the real API nav spliced
in — that generated file is what `zensical build`/`serve` consumes.
Usage:
python scripts/docs/build_config.py
"""
from __future__ import annotations
import posixpath
import re
from pathlib import Path
# Both scripts live in this directory, which Python puts on sys.path[0] when
# `build_config.py` is run directly (its documented invocation).
import gen_ref_pages
import yaml
ROOT = Path(__file__).parent.parent.parent
# A scheme-prefixed nav value (https:, mailto:, ...) is an external link, not
# a page path (same classifier as llms_txt.py; a `://` test would misread
# scheme-only URIs as pages).
_EXTERNAL = re.compile(r"[a-zA-Z][a-zA-Z0-9+.-]*:")
def _nav_pages(nav: list) -> set[str]:
"""Collect every local page reference in the nav (external links excluded)."""
pages: set[str] = set()
for entry in nav:
value = next(iter(entry.values())) if isinstance(entry, dict) else entry
if isinstance(value, list):
pages |= _nav_pages(value)
elif not _EXTERNAL.match(value):
pages.add(value)
return pages
def _validate_nav(nav: list, docs_dir: Path) -> None:
"""Fail on nav/page drift in either direction.
Zensical (0.0.48) ships a nav entry for a nonexistent page as a broken
link without any diagnostic even under --strict, and publishes a page
that no nav entry reaches as unreachable orphan HTML; MkDocs aborted the
build on both (--strict with `validation.omitted_files: warn`).
Validating here keeps those guarantees. The generated `api/` tree is
exempt from the orphan check: its nav is spliced in from the same
generator that writes the files, so it cannot drift.
"""
pages = _nav_pages(nav)
# Containment before existence: `docs_dir / page` would happily resolve
# an absolute value or a `../` escape against the wrong root.
if escaping := sorted(p for p in pages if p.startswith("/") or posixpath.normpath(p).startswith("..")):
raise SystemExit(f"build_config: nav references pages outside docs/: {escaping}")
if missing := sorted(page for page in pages if not (docs_dir / page).is_file()):
raise SystemExit(f"build_config: nav references pages that don't exist under docs/: {missing}")
# Dot-directories (e.g. `.overrides` theme files) are not pages: the site
# builder ignores them, so the orphan check must too.
relative = (page.relative_to(docs_dir) for page in docs_dir.rglob("*.md"))
on_disk = {page.as_posix() for page in relative if not any(part.startswith(".") for part in page.parts)}
if orphaned := sorted(page for page in on_disk - pages if not page.startswith("api/")):
raise SystemExit(f"build_config: pages under docs/ that no nav entry reaches: {orphaned}")
def build_config() -> None:
config = yaml.safe_load((ROOT / "mkdocs.yml").read_text(encoding="utf-8"))
api_nav = gen_ref_pages.generate()
if not api_nav:
raise SystemExit("build_config: gen_ref_pages produced no API pages — did the src/ layout move?")
for entry in config["nav"]:
if isinstance(entry, dict) and "API Reference" in entry:
entry["API Reference"] = api_nav
break
else:
raise SystemExit("build_config: no 'API Reference' entry found in mkdocs.yml nav")
_validate_nav(config["nav"], ROOT / "docs")
output = ROOT / "mkdocs.gen.yml"
output.write_text(yaml.safe_dump(config, sort_keys=False, allow_unicode=True), encoding="utf-8")
if __name__ == "__main__":
build_config()
+170
View File
@@ -0,0 +1,170 @@
"""Fail the docs build when a page's cross-references did not resolve.
Zensical (0.0.48) stays green even under `--strict` on two failure modes
MkDocs strict mode aborted on:
- An unresolvable `[text][identifier]` cross-reference renders as literal
bracket text (mkdocs-autorefs used to warn). The generated API index and
the docstring cross-references rely on such references resolving.
- A failed `objects.inv` inventory download is logged as an ERROR record and
otherwise ignored, silently degrading every link through that inventory
(thousands of standard-library links alone) to plain text.
Both are caught from the built site itself, so no log-wording change can
disarm the check: an unresolved reference leaves a tell-tale bracket
sequence in prose text (code blocks legitimately contain `][`, e.g. dict
indexing, so only text outside `<pre>`/`<code>` counts), and every inventory
declared in `mkdocs.yml` must contribute at least one resolved reference —
an `autorefs-external` anchor, which hand-authored prose links to the same
host never carry — to the site (an inventory that contributes none is dead
config and fails too).
Offline contributors can skip the inventory check by setting
`DOCS_ALLOW_INVENTORY_FAILURE=1`; CI (`CI=true`) never skips it.
Usage:
python scripts/docs/check_crossrefs.py --site-dir site
"""
from __future__ import annotations
import argparse
import os
import re
import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urlsplit
import yaml
ROOT = Path(__file__).parent.parent.parent
# Unresolved cross-reference tell-tales in extracted prose (`\x00` marks a
# skipped code element, see _ProseTextExtractor): the two-part
# `[text][identifier]` reconstruction — the identifier part is always plain
# text, so a code mark inside the second brackets means indexing prose like
# `data[`x`][`y`]`, not a reference — and the shortcut `[`identifier`]` form,
# which extracts as `[\x00]` unless a preceding word character or bracket
# makes it a subscript like `list[`str`]`.
_UNRESOLVED = re.compile(r"\]\[[^\]\s\x00]*\]|(?<![\w\]\x00])\[\x00\]")
# An inventory-resolved reference renders as an anchor with the
# `autorefs-external` class; a hand-authored prose link to the same host has
# no autorefs class and must not satisfy the inventory check.
_EXTERNAL_REF = re.compile(r"<a\s[^>]*autorefs-external[^>]*>")
class _ProseTextExtractor(HTMLParser):
"""Collect text outside <pre>/<code>/<script>/<style> elements.
A skipped element leaves a `\\x00` mark. Block-level tag boundaries break
the text with a newline, so bracket sequences cannot be synthesized by
joining text from unrelated blocks (`x]</td><td>[y`); inline tags break
nothing, because their text genuinely flows within one block — a
subscript like `**tools**[`0`]` must extract as `tools[\\x00]` so the
word-character carve-out in `_UNRESOLVED` still applies. A block-level
tag also resets the skip state, so an unclosed inline `<code>` in
authored raw HTML cannot hide the rest of the page.
"""
_SKIP = frozenset({"pre", "code", "script", "style"})
_BLOCK = frozenset(
{"article", "blockquote", "caption", "dd", "details", "div", "dl", "dt", "figcaption", "figure"}
| {"h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "section", "summary"}
| {"table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul"}
)
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self._skip_depth = 0
self.chunks: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag in self._SKIP:
if not self._skip_depth:
self.chunks.append("\x00")
self._skip_depth += 1
elif tag in self._BLOCK:
self._skip_depth = 0
self.chunks.append("\n")
elif tag == "br" and not self._skip_depth:
# A line break separates text but implies nothing about open
# elements (`<br>` is legal inside `<code>`), so unlike block
# tags it must not reset the skip state.
self.chunks.append("\n")
def handle_endtag(self, tag: str) -> None:
if tag in self._SKIP:
if self._skip_depth:
self._skip_depth -= 1
elif tag in self._BLOCK and not self._skip_depth:
self.chunks.append("\n")
def handle_data(self, data: str) -> None:
if not self._skip_depth:
self.chunks.append(data)
def unresolved_refs(html: str) -> list[str]:
"""Return the unresolved cross-reference fragments rendered in `html`."""
parser = _ProseTextExtractor()
parser.feed(html)
return [fragment.replace("\x00", "<code>") for fragment in _UNRESOLVED.findall("".join(parser.chunks))]
def _inventory_origins() -> set[str]:
"""The scheme+host origins of the inventories declared in mkdocs.yml."""
config = yaml.safe_load((ROOT / "mkdocs.yml").read_text(encoding="utf-8"))
for plugin in config["plugins"]:
if isinstance(plugin, dict) and "mkdocstrings" in plugin:
inventories = plugin["mkdocstrings"]["handlers"]["python"].get("inventories", [])
return {_origin(entry["url"] if isinstance(entry, dict) else entry) for entry in inventories}
return set()
def _origin(url: str) -> str:
parts = urlsplit(url)
return f"{parts.scheme}://{parts.netloc}"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--site-dir", default=str(ROOT / "site"), help="The built site directory to scan.")
args = parser.parse_args()
site_dir = Path(args.site_dir)
# rglob on a missing directory yields nothing, which would read as a
# clean site (or a bogus inventory failure); fail up front instead.
if not site_dir.is_dir():
raise SystemExit(f"check_crossrefs: {site_dir} not found (run the build first)")
unlinked = _inventory_origins()
failures: list[str] = []
for page in sorted(site_dir.rglob("*.html")):
html = page.read_text(encoding="utf-8")
if unlinked and "autorefs-external" in html:
for tag in _EXTERNAL_REF.finditer(html):
unlinked -= {origin for origin in unlinked if f'href="{origin}' in tag.group(0)}
# Both tell-tales have a literal signature in the raw HTML ("][" for
# the two-part form, "[<code" for the shortcut form); skip the parse
# for the majority of pages that contain neither.
if "][" in html or "[<code" in html:
failures.extend(f"{page}: {fragment}" for fragment in unresolved_refs(html))
if failures:
print("error: unresolved cross-references rendered as literal text:", file=sys.stderr)
print("\n".join(failures[:20]), file=sys.stderr)
raise SystemExit(1)
offline_ok = os.environ.get("DOCS_ALLOW_INVENTORY_FAILURE") == "1" and os.environ.get("CI") != "true"
if unlinked and not offline_ok:
print(
"error: no page links into these declared inventories (download failed, or dead"
" inventory config?): " + ", ".join(sorted(unlinked)),
file=sys.stderr,
)
print("set DOCS_ALLOW_INVENTORY_FAILURE=1 to build offline", file=sys.stderr)
raise SystemExit(1)
if __name__ == "__main__":
main()
+232
View File
@@ -0,0 +1,232 @@
"""Generate the API reference pages and navigation.
Zensical does not run MkDocs plugins, so the work that `mkdocs-gen-files` and
`mkdocs-literate-nav` used to do at build time happens here as a plain
pre-build step: this module writes a mkdocstrings stub (`::: <module>`) for
every public module under `docs/api/` and returns the matching nested
navigation, which `scripts/docs/build_config.py` splices into the build config.
Run as a script it just (re)generates `docs/api/`; imported, `generate`
also returns the nav so the config builder can consume it.
"""
from __future__ import annotations
import shutil
from pathlib import Path
import griffe
# A MkDocs/Zensical nav is a list of entries, each either `{title: url}` for a
# page or `{title: [children]}` for a section (a bare `url` string attaches
# a section index page, courtesy of the `navigation.indexes` feature).
NavItem = "str | dict[str, str | list[NavItem]]"
ROOT = Path(__file__).parent.parent.parent
API_DIR = ROOT / "docs" / "api"
# `src/mcp-types` is a distribution directory, not an import package, so each
# package's dotted module path is taken relative to its own parent: deriving
# it from `src/` would emit the unimportable `mcp-types.mcp_types.*`.
PACKAGES = (ROOT / "src" / "mcp", ROOT / "src" / "mcp-types" / "mcp_types")
_KIND_SECTIONS = {
griffe.Kind.MODULE: "Modules",
griffe.Kind.CLASS: "Classes",
griffe.Kind.FUNCTION: "Functions",
griffe.Kind.ATTRIBUTE: "Attributes",
griffe.Kind.TYPE_ALIAS: "Type aliases",
}
class _Node:
"""A module (`url`) and/or a package with child modules (`children`)."""
def __init__(self) -> None:
self.url: str | None = None
self.children: dict[str, _Node] = {}
def child(self, name: str) -> _Node:
return self.children.setdefault(name, _Node())
def to_nav(self, title: str) -> NavItem:
if not self.children:
assert self.url is not None
return {title: self.url}
items: list[NavItem] = []
if self.url is not None:
items.append(self.url)
items.extend(self.children[name].to_nav(name) for name in sorted(self.children))
return {title: items}
def _compact_index(module: griffe.Module, documented: set[str]) -> str | None:
"""Build a compact page body for a module that re-exports from outside its own subtree.
mkdocstrings renders a re-export whose canonical documentation lives on
another page as a full duplicate of it: deterministically for aliases
within one top-level package (`mcp.client.auth` re-exporting from
`mcp.shared.auth`), and order-dependently across top-level packages
(`from mcp_types import y` + `__all__` renders the duplicate only when
the other package happens to be loaded already, and silently omits the
member when it isn't). Modules whose exports all live in their own
subtree (`mcp_types` re-exporting its private `._types` module, or a
module whose `__all__` lists only its own definitions) are unaffected
and keep the plain `::: module` stub (return `None`): their page is
itself the canonical rendering.
For an affected module, pin the semantics instead of inheriting the
accident: every export whose canonical page exists elsewhere under the API
reference becomes a link to it, and only exports documented nowhere else
(re-exports from private modules) keep their full body here, via an
explicit `members:` list.
"""
prefix = f"{module.path}."
exports: dict[str, griffe.Object | griffe.Alias] = {}
for export in module.exports or ():
name = str(export)
# Listed exports must be statically documentable: a name provided at
# runtime (module `__getattr__`) is a docs error by policy, not a skip.
if (member := module.members.get(name)) is None:
msg = f"gen_ref_pages: export {module.path}.{name} is not statically visible"
raise SystemExit(msg)
exports[name] = member
if not any(member.is_alias and not member.target_path.startswith(prefix) for member in exports.values()):
return None
# A plain stub also renders the module's own public members that are not
# in `__all__` (`show_if_no_docstring: false` hides the docstring-less
# ones); keep them, so flipping a page to compact drops nothing.
public = dict(exports)
for name, member in module.members.items():
if (
name not in public
and not name.startswith("_")
and not member.is_alias
and member.kind is not griffe.Kind.MODULE
and member.has_docstrings
):
public[name] = member
inline: list[str] = []
sections: dict[str, list[str]] = {}
for name in sorted(public, key=str.lower):
member = public[name]
try:
target = member.final_target if member.is_alias else member
except griffe.AliasResolutionError as exc:
msg = f"gen_ref_pages: export {module.path}.{name} resolves outside the documented packages"
raise SystemExit(msg) from exc
# Link to the anchor another page actually renders: the deepest alias
# hop whose module is documented (the final target may live in a
# private module and only be re-exported by a public one), rendered
# there only when docstringed (`show_if_no_docstring: false`).
anchor = None
hop = member
while hop.is_alias:
if hop.target_path.rpartition(".")[0] in documented:
anchor = hop.target_path
hop = hop.target
if anchor is not None and member.has_docstrings:
link_target = anchor
else:
inline.append(name)
link_target = f"{module.path}.{name}"
entry = f"- [`{name}`][{link_target}]"
if docstring := target.docstring:
summary = " ".join(docstring.value.split("\n\n", 1)[0].split("\n"))
entry += f"{summary}"
sections.setdefault(_KIND_SECTIONS[target.kind], []).append(entry)
# Rendering the stub resolves the cross-package aliases again, in
# mkdocstrings' own collection. On a warm incremental rebuild the target
# package's pages can all be cache hits, so nothing else loads it and the
# resolution crashes (AliasResolutionError); preloading pins it. The
# module's own root package needs no pin: rendering the stub loads it.
preload = sorted(
{member.target_path.split(".")[0] for member in exports.values() if member.is_alias}
- {module.path.split(".")[0]}
)
body = [f"::: {module.path}", " options:"]
if preload:
body += [" preload_modules:", *(f" - {pkg}" for pkg in preload)]
if inline:
body += [" members:", *(f" - {name}" for name in inline)]
else:
body += [" members: false"]
body.append("")
for title in _KIND_SECTIONS.values():
if title in sections:
body += [f"## {title}", "", *sections[title], ""]
return "\n".join(body)
def _stub(title: str, body: str) -> str:
"""A stub page: explicit title frontmatter plus the page body.
The explicit title matters: the stubs have no H1 of their own, and a
title-less page falls back to "Index"/the filename — which is what
pruned nav rows, browser tabs, and search results show.
"""
return f'---\ntitle: "{title}"\n---\n\n{body.rstrip()}\n'
def generate() -> list[NavItem]:
"""Write `docs/api/**.md` stubs and return the API-section navigation."""
if API_DIR.exists():
shutil.rmtree(API_DIR)
root = _Node()
stubs: dict[Path, str] = {}
pages: dict[str, Path] = {}
documented: set[str] = set()
for package in PACKAGES:
base = package.parent
for path in sorted(package.rglob("*.py")):
module_path = path.relative_to(base).with_suffix("")
doc_path = path.relative_to(base).with_suffix(".md")
parts = tuple(module_path.parts)
if parts[-1] == "__init__":
parts = parts[:-1]
doc_path = doc_path.with_name("index.md")
# A private component anywhere makes the module private: checking
# only the leaf would publish pages for e.g. mcp._vendor.util.
if any(part.startswith("_") for part in parts):
continue
ident = ".".join(parts)
documented.add(ident)
stubs[API_DIR / doc_path] = _stub(parts[-1], f"::: {ident}")
pages[ident] = API_DIR / doc_path
node = root
for part in parts:
node = node.child(part)
node.url = f"api/{doc_path.as_posix()}"
# Load the root packages before inspecting any module: aliases only
# resolve once the module they point at is in the loader's collection.
loader = griffe.GriffeLoader(search_paths=[str(package.parent) for package in PACKAGES])
for package in PACKAGES:
loader.load(package.name)
for ident, doc_path in pages.items():
try:
module = loader.modules_collection[ident]
except KeyError as exc:
raise SystemExit(f"gen_ref_pages: cannot find {ident} in the loaded packages") from exc
if not isinstance(module, griffe.Module):
raise SystemExit(f"gen_ref_pages: {ident} is shadowed by a non-module member")
if body := _compact_index(module, documented):
stubs[doc_path] = _stub(ident.rpartition(".")[2], body)
for full_doc_path, stub in stubs.items():
full_doc_path.parent.mkdir(parents=True, exist_ok=True)
full_doc_path.write_text(stub, encoding="utf-8")
return [root.children[name].to_nav(name) for name in sorted(root.children)]
if __name__ == "__main__":
generate()
+355
View File
@@ -0,0 +1,355 @@
"""Generate llms.txt, llms-full.txt, and per-page markdown (https://llmstxt.org/).
Zensical has no equivalent of MkDocs' build hooks, so this runs as a standalone
post-build step over the source tree (`mkdocs.yml` + `docs/`) and writes
three kinds of artifact into the built `site/`:
- `llms.txt`: a markdown index of the documentation, one link per page,
grouped by nav section.
- a `.md` rendition of every prose page next to its HTML (e.g.
`servers/tools/index.md`), which is what the llms.txt links point at.
- `llms-full.txt`: every prose page concatenated for single-fetch consumption.
Page markdown is the source markdown with YAML frontmatter stripped, `--8<--`
snippet includes resolved (so the `docs_src/` code examples appear inline) and
relative links rewritten to absolute URLs. The API reference pages under
`api/` are mkdocstrings stubs with no prose source, so they are linked as
rendered HTML from an Optional section instead of being embedded.
Usage:
python scripts/docs/llms_txt.py --site-dir site
"""
from __future__ import annotations
import argparse
import posixpath
import re
from pathlib import Path, PurePosixPath
from typing import Any
import yaml
ROOT = Path(__file__).parent.parent.parent
DOCS = ROOT / "docs"
# Pages with no markdown source, linked as HTML under "## Optional".
_OPTIONAL_PAGES = [
("api/mcp/index.md", "mcp API reference", "Auto-generated API reference for the mcp package (rendered HTML)"),
(
"api/mcp_types/index.md",
"mcp-types API reference",
"Auto-generated API reference for the mcp-types package (rendered HTML)",
),
]
_SNIPPET_LINE = re.compile(r'^(?P<indent>[ \t]*)--8<-- "(?P<path>[^"\n]+)"$', flags=re.MULTILINE)
# Every markdown link/image target: `](target#anchor "title")`. Each target is
# classified in `_rewrite_links` — there is deliberately no shape-based
# pre-filter here, so no link can dodge validation by its spelling. Zensical's
# own link validation only covers .md targets (a missing image or
# directory-style link builds green even under --strict; MkDocs failed the
# build), so everything else is validated here.
_LINK = re.compile(r'(\]\([ \t]*)([^)\s]+?)(#[^)\s]*)?( +(?:"[^"]*"|\'[^\']*\'|\([^()]*\)))?([ \t]*\))')
# CommonMark forms the classifier deliberately rejects rather than models:
# angle-bracket destinations `](<target>)` and reference-style definitions
# `[label]: target` (footnote definitions `[^label]:` are a different,
# supported syntax). Either would otherwise dodge validation by its spelling;
# failing loud keeps the guarantee without modelling unused syntax.
_ANGLE_LINK = re.compile(r"\]\([ \t]*<")
_REF_DEFINITION = re.compile(r"^[ \t]*\[(?!\^)[^\]]+\]:", flags=re.MULTILINE)
# Block HTML comments are inert in rendered output: python-markdown passes
# them through verbatim, so commented-out prose must not be validated.
_HTML_COMMENT = re.compile(r"<!--.*?-->", flags=re.DOTALL)
# A scheme-prefixed target (https:, mailto:, tel:, ...) is external — the
# `://` shorthand misses scheme-only URIs like mailto:.
_EXTERNAL = re.compile(r"[a-zA-Z][a-zA-Z0-9+.-]*:")
# Fenced code blocks and inline code spans: their content is inert in the
# rendered HTML, so links inside them are illustrative text, neither validated
# nor rewritten. Fences are matched line-based in `_code_intervals` (closer at
# least as long as the opener, unclosed runs to EOF, per CommonMark) and spans
# only in the text between fences; a span cannot cross a blank line, so a
# stray unpaired backtick cannot swallow the paragraphs (and links) after it.
# Known approximations of the renderer's block model: 4-space-indented
# content is treated as prose, because in this corpus indentation is
# admonition/list body whose links must stay validated — a link in a true
# indented code block is over-validated (fails loud or gets rewritten in the
# rendition), never under-validated; and span pairing is bounded by blank
# lines rather than full block structure.
_FENCE = re.compile(r"^[ \t]*(`{3,}|~{3,})")
_CODE_SPAN = re.compile(r"(?s)(?<!`)(`+)(?!`)((?:(?!\n[ \t]*\n).)+?)(?<!`)\1(?!`)")
# A leading YAML frontmatter block, as MkDocs/Zensical parse it (mkdocs.utils.meta).
_FRONTMATTER = re.compile(r"\A---[ \t]*\n(?P<block>.*?)^(?:---|\.\.\.)[ \t]*(?:\n|\Z)", flags=re.MULTILINE | re.DOTALL)
class _BuildError(Exception):
"""A recoverable problem that should fail the docs build with a clear message."""
def _dest_md_uri(src_uri: str) -> str:
"""Map a source page (`servers/tools.md`) to its built rendition (`servers/tools/index.md`)."""
path = PurePosixPath(src_uri)
directory = path.parent if path.stem == "index" else path.parent / path.stem
return "index.md" if directory == PurePosixPath(".") else f"{directory}/index.md"
def _page_url(src_uri: str) -> str:
"""The directory URL of a page relative to the site root (`servers/tools/`, `""` for the home page)."""
return _dest_md_uri(src_uri).removesuffix("index.md")
def _split_frontmatter(text: str) -> tuple[dict[str, Any], str]:
"""Split a leading YAML frontmatter block from a page (mirrors mkdocs.utils.meta).
Hand-rolled deliberately: mkdocs is only a transitive dependency of this
toolchain, so the pipeline must not import it. The hook this replaced ran
post-frontmatter-extraction, so renditions never contained frontmatter and
`meta` fed the page title and llms.txt description. A leading block that
isn't a YAML mapping is page content, not frontmatter; an empty block is
frontmatter with no meta.
"""
match = _FRONTMATTER.match(text)
if match is None:
return {}, text
try:
meta = yaml.safe_load(match["block"])
except yaml.YAMLError:
return {}, text
if meta is not None and not isinstance(meta, dict):
return {}, text
return meta or {}, text[match.end() :].lstrip("\n")
def _collect_pages(items: list, prose: dict[str, str | None]) -> list[str]:
"""Collect the prose pages under a nav subtree, in nav order.
Records each page in `prose` (src_uri -> nav title, or `None` to fall
back to the page's H1). This is the single owner of the prose-page rule:
a page entry counts when it is a local .md path (external URLs render as
outbound nav links and are omitted, as the MkDocs pipeline did) and is not
part of the generated API reference.
"""
pages: list[str] = []
for entry in items:
title, value = next(iter(entry.items())) if isinstance(entry, dict) else (None, entry)
if isinstance(value, list):
pages.extend(_collect_pages(value, prose))
elif not _EXTERNAL.match(value) and value.endswith(".md") and not value.startswith("api/"):
# Contained values only: an escaping entry would write its
# rendition outside the built site.
if value.startswith("/") or posixpath.normpath(value).startswith(".."):
raise _BuildError(f"llms_txt: nav entry {value!r} escapes docs/")
prose[value] = title
pages.append(value)
return pages
def _walk_nav(nav: list, prose: dict[str, str | None], sections: list[tuple[str, list[str]]]) -> list[str]:
"""Split the nav into a flat list of top-level pages and titled sections.
Populates `sections` ((title, [src_uri]) in nav order) and returns the
top-level page src_uris; page collection itself is `_collect_pages`.
"""
top_level: list[str] = []
for entry in nav:
title, value = next(iter(entry.items())) if isinstance(entry, dict) else (None, entry)
if isinstance(value, list):
pages = _collect_pages(value, prose)
if pages:
assert title is not None
sections.append((title, pages))
else:
top_level.extend(_collect_pages([entry], prose))
return top_level
def _resolve_snippets(markdown: str, src_uri: str) -> str:
def include(match: re.Match[str]) -> str:
indent, path = match["indent"], match["path"]
# Reject snippet paths that escape the repo root (mirrors the snippets
# extension's restrict_base_path).
resolved = (ROOT / path).resolve()
if not resolved.is_relative_to(ROOT.resolve()):
raise _BuildError(f"llms_txt: snippet path {path!r} in {src_uri} escapes the repo root")
try:
content = resolved.read_text(encoding="utf-8").rstrip("\n")
except OSError as exc:
raise _BuildError(f"llms_txt: cannot read snippet {path!r} in {src_uri}") from exc
if path.endswith(".py"):
content = f"# {path}\n{content}"
if indent:
content = "\n".join(indent + line if line else line for line in content.split("\n"))
return content
resolved, substitutions = _SNIPPET_LINE.subn(include, markdown)
if substitutions != sum("--8<--" in line for line in markdown.splitlines()):
raise _BuildError(f"llms_txt: unresolved snippet include in {src_uri}")
return resolved
def _in_code(code: list[tuple[int, int]], position: int) -> bool:
"""Whether `position` falls inside any code interval."""
return any(start <= position < end for start, end in code)
def _prose_h1(markdown: str) -> re.Match[str] | None:
"""The first ATX H1 outside code (at most 3 spaces of indent, per CommonMark).
Code-awareness matters: every resolved `.py` snippet starts with a
`# path` pointer line that must never win over the page's real H1.
"""
code = _code_intervals(markdown)
for match in re.finditer(r"^ {0,3}# (.+)$", markdown, flags=re.MULTILINE):
if not _in_code(code, match.start()):
return match
return None
def _code_intervals(markdown: str) -> list[tuple[int, int]]:
"""The character spans of fenced code blocks and inline code spans."""
fences: list[tuple[int, int]] = []
opener = ""
start = offset = 0
for line in markdown.splitlines(keepends=True):
if not opener:
if match := _FENCE.match(line):
opener, start = match[1], offset
elif (stripped := line.strip()).startswith(opener) and set(stripped) == {opener[0]}:
fences.append((start, offset + len(line)))
opener = ""
offset += len(line)
if opener:
fences.append((start, len(markdown)))
intervals = list(fences)
previous_end = 0
for fence_start, fence_end in [*fences, (len(markdown), len(markdown))]:
segment = markdown[previous_end:fence_start]
for pattern in (_CODE_SPAN, _HTML_COMMENT):
intervals += [(previous_end + m.start(), previous_end + m.end()) for m in pattern.finditer(segment)]
previous_end = fence_end
return intervals
def _rewrite_links(markdown: str, src_uri: str, site_url: str, prose: dict[str, str | None]) -> str:
src_dir = posixpath.dirname(src_uri)
code = _code_intervals(markdown)
rejected = ((_ANGLE_LINK, "angle-bracket link destination"), (_REF_DEFINITION, "reference-style link definition"))
for pattern, form in rejected:
for match in pattern.finditer(markdown):
if not _in_code(code, match.start()):
raise _BuildError(f"llms_txt: {form} in {src_uri} is not supported here; use a plain inline link")
def rewrite(match: re.Match[str]) -> str:
opening, target, anchor, title, closing = match.groups()
if target.startswith("#") or _EXTERNAL.match(target):
return match.group(0) # in-page anchor or external URL (https:, mailto:, ...)
if _in_code(code, match.start()):
return match.group(0) # illustrative link inside a code block/span
if target.startswith("/"):
raise _BuildError(f"llms_txt: absolute link target {target!r} in {src_uri}: link the .md source instead")
linked = posixpath.normpath(posixpath.join(src_dir, target))
if linked == ".." or linked.startswith("../"):
raise _BuildError(f"llms_txt: link target {target!r} in {src_uri} escapes docs/")
if (DOCS / linked).is_dir():
raise _BuildError(
f"llms_txt: directory-style link target {target!r} in {src_uri}: link the page's .md source instead"
)
if not (DOCS / linked).is_file():
raise _BuildError(f"llms_txt: cannot resolve link target {target!r} in {src_uri}")
if linked.endswith(".md"):
# Pages without a markdown rendition (the api/ stubs) link to their HTML instead.
url = _dest_md_uri(linked) if linked in prose else _page_url(linked)
else:
url = linked # assets are published at their docs-relative path
return f"{opening}{site_url}{url}{anchor or ''}{title or ''}{closing}"
return _LINK.sub(rewrite, markdown)
def _title(src_uri: str, nav_title: str | None, meta: dict[str, Any], body: str) -> str:
if nav_title is not None:
return nav_title
if isinstance(meta_title := meta.get("title"), str):
return meta_title
if match := _prose_h1(body):
return match.group(1).strip()
raise _BuildError(f"llms_txt: page {src_uri} has no nav title, no title frontmatter, and no H1")
def generate(site_dir: Path) -> None:
if not (DOCS / "api").is_dir():
raise _BuildError("llms_txt: docs/api not found (run gen_ref_pages first)")
config = yaml.safe_load((ROOT / "mkdocs.yml").read_text(encoding="utf-8"))
site_url = config["site_url"].rstrip("/") + "/"
prose: dict[str, str | None] = {}
sections: list[tuple[str, list[str]]] = []
top_level = _walk_nav(config["nav"], prose, sections)
ordered: list[tuple[str, list[str]]] = ([("Docs", top_level)] if top_level else []) + sections
rendered: dict[str, str] = {}
metas: dict[str, dict[str, Any]] = {}
for src_uri in prose:
metas[src_uri], markdown = _split_frontmatter((DOCS / src_uri).read_text(encoding="utf-8"))
markdown = _resolve_snippets(markdown, src_uri)
rendered[src_uri] = _rewrite_links(markdown, src_uri, site_url, prose)
index = [f"# {config['site_name']}", "", f"> {config['site_description']}", ""]
full: list[str] = []
for section_title, pages in ordered:
index += [f"## {section_title}", ""]
for src_uri in pages:
markdown = rendered[src_uri]
md_uri = _dest_md_uri(src_uri)
(site_dir / md_uri).parent.mkdir(parents=True, exist_ok=True)
(site_dir / md_uri).write_text(markdown, encoding="utf-8")
title = _title(src_uri, prose[src_uri], metas[src_uri], markdown)
description = metas[src_uri].get("description")
tail = f": {description}" if description else ""
index.append(f"- [{title}]({site_url}{md_uri}){tail}")
# `full` re-titles every page, so drop its first prose H1 (the
# same one `_title` falls back to).
h1 = _prose_h1(markdown)
body = markdown if h1 is None else markdown[: h1.start()] + markdown[h1.end() :]
full += [f"# {title}", "", f"Source: {site_url}{_page_url(src_uri)}", "", body.strip(), ""]
index.append("")
index += ["## Optional", ""]
# _OPTIONAL_PAGES must match the generated package indexes exactly: a
# package added to gen_ref_pages.PACKAGES without an entry here would be
# published on the site but silently missing from llms.txt, and a stale
# entry would link a page that no longer exists.
generated = {f"api/{path.name}/index.md" for path in (DOCS / "api").iterdir() if path.is_dir()}
listed = {src_uri for src_uri, _, _ in _OPTIONAL_PAGES}
if generated != listed:
raise _BuildError(
f"llms_txt: _OPTIONAL_PAGES out of sync with docs/api:"
f" missing {sorted(generated - listed)}, stale {sorted(listed - generated)}"
)
for src_uri, title, description in _OPTIONAL_PAGES:
index.append(f"- [{title}]({site_url}{_page_url(src_uri)}): {description}")
index.append("")
(site_dir / "llms.txt").write_text("\n".join(index), encoding="utf-8")
(site_dir / "llms-full.txt").write_text("\n".join(full), encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--site-dir", default=str(ROOT / "site"), help="The built site directory to write into.")
args = parser.parse_args()
try:
generate(Path(args.site_dir))
except _BuildError as exc:
raise SystemExit(str(exc)) from exc
except OSError as exc:
raise SystemExit(f"llms_txt: {exc}") from exc
if __name__ == "__main__":
main()
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
#
# Serve the v2 documentation locally with live reload.
#
# Regenerates the API reference and the concrete Zensical config, then serves
# it. Re-run the script to pick up changes to `src/` (the API reference) or the
# nav; edits to prose pages under `docs/` are picked up by live reload.
#
# Usage:
# scripts/serve-docs.sh [<extra zensical serve args>...]
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
uv run --frozen python scripts/docs/build_config.py
exec uv run --frozen zensical serve -f mkdocs.gen.yml "$@"
+1 -1
View File
@@ -138,7 +138,7 @@ def test_every_example_is_included_by_a_page() -> None:
def test_every_included_path_exists() -> None:
"""Every `docs_src/` path a page includes exists on disk.
`mkdocs build --strict` also enforces this, but only when the docs are
`zensical build --strict` also enforces this, but only when the docs are
built; this puts the same guarantee inside the ordinary `pytest` run.
"""
missing = sorted(filterfalse(_is_real_file, _referenced_examples()))
Generated
+108 -283
View File
@@ -91,29 +91,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
]
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "backrefs"
version = "5.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" },
{ url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" },
{ url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" },
{ url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" },
{ url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" },
]
[[package]]
name = "black"
version = "26.3.1"
@@ -158,34 +135,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" },
]
[[package]]
name = "cairocffi"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" },
]
[[package]]
name = "cairosvg"
version = "2.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cairocffi" },
{ name = "cssselect2" },
{ name = "defusedxml" },
{ name = "pillow" },
{ name = "tinycss2" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" },
]
[[package]]
name = "certifi"
version = "2025.8.3"
@@ -526,19 +475,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
]
[[package]]
name = "cssselect2"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tinycss2" },
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
]
[[package]]
name = "datamodel-code-generator"
version = "0.57.0"
@@ -560,12 +496,12 @@ wheels = [
]
[[package]]
name = "defusedxml"
version = "0.7.1"
name = "deepmerge"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
{ url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" },
]
[[package]]
@@ -641,15 +577,12 @@ wheels = [
]
[[package]]
name = "griffe"
version = "1.14.0"
name = "griffelib"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" }
sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" },
]
[[package]]
@@ -959,12 +892,11 @@ dev = [
{ name = "trio" },
]
docs = [
{ name = "mkdocs" },
{ name = "mkdocs-gen-files" },
{ name = "mkdocs-glightbox" },
{ name = "mkdocs-literate-nav" },
{ name = "mkdocs-material", extra = ["imaging"] },
{ name = "griffelib" },
{ name = "mkdocstrings" },
{ name = "mkdocstrings-python" },
{ name = "pyyaml" },
{ name = "zensical" },
]
[package.metadata]
@@ -1016,12 +948,11 @@ dev = [
{ name = "trio", specifier = ">=0.26.2" },
]
docs = [
{ name = "mkdocs", specifier = ">=1.6.1,<2" },
{ name = "mkdocs-gen-files", specifier = ">=0.5.0" },
{ name = "mkdocs-glightbox", specifier = ">=0.4.0" },
{ name = "mkdocs-literate-nav", specifier = ">=0.6.1" },
{ name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.7.0,<10" },
{ name = "mkdocstrings-python", specifier = ">=2.0.1" },
{ name = "griffelib", specifier = "==2.1.0" },
{ name = "mkdocstrings", specifier = "==1.0.4" },
{ name = "mkdocstrings-python", specifier = "==2.0.5" },
{ name = "pyyaml", specifier = ">=6.0.2" },
{ name = "zensical", specifier = "==0.0.50" },
]
[[package]]
@@ -1541,18 +1472,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" },
]
[[package]]
name = "mkdocs-gen-files"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mkdocs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/35/f26349f7fa18414eb2e25d75a6fa9c7e3186c36e1d227c0b2d785a7bd5c4/mkdocs_gen_files-0.6.0.tar.gz", hash = "sha256:52022dc14dcc0451e05e54a8f5d5e7760351b6701eff816d1e9739577ec5635e", size = 8642, upload-time = "2025-11-23T12:13:22.124Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/ec/72417415563c60ae01b36f0d497f1f4c803972f447ef4fb7f7746d6e07db/mkdocs_gen_files-0.6.0-py3-none-any.whl", hash = "sha256:815af15f3e2dbfda379629c1b95c02c8e6f232edf2a901186ea3b204ab1135b2", size = 8182, upload-time = "2025-11-23T12:13:20.756Z" },
]
[[package]]
name = "mkdocs-get-deps"
version = "0.2.0"
@@ -1567,70 +1486,9 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" },
]
[[package]]
name = "mkdocs-glightbox"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "selectolax" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/72/c03e9d8d2dbe098d7ce5d51309933a1d3aea268965ed097ab16f4b54de15/mkdocs_glightbox-0.5.1.tar.gz", hash = "sha256:7d78a5b045f2479f61b0bbb17742ba701755c56b013e70ac189c9d87a91e80bf", size = 480028, upload-time = "2025-09-04T13:10:29.679Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/cf/e9a0ce9da269746906fdc595c030f6df66793dad1487abd1699af2ba44f1/mkdocs_glightbox-0.5.1-py3-none-any.whl", hash = "sha256:f47af0daff164edf8d36e553338425be3aab6e34b987d9cbbc2ae7819a98cb01", size = 26431, upload-time = "2025-09-04T13:10:27.933Z" },
]
[[package]]
name = "mkdocs-literate-nav"
version = "0.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mkdocs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/5f/99aa379b305cd1c2084d42db3d26f6de0ea9bf2cc1d10ed17f61aff35b9a/mkdocs_literate_nav-0.6.2.tar.gz", hash = "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75", size = 17419, upload-time = "2025-03-18T21:53:09.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/84/b5b14d2745e4dd1a90115186284e9ee1b4d0863104011ab46abb7355a1c3/mkdocs_literate_nav-0.6.2-py3-none-any.whl", hash = "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", size = 13261, upload-time = "2025-03-18T21:53:08.1Z" },
]
[[package]]
name = "mkdocs-material"
version = "9.7.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "babel" },
{ name = "backrefs" },
{ name = "colorama" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "mkdocs" },
{ name = "mkdocs-material-extensions" },
{ name = "paginate" },
{ name = "pygments" },
{ name = "pymdown-extensions" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" },
]
[package.optional-dependencies]
imaging = [
{ name = "cairosvg" },
{ name = "pillow" },
]
[[package]]
name = "mkdocs-material-extensions"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
]
[[package]]
name = "mkdocstrings"
version = "0.30.0"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
@@ -1640,24 +1498,24 @@ dependencies = [
{ name = "mkdocs-autorefs" },
{ name = "pymdown-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" },
{ url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" },
]
[[package]]
name = "mkdocstrings-python"
version = "2.0.1"
version = "2.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffe" },
{ name = "griffelib" },
{ name = "mkdocs-autorefs" },
{ name = "mkdocstrings" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" },
{ url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" },
]
[[package]]
@@ -1805,15 +1663,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "paginate"
version = "0.5.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
]
[[package]]
name = "pathspec"
version = "1.0.4"
@@ -2112,11 +1961,11 @@ wheels = [
[[package]]
name = "pygments"
version = "2.19.2"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
@@ -2135,15 +1984,15 @@ crypto = [
[[package]]
name = "pymdown-extensions"
version = "10.16.1"
version = "11.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" }
sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" },
{ url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" },
]
[[package]]
@@ -2579,54 +2428,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" },
]
[[package]]
name = "selectolax"
version = "0.3.29"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/b9/b5a23e29d5e54c590eaad18bdbb1ced13b869b111e03d12ee0ae9eecf9b8/selectolax-0.3.29.tar.gz", hash = "sha256:28696fa4581765c705e15d05dfba464334f5f9bcb3eac9f25045f815aec6fbc1", size = 4691626, upload-time = "2025-04-30T15:17:37.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/8f/bf3d58ecc0e187806299324e2ad77646e837ff20400880f6fc0cbd14fb66/selectolax-0.3.29-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85aeae54f055cf5451828a21fbfecac99b8b5c27ec29fd10725b631593a7c9a3", size = 3643657, upload-time = "2025-04-30T15:15:40.734Z" },
{ url = "https://files.pythonhosted.org/packages/de/b0/6d90a4d0eacb8253d88a9fcbcb8758b667900f45dcdb4a11c5fbd0d31599/selectolax-0.3.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ff48efe4364c8148a553a4105773a0accee9cc25e0f2a40ddac44d18a5a3000", size = 2089380, upload-time = "2025-04-30T15:15:42.928Z" },
{ url = "https://files.pythonhosted.org/packages/f4/21/394b51998ef99f13f98da063fc71b8edf7191bb30aca06bcbc8a55d5a9ad/selectolax-0.3.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25cfccfefc41361ab8a07f15a224524a4a8b77dfa7d253b34bbd397e45856734", size = 5505065, upload-time = "2025-04-30T15:15:44.986Z" },
{ url = "https://files.pythonhosted.org/packages/dd/57/e38775b672f910e80742cbf7c3def5c670c1b6f9b05e8587b2fa8dc044c3/selectolax-0.3.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5c3523ad5199a4fb9b95b6e24ff9222d3605023ca394b23f7dd910e7536daf", size = 5529205, upload-time = "2025-04-30T15:15:47.149Z" },
{ url = "https://files.pythonhosted.org/packages/ec/0f/f6e3030107b486b6a4870f8471a675d435c4c34b8f9de3374652ed53004b/selectolax-0.3.29-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfb803d6bbe0ef3c8847cf5a01167cc428c0d9179946e1c994cc6178b5332d1a", size = 5146713, upload-time = "2025-04-30T15:15:49.332Z" },
{ url = "https://files.pythonhosted.org/packages/d8/8d/b4fd119c216e8615ca6747f8f336643572178241921f33f5ffa4b074dc44/selectolax-0.3.29-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:db734ba4ef44fa3b57ad9374fd7ccfc7815c0ae5cfcbd5ee25fe8587092618d1", size = 5416352, upload-time = "2025-04-30T15:15:50.909Z" },
{ url = "https://files.pythonhosted.org/packages/d7/e7/94e694d14ae44bddc0d9b144647d5adbec0210d8e2c57d72ad9a133d9469/selectolax-0.3.29-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2bfe4327215a20af4197c5b7e3729a9552fb324bb57250dc7e7abfa0f848a463", size = 5140689, upload-time = "2025-04-30T15:15:52.477Z" },
{ url = "https://files.pythonhosted.org/packages/90/62/79ba965daa1f12e5477b2ec08b289f8289dfc705928b08923d9c4b60c867/selectolax-0.3.29-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a98c3f3d8fffb175456cb06096bc78103ddf6a209bea6392e0e4ea4e25aca71", size = 5481428, upload-time = "2025-04-30T15:15:54.371Z" },
{ url = "https://files.pythonhosted.org/packages/82/3c/46c1f0b739add89d0ef720ad521afaaf31b07a39f781ef9e59c7b5ecef44/selectolax-0.3.29-cp310-cp310-win32.whl", hash = "sha256:394d356ea611a7853c13c910a57c1a80a8356f9c920aa8168b3f8aaa62e433d8", size = 1702100, upload-time = "2025-04-30T15:15:55.833Z" },
{ url = "https://files.pythonhosted.org/packages/75/62/03350ed454fe26aef5580df498d45ace9f26ca6af1640ae681a6af1f5cdf/selectolax-0.3.29-cp310-cp310-win_amd64.whl", hash = "sha256:edd2760699c60dde7d847aebd81f02035f7bddcd0ad3db8e73326dfc84a2dc8f", size = 1807811, upload-time = "2025-04-30T15:15:57.243Z" },
{ url = "https://files.pythonhosted.org/packages/2a/5d/ca72f7adddae4b2b128394a7559739a6a12c156d29b55968cfcfe07fac4d/selectolax-0.3.29-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6a1cd0518fa7656ea1683c4b2d3b5a98306753f364da9f673517847e1680a3e", size = 3649215, upload-time = "2025-04-30T15:15:59.57Z" },
{ url = "https://files.pythonhosted.org/packages/08/c6/ca984f90b12fb10790cc56c2670f1b5f09884ed2f2012a219094b38cbcb4/selectolax-0.3.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e5354d805dd76b4b38002f58e6ae2e7b429ac311bf3601992a6662d2bc86911", size = 2091848, upload-time = "2025-04-30T15:16:01.73Z" },
{ url = "https://files.pythonhosted.org/packages/98/7f/c999ae6d9bfbaac3e8dea3dbb5ca6bdf61c220828e80a6c339e89f9db777/selectolax-0.3.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7073e3bcdc60ebdb5f8777c79b465471ec000ab556134da4e00f037d3321a2ec", size = 5638593, upload-time = "2025-04-30T15:16:03.594Z" },
{ url = "https://files.pythonhosted.org/packages/d6/32/ffd89376a888c24ecaf01fcffc5fe97b82ae03ab163158f51a559f1ebad5/selectolax-0.3.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47587db7cef411d22f8224cf2926aacdb326c4c838d386035229f16ccc2d8d26", size = 5668207, upload-time = "2025-04-30T15:16:05.564Z" },
{ url = "https://files.pythonhosted.org/packages/3a/5c/2de0c7b8be75ad52d44706c67946181b972f27641ab4f6a1f27f46d2a603/selectolax-0.3.29-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21de62b5093b1cb6c5d4cab0bef5f708b9ee1483b640d42be9d955becfcd287a", size = 5276654, upload-time = "2025-04-30T15:16:07.143Z" },
{ url = "https://files.pythonhosted.org/packages/29/29/152bb745b24072d3eecd3b395c756e74763111b9bbd265604f5b96b9a1aa/selectolax-0.3.29-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:af5cd03298cd75cb0fbf712d6ae4f8aca9c13a226d2821ca82f51cc9b33b032f", size = 5543731, upload-time = "2025-04-30T15:16:09.733Z" },
{ url = "https://files.pythonhosted.org/packages/04/1d/df65baaf16ece393f9f1a7c55f015510634adbb163ce72adcafaddf5cf9c/selectolax-0.3.29-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3f58dca53d2d3dc18dfd2cb9210a5625f32598db24e3f857f5be58f21a8f3b88", size = 5275005, upload-time = "2025-04-30T15:16:11.958Z" },
{ url = "https://files.pythonhosted.org/packages/5d/74/e56fd6f9b3087947b812f3862df3265bf5e21396d9673d076e999b1086cf/selectolax-0.3.29-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a6d8e02c6b9ba951d7b5a5dd2788a1d4bbdedc89782a4de165f1a87c4168ac", size = 5617441, upload-time = "2025-04-30T15:16:14.15Z" },
{ url = "https://files.pythonhosted.org/packages/63/d6/243049029bfc937b9f02faf4a4494e693575046414a475bf28ed9632b768/selectolax-0.3.29-cp311-cp311-win32.whl", hash = "sha256:912a1fc03157ebd066d8f59ae9ca2412ef95c7101a51590327c23071b02c97c7", size = 1701370, upload-time = "2025-04-30T15:16:16.339Z" },
{ url = "https://files.pythonhosted.org/packages/c9/7f/baba8c5ce941c8cbd2dfb0c9f2253ba2d8c2d5d0fddda4f5a87eceb2484f/selectolax-0.3.29-cp311-cp311-win_amd64.whl", hash = "sha256:a3d44a295416b79815d2858ed4ccb71bf3b63087483a5d3705daa837c9dcf44d", size = 1808251, upload-time = "2025-04-30T15:16:18.289Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/ca4332eecc19124782f6f0d7cb28c331da2e9d9cf25287ba2b3b6a00cea1/selectolax-0.3.29-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6d3f373efd1db18ac9b2222de2668aaa366a1f0b560241eab128f3ca68e8add1", size = 3656166, upload-time = "2025-04-30T15:16:19.907Z" },
{ url = "https://files.pythonhosted.org/packages/b8/46/2dcae03a94f80f3e0d339c149de8110b5abe1230668b015fd338d9e71a27/selectolax-0.3.29-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:97b9971bb37b54ef4440134f22792d15c9ee12d890a526a7fe0b376502240143", size = 2095991, upload-time = "2025-04-30T15:16:21.654Z" },
{ url = "https://files.pythonhosted.org/packages/1e/bd/95f15396e5f30898227d84a7ec6a39d9a9b34005f0e9f8f38e7fee21ab66/selectolax-0.3.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd99ff0f5a6c017c471635d4ee45b61d25f24689331e407147b2cf5e36892480", size = 5844493, upload-time = "2025-04-30T15:16:23.268Z" },
{ url = "https://files.pythonhosted.org/packages/36/25/64c60da9aec81f2992355b0a3ce00ea1ed99e6f5499868016d6972bd4948/selectolax-0.3.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8377c317bf1d5fd6ccc56dfb5a0928bbcbea3e800b7af54761cfbbb99dc94cb9", size = 5881062, upload-time = "2025-04-30T15:16:24.891Z" },
{ url = "https://files.pythonhosted.org/packages/b6/81/94105217f91f7c6a98ac3164210cba0c6aa8da91cb85405292a6d70e39c3/selectolax-0.3.29-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5388c56456272b2c241fc1906db9cc993984cafdad936cb5e061e3af0c44144e", size = 5470368, upload-time = "2025-04-30T15:16:26.457Z" },
{ url = "https://files.pythonhosted.org/packages/51/6e/40bc259f13e5d3dd0bb8ddd1d55ef099244db2568ffb82fd9d489984d61a/selectolax-0.3.29-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9e4690894f406863e25ba49da27e1a6fda9bfc21b0b315c399d3093be080e81", size = 5693476, upload-time = "2025-04-30T15:16:28.386Z" },
{ url = "https://files.pythonhosted.org/packages/58/bd/2668ee1d5471ad88daf83ca484515ba46774fc9c951d6c4c0beffea89952/selectolax-0.3.29-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:deeab93386b6c9a75052515f5b9e7e3dd623c585871c0c2b3126970ff902603b", size = 5449747, upload-time = "2025-04-30T15:16:30.626Z" },
{ url = "https://files.pythonhosted.org/packages/a1/b5/1c61839ae5af70a8291c643982a99f051b543df90b220b98db1b26bd4899/selectolax-0.3.29-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6abdd8357f1c105c1add01a9f0373511fa832548b2e2778b00a8ba2a4508d6ed", size = 5786843, upload-time = "2025-04-30T15:16:32.231Z" },
{ url = "https://files.pythonhosted.org/packages/67/08/ca42c100ab90168c123e6b521e38cb7618b697a693fdb77e42dabb0670fd/selectolax-0.3.29-cp312-cp312-win32.whl", hash = "sha256:9c969626b2295702076f50aac91e44c3bba639fa2e1a612bf6ae254bf29b4d57", size = 1697859, upload-time = "2025-04-30T15:16:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/5c/22/9524af51d950cc718bd4406f3bed05acbfcb321a4a308ec85b96ccdaa1ef/selectolax-0.3.29-cp312-cp312-win_amd64.whl", hash = "sha256:e7f4cc1b7ce9691559decfd5db7cc500e71a9f6ccfe76c054f284c184a1d1dc9", size = 1804145, upload-time = "2025-04-30T15:16:35.12Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a7/083a00aa9cb6bef0317baba4269841c366652558d77189275bed2da6aa81/selectolax-0.3.29-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e3112f05a34bf36d36ecc51520b1d98c4667b54a3f123dffef5072273e89a360", size = 3651407, upload-time = "2025-04-30T15:16:37.282Z" },
{ url = "https://files.pythonhosted.org/packages/7e/cd/6c89ac27961ef5f5e9b40eda0d0653b9c95c93485fb8a554bf093eac1c77/selectolax-0.3.29-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:38462ae369897f71da287f1282079c11f1b878b99a4d1d509d1116ce05226d88", size = 2092649, upload-time = "2025-04-30T15:16:38.817Z" },
{ url = "https://files.pythonhosted.org/packages/3e/12/82710124b7b52613fdb9d5c14494a41785eb83e1c93ec7e1d1814c2ce292/selectolax-0.3.29-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdd1e63735f2fb8485fb6b9f4fe30d6c030930f438f46a4a62bd9886ab3c7fd9", size = 5821738, upload-time = "2025-04-30T15:16:40.747Z" },
{ url = "https://files.pythonhosted.org/packages/8b/08/8ceb3eb7fee9743026a4481fccb771f257c82b2c853a1a30271902234eab/selectolax-0.3.29-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea52e0c128e8e89f98ab0ccaabbc853677de5730729a3351da595976131b66e0", size = 5856069, upload-time = "2025-04-30T15:16:42.496Z" },
{ url = "https://files.pythonhosted.org/packages/47/6c/ec2b7aff0f6202e4157415d76bd588108cc518374bf53afa81c122691780/selectolax-0.3.29-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0933659b4250b91317ccd78167e6804389cdaf7ed86c5d034b058a550d23110f", size = 5443255, upload-time = "2025-04-30T15:16:44.083Z" },
{ url = "https://files.pythonhosted.org/packages/cd/90/d5fea46ff191d02c2380a779b119ea6799751b79fcddb2bb230b21b38fc5/selectolax-0.3.29-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0c9005e9089a6b0c6fb6a9f691ddbbb10a3a23ebeff54393980340f3dbcdb99", size = 5637529, upload-time = "2025-04-30T15:16:46.175Z" },
{ url = "https://files.pythonhosted.org/packages/9d/83/7f876a515f5af31f7b948cf10951be896fe6deeff2b9b713640c8ec82fd3/selectolax-0.3.29-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac940963c52f13cdf5d7266a979744949b660d367ce669efa073b557f6e09a18", size = 5379121, upload-time = "2025-04-30T15:16:47.909Z" },
{ url = "https://files.pythonhosted.org/packages/57/cb/7dc739a484b1a17ccf92a23dfe558ae615c232bd81e78a72049c25d1ff66/selectolax-0.3.29-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:484274f73839f9a143f4c13ce1b0a0123b5d64be22f967a1dc202a9a78687d67", size = 5727944, upload-time = "2025-04-30T15:16:49.52Z" },
{ url = "https://files.pythonhosted.org/packages/b7/09/95da4d2919d99a6090327390b84bc5440133196351e5e04c24cccda06cbb/selectolax-0.3.29-cp313-cp313-win32.whl", hash = "sha256:29e71fbd58b90d2920ef91a940680cb5331710fe397925ce9d10c3f2f086bf27", size = 1697529, upload-time = "2025-04-30T15:16:51.123Z" },
{ url = "https://files.pythonhosted.org/packages/0e/17/5a3951da22a4ad8f959088ddc370c68b28dad03190d91fcd137a52410fb9/selectolax-0.3.29-cp313-cp313-win_amd64.whl", hash = "sha256:e13befacff5f78102aa11465055ecb6d4b35f89663e36f271f2b506bcab14112", size = 1803334, upload-time = "2025-04-30T15:16:53.775Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -2696,55 +2497,58 @@ dependencies = [
{ name = "pydantic" },
]
[[package]]
name = "tinycss2"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[package]]
name = "tomli"
version = "2.2.1"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
@@ -2868,15 +2672,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
]
[[package]]
name = "wrapt"
version = "1.17.3"
@@ -2946,6 +2741,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
]
[[package]]
name = "zensical"
version = "0.0.50"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "deepmerge" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "pygments" },
{ name = "pymdown-extensions" },
{ name = "pyyaml" },
{ name = "tomli" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/79/f7959b13c766a831f1248779bb718555f70f40c5b8e68db7de1de9936662/zensical-0.0.50.tar.gz", hash = "sha256:7040e52ebe5e6a275e4edeb351bf2bc314d007f3fb5750f178a38d840723e69c", size = 3979246, upload-time = "2026-07-09T13:52:11.908Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/d2/e126d56642a5fd437eeb5a067b7c0d1f766c08e22d6a708bb923986f1d44/zensical-0.0.50-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:26dda9dbacab84db2743726f83202798e1893fbfddecb6a845812d7a331043ad", size = 12817706, upload-time = "2026-07-09T13:51:29.624Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9b/e1a22fb4ea8fc3e8377d7b394ee06cf0349e7f9a64ecb4c9873dee79b102/zensical-0.0.50-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:376887def6470c57509b48519870f74e2b987e30509cf5c819fcf372771f9403", size = 12691061, upload-time = "2026-07-09T13:51:33.194Z" },
{ url = "https://files.pythonhosted.org/packages/38/f5/16c087635680efb39e1d7b7aa3c2cca548305aa29ad51dee04f37b32b65f/zensical-0.0.50-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbf3034a2cb1a9d2a72a5ee7047688717c93da0d8a90d25dd871db8d5f33a6d", size = 13129505, upload-time = "2026-07-09T13:51:36.383Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/50d02402e53aa01a5c5c116a3522c7bb18d6ae4da964d8b236173b6fa086/zensical-0.0.50-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26fe4167663d544ca385ef420a49d3b738c7606a79d7a62a6bedcbf74cd383e7", size = 13055300, upload-time = "2026-07-09T13:51:39.748Z" },
{ url = "https://files.pythonhosted.org/packages/cf/0f/45f4f3e56a6483ea9f88478b20d6b434718e7c46579239589202e5135ec1/zensical-0.0.50-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8813d1f8895b19520d4a982d6bfcfd06372d73d96e6a63f0d2cf8f3b6036d5a1", size = 13445447, upload-time = "2026-07-09T13:51:43.106Z" },
{ url = "https://files.pythonhosted.org/packages/33/9b/1f5065c664afde6a63e0ada8c14eae75f9e6960df597a5ca28c07340f938/zensical-0.0.50-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a64f957f94f7d8e8847a7f1e8205509ba4b188c93c157c5e7be49bcb8556a127", size = 13098775, upload-time = "2026-07-09T13:51:46.437Z" },
{ url = "https://files.pythonhosted.org/packages/46/65/1e42e18d8f9a0cb9fcc8b871852810c7fe196237b79aaa831e863325091e/zensical-0.0.50-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:781d333bcb42c6a6b92360e05eadc944e2674794e1a95537971de1bf64cb9f89", size = 13305922, upload-time = "2026-07-09T13:51:51.107Z" },
{ url = "https://files.pythonhosted.org/packages/50/36/9cace194ba13aef3bdde8ca2d327f3a05cdf5c456e94f6d896852e2cbd38/zensical-0.0.50-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5d0f86eec76c23efb04566cd444bf025845ac8a02e75c8a6b4af13a39a2a9955", size = 13325488, upload-time = "2026-07-09T13:51:54.883Z" },
{ url = "https://files.pythonhosted.org/packages/b1/80/2891a9316e486794dae43151561b49f3313499cfb2c95c7886abad7bcf2d/zensical-0.0.50-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:392a7299a3c4d1ac7ff288bbed181d77a9fd23e6614180476e01630aaee0d67e", size = 13496866, upload-time = "2026-07-09T13:51:58.257Z" },
{ url = "https://files.pythonhosted.org/packages/7e/09/fbd3af58081a0d399fa913ef1f2fd6a007f2f06f7026c8d6a6096e34b996/zensical-0.0.50-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cac3448d8c8a76dfb43e55908b4ca9ad17596aee89a9287a184c1a0ba7dce2a2", size = 13437565, upload-time = "2026-07-09T13:52:01.766Z" },
{ url = "https://files.pythonhosted.org/packages/d3/79/709311ff202326260ff223fb6e9ddefa3100474c428e3c70a04036cefe0e/zensical-0.0.50-cp310-abi3-win32.whl", hash = "sha256:32b244f96a930f68e049b2e03d50790c120ef701b6277ddd854905a33040c75f", size = 12371966, upload-time = "2026-07-09T13:52:05.207Z" },
{ url = "https://files.pythonhosted.org/packages/e3/e1/4babb30544d7465c95a6a93abc0680b1c51752411a225c2b7f2cb44e80c7/zensical-0.0.50-cp310-abi3-win_amd64.whl", hash = "sha256:2f054769bf96ae46353de3eca2463ad4db6df1da9fde515c6d7fc54c3608e615", size = 12604832, upload-time = "2026-07-09T13:52:09.013Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"