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:
committed by
GitHub
parent
74a242ae7f
commit
4fc8882c02
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
Executable
+40
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+19
@@ -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 "$@"
|
||||
@@ -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()))
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user