add project, change cli file names, use config

This commit is contained in:
2026-09-16 08:17:29 -03:00
parent a29e0708e8
commit 86d051da48
47 changed files with 3218 additions and 2106 deletions

View File

@@ -2,3 +2,4 @@
out/
__pycache__/
*.pyc
.venv/

View File

@@ -1,31 +1,33 @@
# docgen — code to diagram, and to everything else the IR can feed.
#
# Derived from where this file sits, so the folder can be copied anywhere and
# renamed and still work. The logic lives in the Python, never here.
# renamed and still work. The logic lives in the Python, never here: every target
# is one line calling `python3 -m docgen <command>`.
#
# make sync create .venv with every optional group (uv)
# make book SRC=../station the whole operation, measured at both ends
# make run CONFIG=docgen.toml every book a run file lists
# make check prove docgen, on a tree it builds itself
# make check BOOK=out/book/x prove one book — its own level
# make ir SRC=../station extract -> out/ir.json (one step, on its own)
# make graph out/ir.json -> out/graph.svg
# make index out/ir.json -> out/index.md
# make self docgen's book of itself, then check it
# make doctor what this machine has
#
# Every target below is one step of a book and still works alone — that is the
# property the book spine exists to preserve, not to replace.
# Every step target still works alone — that is the property the book spine
# exists to preserve, not to replace. The steps compose by hand too:
#
# The pipeline is three commands and they compose, which is the point:
#
# python3 -m docgen.extractors.python --root SRC -o ir.json
# python3 -m docgen.ops ir.json --overview -o view.json
# python3 -m docgen.emitters dot view.json -o graph.svg --theme dark
# python3 -m docgen extract python --root SRC -o ir.json
# python3 -m docgen view ir.json --overview -o view.json
# python3 -m docgen emit dot view.json -o graph.svg --theme dark
HERE := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
PKG := $(notdir $(HERE))
PARENT := $(patsubst %/,%,$(dir $(HERE)))
PY ?= python3
RUN := PYTHONPATH=$(PARENT) $(PY) -m
VENV_PY := $(HERE)/.venv/bin/python
# The synced environment when there is one (`make sync`), the system Python when
# there is not — so `make check` still works with nothing installed at all.
PY ?= $(if $(wildcard $(VENV_PY)),$(VENV_PY),python3)
CLI := PYTHONPATH=$(PARENT) $(PY) -m $(PKG)
OUT ?= $(HERE)/out
SRC ?=
@@ -34,20 +36,25 @@ OPENAPI ?=
HAR ?=
STYLE ?= lucid
THEME ?=
DEPTH ?= 2
SCALE ?= 0.55
BOOK ?=
SLUG ?=
# NOT `LANG`: that is the shell's locale variable, so `?=` inherits
# en_US.UTF-8 from the environment and --lang rejects it.
# en_US.UTF-8 from the environment and --reader rejects it.
READER ?= python
OVERLAY ?=
CONFIG ?= docgen.toml
ONLY ?=
CHECK ?=
comma := ,
THEME_ARG := $(if $(THEME),--theme $(THEME))
STYLE_ARGS := --style $(STYLE) $(THEME_ARG)
SLUG_ARG := $(if $(SLUG),--slug $(SLUG))
OVER_ARG := $(if $(OVERLAY),--overlay $(OVERLAY))
ONLY_ARGS := $(foreach n,$(subst $(comma), ,$(ONLY)),--only $(n))
.PHONY: help book check ir db code graph index site minimap explore docs view self doctor clean
.PHONY: help sync lock book run check ir db code view graph index site minimap explore docs self doctor clean
help: ## List every target
@echo "docgen — static analysis of a tree, and the artifacts that fall out of it"
@@ -61,62 +68,81 @@ help: ## List every target
@echo " BOOK=/path where a book goes, and which book to check"
@echo " READER=python|code ast, or tree-sitter SLUG=name what to call the book"
@echo " OVERLAY=overlay.json hand-written notebook additions, re-applied every build"
@echo " DEPTH=2 how deep to draw"
@echo " CONFIG=docgen.toml a run file ONLY=a,b just these books"
@echo " CHECK=1 with run: each book's own level after building it"
@echo
@echo " Three levels of test, by what they assert about:"
@echo " make doctor the machine. Never fails."
@echo " make check docgen. Exits 1."
@echo " make check BOOK=<dir> that book. Exits 1."
check: ## Prove docgen (or one book, with BOOK=<dir>)
@if [ -n "$(BOOK)" ]; then \
$(RUN) $(PKG).book.checks "$(BOOK)"; \
else \
$(PY) $(HERE)/selftest.py; \
fi
sync: ## Create .venv with every optional group, from uv.lock
@command -v uv >/dev/null || { echo "Error: uv is not installed — docgen still runs on the system python3" >&2; exit 1; }
@cd $(HERE) && uv sync --all-groups
lock: ## Re-resolve uv.lock after editing pyproject.toml
@cd $(HERE) && uv lock
book: ## SRC (or SCHEMA/OPENAPI/HAR) -> one operation, measured at both ends
@test -n "$(SRC)$(SCHEMA)$(OPENAPI)$(HAR)" \
|| { echo "Error: set SRC=/path/to/tree (or SCHEMA=, OPENAPI=, HAR=)" >&2; exit 1; }
@$(RUN) $(PKG).book \
$(if $(SRC),--root "$(SRC)" --lang $(READER)) \
@$(CLI) book \
$(if $(SRC),--root "$(SRC)" --reader $(READER)) \
$(if $(SCHEMA),--schema "$(SCHEMA)") \
$(if $(OPENAPI),--openapi "$(OPENAPI)") \
$(if $(HAR),--har "$(HAR)") \
-o "$(if $(BOOK),$(BOOK),$(OUT)/book)" \
--style $(STYLE) $(THEME_ARG) $(SLUG_ARG) $(OVER_ARG)
$(STYLE_ARGS) $(SLUG_ARG) $(OVER_ARG)
run: ## CONFIG (a run file) -> every book it lists; ONLY=a,b for some
@$(CLI) run "$(CONFIG)" $(ONLY_ARGS) $(if $(CHECK),--check)
check: ## Prove docgen (or one book, with BOOK=<dir>)
@$(CLI) check $(if $(BOOK),"$(BOOK)")
ir: ## Extract SRC into OUT/ir.json
@test -n "$(SRC)" || { echo "Error: set SRC=/path/to/tree" >&2; exit 1; }
@mkdir -p $(OUT)
@$(RUN) $(PKG).extractors.python --root "$(SRC)" -o $(OUT)/ir.json
@$(RUN) $(PKG).ir $(OUT)/ir.json
@$(CLI) extract python --root "$(SRC)" -o $(OUT)/ir.json
@$(CLI) validate $(OUT)/ir.json
code: ## Extract C#/TypeScript from SRC (needs the `code` group)
@test -n "$(SRC)" || { echo "Error: set SRC=/path/to/tree" >&2; exit 1; }
@$(CLI) extract code --root "$(SRC)" -o $(OUT)/ir.json
@$(CLI) validate $(OUT)/ir.json
db: ## Extract a graphgen-compatible SCHEMA into OUT/ir.json
@test -n "$(SCHEMA)" || { echo "Error: set SCHEMA=/path/to/schema.json" >&2; exit 1; }
@mkdir -p $(OUT)
@$(RUN) $(PKG).extractors --schema "$(SCHEMA)" -o $(OUT)/ir.json
@$(RUN) $(PKG).ir $(OUT)/ir.json
@$(CLI) extract db --schema "$(SCHEMA)" -o $(OUT)/ir.json
@$(CLI) validate $(OUT)/ir.json
view: ## OUT/ir.json -> OUT/view.json, the default view for its source type
@$(RUN) $(PKG).ops $(OUT)/ir.json --overview -o $(OUT)/view.json
@$(CLI) view $(OUT)/ir.json --overview -o $(OUT)/view.json
graph: view ## OUT/view.json -> whatever its structure asks for
@$(RUN) $(PKG).emitters auto $(OUT)/view.json -o $(OUT) \
--style $(STYLE) $(THEME_ARG)
@$(CLI) emit auto $(OUT)/view.json -o $(OUT) $(STYLE_ARGS)
index: ## OUT/ir.json -> OUT/index.md and OUT/sidebar.json
@$(CLI) emit index $(OUT)/ir.json -o $(OUT)/index.md
@$(CLI) emit index $(OUT)/ir.json -o $(OUT)/sidebar.json
site: view ## OUT/view.json -> a self-contained docs site in OUT/site
@$(RUN) $(PKG).emitters site $(OUT)/view.json -o $(OUT)/site \
--style $(STYLE) $(THEME_ARG)
@$(CLI) emit site $(OUT)/view.json -o $(OUT)/site $(STYLE_ARGS)
@echo " open $(OUT)/site/index.html"
minimap: ## OUT/ir.json -> OUT/minimap.svg — what is where, read from the colours
@$(CLI) emit minimap $(OUT)/ir.json -o $(OUT)/minimap.svg $(STYLE_ARGS) --scale $(SCALE)
explore: ## OUT/ir.json -> OUT/explore/ — navigate on one side, explore on the other
@$(CLI) emit explore $(OUT)/ir.json -o $(OUT)/explore $(STYLE_ARGS) --scale $(SCALE)
@echo " open $(OUT)/explore/explore.html"
docs: ## Regenerate the figures in docs/ — docgen documented by docgen
@mkdir -p docs/img
@$(RUN) $(PKG).extractors.python --root $(HERE) -o /tmp/$(PKG)-docs.json >/dev/null
@$(RUN) $(PKG).ops /tmp/$(PKG)-docs.json --overview -o /tmp/$(PKG)-docs-view.json >/dev/null
@$(RUN) $(PKG).emitters dot /tmp/$(PKG)-docs-view.json -o docs/img/architecture.svg -q
@$(RUN) $(PKG).emitters minimap /tmp/$(PKG)-docs.json -o docs/img/minimap.svg --scale 0.5 --width 860
@$(RUN) $(PKG).emitters erd $(OUT)/ir.json -o docs/img/erd.svg 2>/dev/null \
@mkdir -p $(HERE)/docs/img
@$(CLI) extract python --root $(HERE) -o /tmp/$(PKG)-docs.json >/dev/null
@$(CLI) view /tmp/$(PKG)-docs.json --overview -o /tmp/$(PKG)-docs-view.json >/dev/null
@$(CLI) emit dot /tmp/$(PKG)-docs-view.json -o $(HERE)/docs/img/architecture.svg -q
@$(CLI) emit minimap /tmp/$(PKG)-docs.json -o $(HERE)/docs/img/minimap.svg --scale 0.5 --width 860
@$(CLI) emit erd $(OUT)/ir.json -o $(HERE)/docs/img/erd.svg 2>/dev/null \
|| echo " (erd figure kept — needs a schema IR at $(OUT)/ir.json to refresh)"
@PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG).emitters.site import VIEWER, _slots, _fill; \
from $(PKG).style import Style; import pathlib; \
@@ -124,25 +150,6 @@ pathlib.Path('$(HERE)/docs/viewer.html').write_text( \
_fill(VIEWER.replace('__TITLE__', 'docgen docs'), _slots(Style.load('lucid'))))"
@echo " open $(HERE)/docs/index.html"
explore: ## OUT/ir.json -> OUT/explore/ — navigate on one side, explore on the other
@$(RUN) $(PKG).emitters explore $(OUT)/ir.json -o $(OUT)/explore \
--style $(STYLE) $(THEME_ARG) --scale $(SCALE)
@echo " open $(OUT)/explore/explore.html"
minimap: ## OUT/ir.json -> OUT/minimap.svg — what is where, read from the colours
@$(RUN) $(PKG).emitters minimap $(OUT)/ir.json -o $(OUT)/minimap.svg \
--style $(STYLE) $(THEME_ARG) --scale $(SCALE)
code: ## Extract C#/TypeScript from SRC (needs tree-sitter)
@test -n "$(SRC)" || { echo "Error: set SRC=/path/to/tree" >&2; exit 1; }
@mkdir -p $(OUT)
@$(RUN) $(PKG).extractors code --root "$(SRC)" -o $(OUT)/ir.json
@$(RUN) $(PKG).ir $(OUT)/ir.json
index: ## OUT/ir.json -> OUT/index.md and OUT/sidebar.json
@$(RUN) $(PKG).emitters index $(OUT)/ir.json -o $(OUT)/index.md
@$(RUN) $(PKG).emitters index $(OUT)/ir.json -o $(OUT)/sidebar.json
self: ## docgen's book of the widest tree it can see, then check it
@$(eval SELF_SRC := $(shell PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG) import reference; \
r = reference.root(); print(r if r else '$(HERE)')"))
@@ -153,18 +160,19 @@ r = reference.root(); print(r if r else '$(HERE)')"))
@$(MAKE) --no-print-directory check BOOK=$(OUT)/book/self
doctor: ## Report whether this machine can run it
@printf 'python : '; $(PY) --version 2>&1 || echo MISSING
@printf 'python : %s ' '$(PY)'; $(PY) --version 2>&1 || echo MISSING
@printf 'uv : '; if ! command -v uv >/dev/null; then echo 'absent — fine; docgen runs on the system python3'; \
elif [ -x $(VENV_PY) ]; then echo "$$(uv --version), .venv synced"; \
else echo "$$(uv --version), .venv not synced — make sync for the optional groups"; fi
@printf 'dot : '; (dot -V 2>&1) || echo 'MISSING — sudo apt install graphviz (only to render)'
@printf 'tree-sit : '; $(PY) -c 'import tree_sitter, tree_sitter_c_sharp, tree_sitter_typescript; print("ok — C# and TypeScript available")' 2>/dev/null || echo 'absent — Python only. pip install tree_sitter tree_sitter_c_sharp tree_sitter_typescript'
@printf 'networkx : '; $(PY) -c 'import networkx; print(networkx.__version__ + " — for lab/ experiments")' 2>/dev/null || echo 'absent — only used in lab/'
@printf 'package : %s (from %s)\n' '$(PKG)' '$(PARENT)'
@printf 'tree-sit : '; $(PY) -c 'import tree_sitter, tree_sitter_c_sharp, tree_sitter_typescript; print("ok — C# and TypeScript available")' 2>/dev/null || echo 'absent — Python only. make sync, or the `code` group'
@printf 'lxml : '; $(PY) -c 'import lxml; print("ok — theme harvesting available")' 2>/dev/null || echo 'absent — only used to harvest a theme'
@printf 'yaml : '; $(PY) -c 'import yaml; print("ok — needed only to read OpenAPI")' 2>/dev/null || echo 'absent — only used by the OpenAPI reader'
@printf 'networkx : '; $(PY) -c 'import networkx; print(networkx.__version__ + " — for lab/ experiments")' 2>/dev/null || echo 'absent — only used in lab/'
@printf 'reference: '; PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG) import reference; print(reference.describe())"
@printf 'styles : '; $(RUN) $(PKG).style 2>/dev/null \
|| $(RUN) $(PKG) 2>/dev/null \
|| PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG).style import Style; print(', '.join(Style.available()))"
@PYTHONPATH=$(PARENT) $(PY) -c "import $(PKG).ir, $(PKG).emitters.dot, $(PKG).ops" >/dev/null 2>&1 \
@printf 'package : %s (from %s)\n' '$(PKG)' '$(PARENT)'
@printf 'styles : '; PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG).style import Style; print(', '.join(Style.available()))"
@PYTHONPATH=$(PARENT) $(PY) -c "import $(PKG).ir, $(PKG).emitters.dot, $(PKG).ops, $(PKG).cli" >/dev/null 2>&1 \
&& echo 'import : ok' || echo 'import : FAILED — is the folder intact?'
clean: ## Delete OUT. Nothing else is ever written to

View File

@@ -14,19 +14,68 @@ extractors/ → graph IR (JSON) → emitters/
```
```bash
make sync # optional: .venv with every group, from uv.lock
make check # prove it, on a tree it builds itself
make self # run the whole thing over soleprint
make ir SRC=../../station/tools/histgen
make index && make graph
make book SRC=/path/to/repo # one whole operation, measured at both ends
make run CONFIG=docgen.toml # every book a run file lists
make help
```
Or as three composable commands, which is what the Makefile is wrapping:
The full documentation is `docs/index.html` — open it in a browser.
## Layout
```
cli/ the command line — every command, and nothing else
book/ an operation: larder measure, steps, web output, run files
extractors/ source -> IR, one per source type
ir/ the contract: schema.json, the dataclasses, the validator
ops/ IR -> a smaller IR
emitters/ IR -> an artifact
notebook/ the notebook spec, before it is an .ipynb
style/ slots, themes, and harvesting a theme from real diagrams
fixtures/ docgen's own test inputs
lab/ sanctioned experiments; nothing imports it
reference.py the one seam to the repo above, for reading OpenAPI
```
Library packages hold **no command-line code** — no argparse, no `__main__.py`,
no `cli_dot.py` beside `dot.py`. It all lives in `cli/`, behind one entry point,
and the selftest asserts it stays there.
`pyproject.toml` declares no required dependency: the structural path is the
stdlib, Python 3.11+. The optional groups (`code`, `openapi`, `harvest`, `lab`)
are pinned in `uv.lock`; `[tool.uv] package = false`, as dataconvert does, since
this is a folder run in place rather than something to install.
## Run files
```toml
# docgen.toml — beside the project it describes; paths relative to this file
[defaults]
out = "out/book"
[[book]]
name = "station"
root = "../soleprint/station"
[[book]]
name = "shop"
schema = "schemas/shop.json"
```
`make run CONFIG=docgen.toml [ONLY=station] [CHECK=1]`. One failing book never
stops the others; unknown keys are refused; a rebuild replaces the previous
build's outputs and never touches a hand-written `checks.py`.
`docgen.example.toml` runs against the shipped fixtures.
Or as three composable commands, which is what the Makefile is wrapping — run
from the directory above this one:
```bash
python3 -m docgen.extractors.python --root SRC -o ir.json
python3 -m docgen.ops ir.json --drop-stdlib -o view.json
python3 -m docgen.emitters dot view.json -o graph.svg --theme dark
python3 -m docgen extract python --root SRC -o ir.json
python3 -m docgen view ir.json --drop-stdlib -o view.json
python3 -m docgen emit dot view.json -o graph.svg --theme dark
```
## DOT collapses three concerns; this separates them
@@ -71,7 +120,7 @@ boundary, and it reads the field lists out of `schema.json` so the two cannot
drift.
```bash
python3 -m docgen.ir ir.json
python3 -m docgen validate ir.json
```
It catches what a schema cannot: an edge naming a node that does not exist, a
@@ -124,7 +173,7 @@ belongs to every consumer at once — the index, the diagram and the diff all wa
"just this subsystem, two hops out, without the stdlib".
```bash
python3 -m docgen.ops ir.json --drop-stdlib --around docgen.ir --hops 2 -o view.json
python3 -m docgen view ir.json --drop-stdlib --around docgen.ir --hops 2 -o view.json
```
`drop_stdlib`, `drop_external`, `only_kinds`, `drop_kinds`, `subtree`,
@@ -191,7 +240,9 @@ about either.
## Testing
```bash
make check # 59 checks, offline, nothing installed
make check # docgen's own suite: 287 after make sync, 272 with nothing
make check BOOK=out/book/x # one book's own level — generated and hand-written checks
make doctor # the machine; never fails
```
**Golden tests go on the IR, never on the SVG.** Graphviz measures label text
@@ -202,7 +253,7 @@ Self-hosting is the honest end-to-end check, and it is where the real bugs came
from — two name-resolution faults that no fixture had reached:
```bash
make self # extract soleprint, and read out/index.md
make self # docgen's book of the widest tree it can see, then its checks
```
## Where this sits

View File

@@ -0,0 +1,8 @@
""" python3 -m docgen <command> [args] — see cli/__init__.py for the commands."""
import sys
from .cli import main
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,10 +1,10 @@
"""
Run a book: larder, the steps, the web output, the book measure.
python3 -m docgen.book --root ../station -o out/book/station
python3 -m docgen book --root ../station -o out/book/station
Composes functions that already exist. Nothing here parses, lays out or styles
anything — `ops.overview`, `emitters.dot`, `emitters.site` and `notebook.spec`
anything — `ops.overview`, `emitters.auto.draw`, `emitters.site` and `notebook.spec`
do all of it, and this decides the order and writes the ledger. If this file
ever starts doing the work, the seam has moved to the wrong place.
@@ -218,6 +218,41 @@ def _load_cell(step_id: str, artifact: str) -> str:
return f'print(load("{artifact}") is not None)'
# What a build writes into a book directory, and therefore what a rebuild may
# remove first. Named, never globbed: a book directory also holds the hand-written
# `checks.py` and `overlay.json`, and a misconfigured output path could point at
# somebody's source tree — so nothing outside this list is ever deleted.
GENERATED = ("book.json", "notebook.ipynb", "steps", "site", "explore")
def clear(out) -> list[str]:
"""Remove a previous build's outputs from `out`. Returns what was removed.
Without this, re-running a book leaves the last run's artifacts beside the new
ledger: a graph that is now `graph.md` keeps its old `graph.svg`, and the site
goes on showing it. The ledger would not list the stale file, so nothing would
say it was stale — which is the failure the book measure exists to prevent.
Only acts on a directory that already holds a `book.json`, i.e. one this code
wrote. Anything else is left exactly as found.
"""
import shutil
out = Path(out)
if not (out / "book.json").is_file():
return []
removed = []
for name in GENERATED:
target = out / name
if target.is_dir():
shutil.rmtree(target)
removed.append(name)
elif target.is_file():
target.unlink()
removed.append(name)
return removed
def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid",
theme: str | None = None, exclude=(), overlay=None, quiet: bool = False) -> Book:
"""The whole book, in one process. Returns it; `book.json` is written."""
@@ -227,6 +262,7 @@ def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid"
out = Path(out)
slug = slug or Path(str(source)).name or "book"
clear(out)
steps_dir = out / "steps"
steps_dir.mkdir(parents=True, exist_ok=True)
@@ -262,32 +298,26 @@ def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid"
style_obj = Style.load(style, theme=theme)
# The drawing, chosen by structure rather than by the caller. `classify`
# already decided; this runs what it named.
# The drawing, chosen by structure rather than by the caller — the same
# `draw()` the `emit auto` and `emit site` commands use, so the three can
# never disagree about what a graph wants.
from ..emitters.auto import draw
drawn = draw(view, style_obj)
graph_rel = None
if verdict["emitter"] == "erd":
from ..emitters.erd import emit as erd_emit
p = steps_dir / "graph.svg"
p.write_text(erd_emit(view, style_obj))
graph_rel, label = "steps/graph.svg", "drawn as an ERD"
elif verdict["emitter"] == "index":
from ..emitters.index import to_markdown
p = steps_dir / "graph.md"
p.write_text(to_markdown(view))
graph_rel, label = None, "not a diagram — written as a list"
labels = {"erd": "drawn as an ERD", "dot": f"drawn as a {verdict['kind']}",
"index": "not a diagram — written as a list"}
if drawn["content"] is None:
book.step("graph", "not drawn", skipped=drawn["why"])
else:
from ..emitters.dot import emit as dot_emit, render
p = steps_dir / "graph.svg"
try:
p.write_bytes(render(dot_emit(view, style_obj,
rankdir=(verdict.get("options") or {}).get("rankdir"))))
graph_rel, label = "steps/graph.svg", f"drawn as a {verdict['kind']}"
except Exception as e: # noqa: BLE001 - graphviz may not be installed
p = None
label = f"not drawn — {type(e).__name__}"
book.step("graph", label, skipped=str(e)[:200])
if p is not None:
book.step("graph", label, p, note=verdict["why"])
p = steps_dir / f"graph{drawn['suffix']}"
if isinstance(drawn["content"], bytes):
p.write_bytes(drawn["content"])
else:
p.write_text(drawn["content"])
if drawn["suffix"] == ".svg":
graph_rel = f"steps/{p.name}"
book.step("graph", labels[drawn["emitter"]], p, note=drawn["why"])
from ..emitters.index import to_markdown, to_sidebar
p = steps_dir / "index.md"

View File

@@ -1,7 +1,7 @@
"""
Does *this* book still hold — the third test level.
python3 -m docgen.book.checks out/book/station
python3 -m docgen check out/book/station
docgen has three levels of test, and they differ by what they assert *about*.
The distinction is rig's, from `rig/ctrl/selftest.sh`, and it is worth keeping
@@ -68,7 +68,7 @@ class Loaded:
if not path.exists():
raise FileNotFoundError(
f"{path} does not exist — is {self.dir} a book directory?\n"
"Build one with: python3 -m docgen.book --root <src> -o <dir>"
"Build one with: python3 -m docgen book --root <src> -o <dir>"
)
self.data = json.loads(path.read_text())
@@ -272,34 +272,3 @@ def _run_cells(cells: list, cwd: Path) -> tuple[int, str | None]:
finally:
os.chdir(previous)
return ran, failure
def main(argv=None) -> int:
import argparse
p = argparse.ArgumentParser(
prog="python3 -m docgen.book.checks",
description="Check one book: the spine's assertions, then its own.",
)
p.add_argument("book", type=Path, help="A book directory (holding book.json).")
p.add_argument("--only", choices=("generated", "custom"),
help="Run one level rather than both.")
args = p.parse_args(argv)
try:
book = Loaded(args.book)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
print(f"{book.dir}{book.data.get('slug', '?')}")
r = Report()
if args.only != "custom":
generated(book, r)
if args.only != "generated":
custom(book, r)
return r.total()
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,327 @@
"""
A run file: every book a project builds, so a rebuild is one command.
python3 -m docgen run docgen.toml
python3 -m docgen run docgen.toml --only station --only shop
python3 -m docgen run docgen.toml --list # what would run, resolved
Books are rebuilt many times — after a merge, before a release, whenever the
source moves — and each one is a source, an output directory and a handful of
options. Typed out every time, those drift: one run excludes `migrations`, the
next forgets. Written down once, the rebuild is exact.
## The shape
# docgen.toml
reference = "../soleprint" # optional; $DOCGEN_REFERENCE wins if set
[defaults]
out = "out/book" # each book goes to <out>/<name>
style = "lucid"
theme = "dark"
exclude = ["migrations", "tests"]
[[book]]
name = "station"
root = "../soleprint/station" # a tree; reader = "python" by default
[[book]]
name = "orders-api"
openapi = "specs/orders.yaml"
overlay = "overlays/orders.json"
out = "/srv/docs/orders" # overrides <defaults.out>/<name>
Each book names **exactly one** source — `root`, `schema`, `openapi` or `har` —
the same choice the `book` command makes you take.
## Three rules, each one a thing that went wrong somewhere else
**Paths are relative to the run file, not to wherever you ran the command.** A
run file is kept beside the project it describes; if its paths meant different
things from different directories, the same file would build different books.
**A book's value replaces the default, for every key.** Including `exclude`,
which is the one where merging looks tempting. One rule nobody has to remember
beats a clever one somebody has to look up.
**Unknown keys are refused, not ignored.** `exlude = [...]` silently doing
nothing is how a rebuild quietly starts reading `node_modules`. The style loader
takes the same line (requirements R30).
## Why TOML
It is read by the stdlib (`tomllib`), it allows comments — and a run file is
hand-written, so the reason a book excludes something belongs next to the
exclusion — and it is already the format of the `pyproject.toml` beside it. The
IR and style files stay JSON, because those are data that tools write; this is
configuration that people write.
"""
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python < 3.11
tomllib = None
SOURCES = {"root": "python", "schema": "db", "openapi": "openapi", "har": "usage"}
READERS = ("python", "code")
TOP_KEYS = {"reference", "defaults", "book"}
DEFAULT_KEYS = {"out", "style", "theme", "exclude", "reader"}
BOOK_KEYS = {"name", "out", "style", "theme", "exclude", "reader", "overlay", *SOURCES}
# A book name becomes a directory name and a slug, so it is held to what is safe
# as both — no separators, nothing that means something to a shell.
NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
class ConfigError(ValueError):
"""A run file that cannot be run. Carries every problem, not the first."""
def __init__(self, path, problems: list[str]):
self.path, self.problems = path, problems
super().__init__(f"{path}: {len(problems)} problem(s)\n " + "\n ".join(problems))
@dataclass
class Entry:
"""One book, fully resolved — nothing relative, nothing defaulted later."""
name: str
kind: str # python | code | db | openapi | usage
source: Path
out: Path
style: str = "lucid"
theme: str | None = None
overlay: Path | None = None
exclude: tuple = ()
def line(self) -> str:
return f"{self.name:<18} {self.kind:<8} {self.source} -> {self.out}"
@dataclass
class RunFile:
path: Path
books: list[Entry] = field(default_factory=list)
reference: Path | None = None
def select(self, names) -> list[Entry]:
"""The named books, in run-file order. An unknown name is an error."""
if not names:
return list(self.books)
known = {b.name for b in self.books}
unknown = [n for n in names if n not in known]
if unknown:
raise ConfigError(self.path, [
f"no book named {n!r} — have: {', '.join(sorted(known))}" for n in unknown
])
wanted = set(names)
return [b for b in self.books if b.name in wanted]
def _path(value, base: Path) -> Path:
# Normalised lexically, so `--list` shows `atlas2/out` rather than
# `atlas2/docgen/../out`. Not resolved: a symlinked project should be named
# the way its owner names it.
p = Path(str(value)).expanduser()
return Path(os.path.normpath(p if p.is_absolute() else base / p))
def _within(inner: Path, outer: Path) -> bool:
inner, outer = inner.resolve(), outer.resolve()
return inner == outer or outer in inner.parents
def load(path) -> RunFile:
"""Read and resolve a run file. Raises ConfigError listing every problem."""
path = Path(path)
if tomllib is None:
raise ConfigError(path, ["run files need Python 3.11+ (tomllib)"])
try:
data = tomllib.loads(path.read_text())
except OSError as e:
raise ConfigError(path, [f"cannot read: {e}"]) from None
except tomllib.TOMLDecodeError as e:
raise ConfigError(path, [f"not valid TOML: {e}"]) from None
base = path.resolve().parent
problems: list[str] = []
for key in sorted(set(data) - TOP_KEYS):
problems.append(f"unknown top-level key {key!r} — have: {', '.join(sorted(TOP_KEYS))}")
defaults = data.get("defaults") or {}
if not isinstance(defaults, dict):
problems.append("[defaults] must be a table")
defaults = {}
for key in sorted(set(defaults) - DEFAULT_KEYS):
problems.append(f"[defaults] has unknown key {key!r} — have: "
f"{', '.join(sorted(DEFAULT_KEYS))}")
reference = None
if "reference" in data:
reference = _path(data["reference"], base)
if not reference.is_dir():
problems.append(f"reference {reference} is not a directory")
raw_books = data.get("book") or []
if not isinstance(raw_books, list) or not raw_books:
problems.append("no books — add at least one [[book]] table")
raw_books = []
books: list[Entry] = []
for i, raw in enumerate(raw_books):
where = f"book[{i}]"
if not isinstance(raw, dict):
problems.append(f"{where} must be a table")
continue
name = raw.get("name")
if isinstance(name, str):
where = f"book {name!r}"
if not isinstance(name, str) or not NAME.match(name):
problems.append(f"{where} needs a name of letters, digits, '.', '_' or '-'")
continue
for key in sorted(set(raw) - BOOK_KEYS):
problems.append(f"{where} has unknown key {key!r} — have: "
f"{', '.join(sorted(BOOK_KEYS))}")
named = [k for k in SOURCES if k in raw]
if len(named) != 1:
problems.append(
f"{where} must name exactly one source ({', '.join(SOURCES)}); "
f"it names {', '.join(named) if named else 'none'}"
)
continue
source_key = named[0]
source = _path(raw[source_key], base)
def pick(key, default=None):
# A book's value replaces the default, for every key.
return raw[key] if key in raw else defaults.get(key, default)
reader = pick("reader", "python")
if source_key == "root":
if reader not in READERS:
problems.append(f"{where}: reader must be one of {READERS}, got {reader!r}")
continue
kind = reader
if not source.is_dir():
problems.append(f"{where}: root {source} is not a directory")
else:
if "reader" in raw:
problems.append(f"{where}: reader only applies to root, not {source_key}")
kind = SOURCES[source_key]
if not source.is_file():
problems.append(f"{where}: {source_key} {source} is not a file")
if "out" in raw:
out = _path(raw["out"], base)
else:
out = _path(defaults.get("out", "out"), base) / name
overlay = _path(raw["overlay"], base) if "overlay" in raw else None
if overlay is not None and not overlay.is_file():
problems.append(f"{where}: overlay {overlay} is not a file")
exclude = pick("exclude", [])
if not isinstance(exclude, list) or not all(isinstance(x, str) for x in exclude):
problems.append(f"{where}: exclude must be a list of directory names")
exclude = []
elif source_key != "root":
# Only an exclude the book sets itself is a mistake. One inherited
# from [defaults] is meant for the trees and simply does not apply —
# refusing it would make a shared default impossible to write.
if "exclude" in raw:
problems.append(f"{where}: exclude only applies to a root, not {source_key}")
exclude = []
# Writing a book inside the tree it reads means the next run reads the
# last run's output. That is a rebuild that changes on every rebuild.
if source_key == "root" and source.is_dir() and _within(out, source):
problems.append(f"{where}: out {out} is inside the tree it reads ({source})")
books.append(Entry(
name=name, kind=kind, source=source, out=out,
style=pick("style", "lucid"), theme=pick("theme"),
overlay=overlay, exclude=tuple(exclude),
))
seen_names, seen_outs = {}, {}
for b in books:
if b.name in seen_names:
problems.append(f"book {b.name!r} is listed twice")
seen_names[b.name] = b
key = b.out.resolve()
if key in seen_outs:
# Two books into one directory: the second clears the first on every
# run, and nothing says so.
problems.append(f"books {seen_outs[key]!r} and {b.name!r} both write to {b.out}")
seen_outs[key] = b.name
if problems:
raise ConfigError(path, problems)
return RunFile(path=path, books=books, reference=reference)
def apply_reference(runfile: RunFile) -> str | None:
"""Point the one seam at the run file's reference — unless the caller's
environment already does. The caller's env beats the file, which is the
precedence rig uses for every setting it has.
"""
from .. import reference as ref
if runfile.reference is None or os.environ.get(ref.ENV_VAR):
return None
os.environ[ref.ENV_VAR] = str(runfile.reference)
return str(runfile.reference)
def run(runfile: RunFile, names=None, *, check: bool = False, quiet: bool = True):
"""Build every selected book, in order. One failure never stops the rest.
Returns one result per book:
{"name", "out", "ok", "error", "lost": [claims], "checks_failed": [names]}
"""
from . import checks as checks_mod
from .build import run as build
apply_reference(runfile)
results = []
for entry in runfile.select(names):
result = {"name": entry.name, "out": str(entry.out), "ok": False,
"error": None, "lost": [], "checks_failed": []}
try:
overlay = None
if entry.overlay is not None:
from ..notebook import spec as spec_mod
overlay = spec_mod.load(entry.overlay)
book = build(entry.kind, entry.source, entry.out, slug=entry.name,
style=entry.style, theme=entry.theme, exclude=entry.exclude,
overlay=overlay, quiet=quiet)
result["lost"] = [r["claim"] for r in book.compare() if not r["ok"]]
except Exception as e: # noqa: BLE001 - one bad book must not cost the run
result["error"] = f"{type(e).__name__}: {e}"
results.append(result)
continue
if check:
import contextlib
import io
report = checks_mod.Report()
loaded = checks_mod.Loaded(entry.out)
sink = io.StringIO() if quiet else None
with contextlib.redirect_stdout(sink) if sink else contextlib.nullcontext():
checks_mod.generated(loaded, report)
checks_mod.custom(loaded, report)
result["checks_failed"] = list(report.failed)
result["ok"] = not result["lost"] and not result["checks_failed"]
results.append(result)
return results

View File

@@ -0,0 +1,75 @@
"""
The command line — every command docgen has, and nothing else.
python3 -m docgen <command> [args]
book one operation, measured at both ends
run every book a run file lists
check docgen's own suite, or one book's own level
extract source -> ir.json
validate check an ir.json at the boundary
view ir.json -> a narrower ir.json
emit ir.json -> an artifact
## Why all of it lives here
The packages beside this one — `extractors/`, `ops/`, `emitters/`, `book/` — are
library code, and they hold no command-line code at all: no argparse, no
`__main__.py`, no `cli_dot.py` sitting next to `dot.py`. Two kinds of file in one
folder makes every folder answer two questions, and the command-line half is the
part that grows copies — each old `cli_*` re-implemented "read the IR, validate
it, load the style" by hand.
So the split is by kind rather than by feature: library in the packages, the
command line here, and `selftest.py` asserts that nothing outside this package
imports `argparse`, so it stays that way without anyone remembering to.
Plain argparse and no scaffold, on purpose. docgen is copied out of the repo as a
folder, and a shared CLI helper that lives in some other tool would tie the copy
back to the repo.
"""
import sys
# command -> (module in this package, one-line description)
COMMANDS = {
"book": ("book", "one operation, measured at both ends"),
"run": ("run", "every book a run file lists"),
"check": ("check", "docgen's own suite, or one book's own level"),
"extract": ("extract", "source -> ir.json"),
"validate": ("validate", "check an ir.json at the boundary"),
"view": ("view", "ir.json -> a narrower ir.json"),
"emit": ("emit", "ir.json -> an artifact"),
}
def usage() -> str:
from .common import PROG
lines = [f"usage: {PROG} <command> [args]", ""]
lines += [f" {name:<9} {desc}" for name, (_, desc) in COMMANDS.items()]
lines += ["", f" {PROG} <command> --help for a command's options"]
return "\n".join(lines)
def main(argv=None) -> int:
from importlib import import_module
from .common import Abort
argv = sys.argv[1:] if argv is None else list(argv)
if not argv or argv[0] in ("-h", "--help", "help"):
print(usage())
return 0 if argv else 2
name, rest = argv[0], argv[1:]
if name not in COMMANDS:
print(f"Error: no command {name!r}\n\n{usage()}", file=sys.stderr)
return 2
module = import_module(f".{COMMANDS[name][0]}", package=__name__)
try:
return module.main(rest)
except Abort as e:
# The only exit path for an expected failure: one line, stderr, exit 1.
print(f"Error: {e}", file=sys.stderr)
return 1

View File

@@ -1,43 +1,47 @@
""" python3 -m docgen.book --root ../station -o out/book/station
"""
python3 -m docgen book --root SRC -o out/book/name [--reader python|code]
python3 -m docgen book --schema schema.json -o DIR
python3 -m docgen book --openapi spec.yaml -o DIR
python3 -m docgen book --har session.har -o DIR
One book: larder measure, the steps, the web output, book measure. The step
artifacts land in `steps/` and are ordinary files nothing here needs this
command to have been the thing that produced them.
Exits 1 when the two ends do not reconcile. The book is still written the
evidence is the point but a build that lost input should fail a pipeline
rather than pass quietly.
"""
import argparse
import sys
from pathlib import Path
KINDS = ("python", "code", "db", "openapi", "usage")
from .common import PROG, Abort, style_args
def main(argv=None) -> int:
p = argparse.ArgumentParser(
prog="python3 -m docgen.book",
description="Run one docgen operation, measured at both ends.",
)
def main(argv) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} book",
description="Run one docgen operation, measured at both ends.")
src = p.add_mutually_exclusive_group(required=True)
src.add_argument("--root", type=Path, help="A source tree (Python, or --lang code).")
src.add_argument("--root", type=Path, help="A source tree.")
src.add_argument("--schema", type=Path, help="A graphgen-compatible schema.json.")
src.add_argument("--openapi", type=Path, help="An OpenAPI document.")
src.add_argument("--har", type=Path, help="A HAR recording.")
p.add_argument("--lang", choices=("python", "code"), default="python",
p.add_argument("--reader", choices=("python", "code"), default="python",
help="With --root: the stdlib ast reader, or tree-sitter. Default python.")
p.add_argument("--output", "-o", type=Path, required=True,
help="The book directory. Created if absent.")
help="The book directory. Created if absent; a previous build is replaced.")
p.add_argument("--slug", help="What to call it. Defaults to the source's name.")
p.add_argument("--style", default="lucid")
p.add_argument("--theme", default=None, help="dark (default) or lucid.")
style_args(p)
p.add_argument("--overlay", type=Path,
help="A hand-written overlay, re-applied on every build.")
help="A hand-written notebook overlay, re-applied on every build.")
p.add_argument("--exclude", action="append", default=[],
help="Directory name to skip. Repeatable.")
p.add_argument("--quiet", "-q", action="store_true")
args = p.parse_args(argv)
if args.root is not None:
kind, source = args.lang, args.root
kind, source = args.reader, args.root
elif args.schema is not None:
kind, source = "db", args.schema
elif args.openapi is not None:
@@ -46,35 +50,24 @@ def main(argv=None) -> int:
kind, source = "usage", args.har
if not Path(source).exists():
print(f"Error: {source} does not exist", file=sys.stderr)
return 1
raise Abort(f"{source} does not exist")
overlay = None
if args.overlay:
if not args.overlay.exists():
# Absent is fine and is the documented default; named-but-missing is
# a typo, and quietly building without it would hide the typo.
print(f"Error: overlay {args.overlay} does not exist", file=sys.stderr)
return 1
raise Abort(f"overlay {args.overlay} does not exist")
from ..notebook import spec as spec_mod
overlay = spec_mod.load(args.overlay)
from .build import run
from ..book.build import run
try:
book = run(kind, source, args.output, slug=args.slug, style=args.style,
theme=args.theme, exclude=tuple(args.exclude), overlay=overlay,
quiet=args.quiet)
except Exception as e: # noqa: BLE001 - the CLI reports, it does not traceback
print(f"Error: {type(e).__name__}: {e}", file=sys.stderr)
return 1
# Exit 1 when the two ends do not reconcile. The book is still written —
# the evidence is the point — but a build that lost input should fail a
# pipeline rather than pass quietly.
lost = [r for r in book.compare() if not r.get("ok")]
return 1 if lost else 0
raise Abort(f"{type(e).__name__}: {e}") from None
if __name__ == "__main__":
sys.exit(main())
return 1 if any(not r["ok"] for r in book.compare()) else 0

View File

@@ -0,0 +1,50 @@
"""
python3 -m docgen check docgen's own suite
python3 -m docgen check <book-dir> that book's own level
python3 -m docgen check <book-dir> --only generated|custom
Two of docgen's three test levels, told apart by what they assert *about*. The
third, the machine, is `make doctor`, and never fails.
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
from .common import PROG, Abort
HERE = Path(__file__).resolve().parent.parent
def main(argv) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} check")
p.add_argument("book", type=Path, nargs="?",
help="A book directory (holding book.json). Omit for docgen's own suite.")
p.add_argument("--only", choices=("generated", "custom"),
help="With a book: run one half rather than both.")
args = p.parse_args(argv)
if args.book is None:
if args.only:
raise Abort("--only applies to a book")
# A subprocess rather than an import: the suite is a script that runs at
# import time and exits, and it has to see a clean interpreter to test
# what a fresh one would do.
return subprocess.call([sys.executable, str(HERE / "selftest.py")])
from ..book.checks import Loaded, Report, custom, generated
try:
book = Loaded(args.book)
except (FileNotFoundError, json.JSONDecodeError) as e:
raise Abort(str(e)) from None
print(f"{book.dir}{book.data.get('slug', '?')}")
r = Report()
if args.only != "custom":
generated(book, r)
if args.only != "generated":
custom(book, r)
return r.total()

View File

@@ -0,0 +1,79 @@
"""
What every command does the same way — written once.
Each old `cli_*` file re-implemented these by hand: read a JSON file and say so
if it cannot, validate it and print the first five problems, load a style and
report a bad name, write to a file or to stdout. Eight copies had already begun
to differ in their wording. Now a command raises `Abort` and `cli.main` turns it
into the one exit path: `Error: <message>` on stderr, exit 1.
"""
import json
import sys
from pathlib import Path
# The package name is derived, not written: the folder can be copied anywhere
# and renamed, and help text saying `docgen` for a folder called `docs2` lies.
ROOT = __package__.rsplit(".", 1)[0]
PROG = f"python3 -m {ROOT}"
class Abort(Exception):
"""An expected failure. Printed as one line, exit 1 — never a traceback."""
def read_json(path: Path) -> dict:
try:
return json.loads(Path(path).read_text())
except (OSError, json.JSONDecodeError) as e:
raise Abort(f"could not read {path}: {e}") from None
def read_ir(path: Path) -> dict:
"""Read an IR and validate it at the boundary, or abort saying why."""
from ..ir import check
data = read_json(path)
problems = check(data)
if problems:
shown = "\n ".join(problems[:5])
more = f"\n … and {len(problems) - 5} more" if len(problems) > 5 else ""
raise Abort(f"{path} is not a valid IR ({len(problems)} problem(s)):\n {shown}{more}")
return data
def load_style(name: str, theme: str | None):
from ..style import Style, StyleError
try:
return Style.load(name, theme=theme)
except StyleError as e:
raise Abort(str(e)) from None
def style_args(p) -> None:
p.add_argument("--style", default="lucid", help="A shipped style, or a path to one.")
p.add_argument("--theme", default=None, help="dark (default) or lucid.")
def emit(text, output: Path | None, report: str = "") -> None:
"""Write to `output`, or to stdout when there is none."""
if output is None:
if isinstance(text, bytes):
sys.stdout.buffer.write(text)
else:
sys.stdout.write(text)
return
output = Path(output)
output.parent.mkdir(parents=True, exist_ok=True)
if isinstance(text, bytes):
output.write_bytes(text)
else:
output.write_text(text)
if report:
print(report)
def dump_ir(ir, output: Path | None, report: str = "") -> None:
data = ir.to_dict() if hasattr(ir, "to_dict") else ir
emit(json.dumps(data, indent=2) + "\n", output, report)

View File

@@ -0,0 +1,252 @@
"""
python3 -m docgen emit auto <ir.json> -o DIR whatever the structure asks for
python3 -m docgen emit dot <ir.json> [-o out.svg|.dot]
python3 -m docgen emit erd <ir.json> [-o out.svg]
python3 -m docgen emit index <ir.json> [-o out.md|.json]
python3 -m docgen emit minimap <ir.json> [-o out.svg] [--scale 0.55]
python3 -m docgen emit notebook <ir.json> [-o out.ipynb] [--overlay f.json]
python3 -m docgen emit site <ir.json> -o DIR
python3 -m docgen emit explore <ir.json> -o DIR
IR -> an artifact. Every emitter validates its input first, so a broken document
is reported here rather than drawn wrong. `--style` and `--theme` apply to every
emitter that draws.
"""
import argparse
import json
import re
import sys
from pathlib import Path
from .common import PROG, Abort, emit as write, load_style, read_ir, style_args
def _parser(name: str, *, styled: bool = True, output_help: str = "") -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog=f"{PROG} emit {name}")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, help=output_help or None)
if styled:
style_args(p)
return p
def _size(svg: str) -> str:
m = re.search(r'width="(\d+)pt" height="(\d+)pt"', svg)
return f"{m.group(1)}x{m.group(2)}" if m else ""
def auto(argv) -> int:
p = _parser("auto", output_help="Directory to write into.")
p.add_argument("--force", help="Use this emitter regardless of what fits.")
args = p.parse_args(argv)
from ..emitters.auto import draw
data, style = read_ir(args.ir), load_style(args.style, args.theme)
drawn = draw(data, style, force=args.force)
print(f" {drawn['verdict']['kind']:<8} -> {drawn['emitter']}")
print(f" {drawn['why']}")
if drawn["content"] is None:
raise Abort(drawn["why"])
out_dir = args.output or Path(".")
path = out_dir / f"{args.ir.stem}{drawn['suffix']}"
write(drawn["content"], path, f" {path}")
return 0
def dot(argv) -> int:
p = _parser("dot", output_help="Write here. .dot or .svg by suffix.")
p.add_argument("--max-depth", type=int, default=None)
p.add_argument("--quiet", "-q", action="store_true", help="Do not warn about shape.")
args = p.parse_args(argv)
from ..emitters.dot import RenderError, emit, render
from ..ops import shape
data, style = read_ir(args.ir), load_style(args.style, args.theme)
# Aspect ratio is a property of the graph, not of the renderer: a layered
# engine puts one dependency level in one row, so the widest level is the
# width. Say so before writing the file, because the alternative is finding
# out from a 13671pt image — and the fix is never a layout flag, it is a
# smaller question.
if not args.quiet:
sh = shape(data)
if sh["widest_level"] > 20 or sh["nodes"] > 60:
est = sh["widest_level"] / max(sh["levels"], 1)
print(f" note: {sh['nodes']} nodes, {sh['levels']} levels, widest level "
f"{sh['widest_level']} — this will render roughly {est:.0f}:1.\n"
" Around 20 nodes is where it stops being a diagram. Try "
"`view --split`,\n `--around <id> --hops 2`, or `--subtree <id>`. "
"Layout flags will not fix it.", file=sys.stderr)
if sh["isolated"] > sh["nodes"] // 3:
print(f" {sh['isolated']} of {sh['nodes']} nodes have no edges; they are "
"laid out side by side.", file=sys.stderr)
dot_text = emit(data, style, max_depth=args.max_depth)
if not args.output or args.output.suffix == ".dot":
write(dot_text, args.output, f" {args.style}/{style.theme:6} {args.output}")
return 0
try:
rendered = render(dot_text, fmt=args.output.suffix.lstrip(".") or "svg")
except RenderError as e:
raise Abort(str(e)) from None
write(rendered, args.output, f" {args.style}/{style.theme:6} {args.output}")
return 0
def erd(argv) -> int:
args = _parser("erd").parse_args(argv)
from ..emitters.erd import emit
data, style = read_ir(args.ir), load_style(args.style, args.theme)
try:
svg = emit(data, style)
except ValueError as e:
raise Abort(str(e)) from None
write(svg, args.output, f" erd/{style.theme:6} {args.output} {_size(svg)}")
return 0
def index(argv) -> int:
p = _parser("index", styled=False,
output_help=".md for the document, .json for a sidebar.")
p.add_argument("--title", default="")
args = p.parse_args(argv)
from ..emitters.index import to_markdown, to_sidebar
data = read_ir(args.ir)
if args.output and args.output.suffix == ".json":
text = json.dumps(to_sidebar(data), indent=2) + "\n"
else:
text = to_markdown(data, title=args.title)
write(text, args.output, f" index {args.output}")
return 0
def minimap(argv) -> int:
p = _parser("minimap")
p.add_argument("--scale", type=float, default=0.55, help="Pixels per source line.")
p.add_argument("--width", type=int, default=1180, help="Wrap a shelf past this.")
args = p.parse_args(argv)
from ..emitters.minimap import emit
data, style = read_ir(args.ir), load_style(args.style, args.theme)
try:
svg = emit(data, style, scale=args.scale, target_width=args.width)
except ValueError as e:
raise Abort(str(e)) from None
write(svg, args.output, f" minimap {args.output} {_size(svg)}")
return 0
def notebook(argv) -> int:
p = _parser("notebook", styled=False)
p.add_argument("--overlay", type=Path, help="Hand-written additions, re-applied.")
p.add_argument("--spec-out", type=Path, help="Write the generated spec too.")
p.add_argument("--scaffold", type=Path, help="Write a blank overlay and stop.")
p.add_argument("--base-url", default="https://api.example.invalid")
args = p.parse_args(argv)
from ..emitters.notebook import emit
from ..notebook import dump, from_ir, merge, scaffold
base = from_ir(read_ir(args.ir), base_url=args.base_url)
if args.scaffold:
dump(scaffold(base), args.scaffold)
print(f" overlay {args.scaffold} {len(base['steps'])} step(s), none filled in")
return 0
overlay = None
if args.overlay:
if args.overlay.exists():
overlay = json.loads(args.overlay.read_text())
else:
print(f" note: no overlay at {args.overlay} — generating the base only",
file=sys.stderr)
spec, drift = merge(base, overlay)
for d in drift:
# The base moved under the overlay. Worth saying out loud; not a reason
# to refuse to build the document.
print(f" drift: {d}", file=sys.stderr)
if args.spec_out:
dump(spec, args.spec_out)
print(f" spec {args.spec_out}")
text = emit(spec)
cells = len(json.loads(text)["cells"])
extra = f", {len(drift)} drift" if drift else ""
write(text, args.output,
f" notebook {args.output} {len(spec['steps'])} steps, {cells} cells{extra}")
return 0
def site(argv) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} emit site")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, required=True, help="Directory.")
style_args(p)
p.add_argument("--title", default="")
p.add_argument("--no-graph", action="store_true")
args = p.parse_args(argv)
from ..emitters.auto import draw
from ..emitters.site import write as site_write
data, style = read_ir(args.ir), load_style(args.style, args.theme)
args.output.mkdir(parents=True, exist_ok=True)
graph_name = None
if not args.no_graph:
# Whatever the structure asks for, so the page carries the right picture
# — the same `draw()` the book and `emit auto` use.
drawn = draw(data, style)
if drawn["content"] is not None and drawn["suffix"] == ".svg":
graph_name = "graph.svg"
write(drawn["content"], args.output / graph_name)
else:
print(f" note: {drawn['verdict']['kind']}{drawn['why']}", file=sys.stderr)
print(" no diagram on the page; the index is the artifact", file=sys.stderr)
for f in site_write(data, style, args.output, graph=graph_name, title=args.title):
print(f" site {f}")
if graph_name:
print(f" site {args.output / graph_name}")
return 0
def explore(argv) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} emit explore")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, required=True, help="Directory.")
style_args(p)
p.add_argument("--scale", type=float, default=0.55)
p.add_argument("--width", type=int, default=1100)
p.add_argument("--hops", type=int, default=1)
p.add_argument("--title", default="")
args = p.parse_args(argv)
from ..emitters.explore import write as explore_write
data, style = read_ir(args.ir), load_style(args.style, args.theme)
try:
path = explore_write(data, style, args.output, scale=args.scale, width=args.width,
hops=args.hops, title=args.title)
except ValueError as e:
raise Abort(str(e)) from None
graphs = args.output / "graphs"
count = len(list(graphs.glob("*.svg"))) if graphs.exists() else 0
print(f" explore {path} {count} neighbourhood diagram(s)")
return 0
EMITTERS = {"auto": auto, "dot": dot, "erd": erd, "index": index, "minimap": minimap,
"notebook": notebook, "site": site, "explore": explore}
def main(argv) -> int:
if not argv or argv[0] in ("-h", "--help"):
print(__doc__.strip("\n"))
return 0 if argv else 2
if argv[0] not in EMITTERS:
raise Abort(f"no emitter {argv[0]!r} — have: {', '.join(EMITTERS)}")
return EMITTERS[argv[0]](argv[1:])

View File

@@ -0,0 +1,118 @@
"""
python3 -m docgen extract python --root SRC [-o ir.json] [--exclude NAME ...]
python3 -m docgen extract code --root SRC [-o ir.json] [--ext .cs ...]
python3 -m docgen extract db --schema schema.json [-o ir.json]
python3 -m docgen extract openapi --spec spec.yaml [-o ir.json]
python3 -m docgen extract usage --har session.har [-o ir.json]
Source -> IR, one reader per source type. Writes to stdout without `-o`, so it
composes with `view` and `emit`.
"""
import argparse
import json
from collections import Counter
from pathlib import Path
from .common import PROG, Abort, dump_ir
def _parser(reader: str, source_flag: str, help_: str) -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog=f"{PROG} extract {reader}")
p.add_argument(source_flag, "-s", required=True, type=Path, help=help_)
p.add_argument("--output", "-o", type=Path, help="Where to write. Default stdout.")
return p
def _counts(g) -> str:
c = Counter(n.kind for n in g.nodes)
return " · ".join(f"{v} {k}" for k, v in sorted(c.items(), key=lambda kv: -kv[1]))
def python(argv) -> int:
p = _parser("python", "--root", "Tree to read.")
p.add_argument("--exclude", action="append", default=[], help="Directory name to skip.")
args = p.parse_args(argv)
from ..extractors.python import extract
try:
g = extract(args.root, exclude=tuple(args.exclude))
except (NotADirectoryError, OSError) as e:
raise Abort(str(e)) from None
unparsed = sum(1 for n in g.nodes if n.attrs.get("error"))
dump_ir(g, args.output,
f"{len(g.nodes)} nodes, {len(g.edges)} edges -> {args.output}"
+ (f" ({unparsed} file(s) unparsed)" if unparsed else ""))
return 0
def code(argv) -> int:
p = _parser("code", "--root", "Tree to read.")
p.add_argument("--ext", action="append", default=[],
help="Limit to these extensions. Default: every registered one.")
p.add_argument("--exclude", action="append", default=[], help="Directory name to skip.")
args = p.parse_args(argv)
from ..extractors.code import MissingParser, extract
try:
g = extract(args.root, suffixes=args.ext or None, exclude=tuple(args.exclude))
except (MissingParser, NotADirectoryError, OSError) as e:
raise Abort(str(e)) from None
dump_ir(g, args.output, f"{len(g.nodes)} nodes -> {args.output} {_counts(g)}")
return 0
def db(argv) -> int:
args = _parser("db", "--schema",
"A graphgen-compatible schema.json, as modelgen emits.").parse_args(argv)
from ..extractors.db import extract
try:
g = extract(args.schema)
except (OSError, json.JSONDecodeError, KeyError) as e:
raise Abort(f"could not read {args.schema}: {e}") from None
dump_ir(g, args.output, f"{len(g.nodes)} nodes, {len(g.edges)} edges -> {args.output}")
return 0
def openapi(argv) -> int:
args = _parser("openapi", "--spec", "An OpenAPI document.").parse_args(argv)
from ..extractors.openapi import extract
try:
g = extract(args.spec)
except (OSError, ImportError, ValueError) as e:
raise Abort(str(e)) from None
eps = sum(1 for n in g.nodes if n.kind == "endpoint")
dump_ir(g, args.output,
f"{len(g.nodes)} nodes ({eps} endpoints), {len(g.edges)} edges -> {args.output}")
return 0
def usage(argv) -> int:
args = _parser("usage", "--har",
"A HAR recording, as devtools/mitmproxy/Charles export.").parse_args(argv)
from ..extractors.usage import extract
try:
g = extract(args.har)
except (OSError, json.JSONDecodeError, KeyError) as e:
raise Abort(f"could not read {args.har}: {e}") from None
eps = sum(1 for n in g.nodes if n.kind == "endpoint")
ops = sum(1 for n in g.nodes if n.kind == "operation")
dump_ir(g, args.output,
f"{len(g.nodes)} nodes ({eps} endpoints, {ops} graphql), "
f"{len(g.edges)} sequence edges -> {args.output}")
return 0
READERS = {"python": python, "code": code, "db": db, "openapi": openapi, "usage": usage}
def main(argv) -> int:
if not argv or argv[0] in ("-h", "--help"):
print(__doc__.strip("\n"))
return 0 if argv else 2
if argv[0] not in READERS:
raise Abort(f"no reader {argv[0]!r} — have: {', '.join(READERS)}")
return READERS[argv[0]](argv[1:])

View File

@@ -0,0 +1,79 @@
"""
python3 -m docgen run [docgen.toml] [--only NAME ...] [--check] [--list]
Every book a run file lists, rebuilt in order. See `book/config.py` for the file.
--only NAME just these books (repeatable), still in run-file order
--check run each book's own level after building it
--list print what would run, fully resolved, and build nothing
One failing book never stops the rest; the run exits 1 if any book failed to
build, lost input between its two ends, or failed its checks. Without a path it
reads `docgen.toml` in the current directory.
"""
import argparse
from pathlib import Path
from .common import PROG, Abort
def main(argv) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} run")
p.add_argument("config", type=Path, nargs="?", default=Path("docgen.toml"))
p.add_argument("--only", action="append", default=[], metavar="NAME")
p.add_argument("--check", action="store_true",
help="Run each book's own level after building it.")
p.add_argument("--list", action="store_true",
help="Print what would run, resolved, and build nothing.")
p.add_argument("--verbose", "-v", action="store_true",
help="Show each book's own output, not just the summary.")
args = p.parse_args(argv)
from ..book.config import ConfigError, load, run
if not args.config.exists():
raise Abort(f"no run file at {args.config} — see docgen.example.toml")
try:
runfile = load(args.config)
selected = runfile.select(args.only)
except ConfigError as e:
raise Abort(str(e)) from None
if args.list:
# The resolved form, because the whole question a run file raises is
# "relative to what" — and the answer should be visible, not inferred.
print(f"{runfile.path}{len(selected)} of {len(runfile.books)} book(s)")
if runfile.reference:
print(f" reference {runfile.reference}")
for b in selected:
print(f" {b.line()}")
return 0
print(f"{runfile.path}{len(selected)} book(s)")
results = run(runfile, [b.name for b in selected], check=args.check,
quiet=not args.verbose)
width = max(len(r["name"]) for r in results)
for r in results:
if r["error"]:
# First line only: a missing-reference error is a paragraph of advice,
# and in a summary table it buries the next book's row.
status, detail = "FAIL", r["error"].splitlines()[0]
elif r["lost"]:
status, detail = "LOST", "; ".join(r["lost"])
elif r["checks_failed"]:
status, detail = "FAIL", f"{len(r['checks_failed'])} check(s): " \
+ ", ".join(r["checks_failed"][:3])
else:
status, detail = "ok ", r["out"]
print(f" {status} {r['name']:<{width}} {detail}")
failed = [r for r in results if not r["ok"]]
print()
if failed:
print(f"{len(failed)} of {len(results)} book(s) did not hold"
+ ("" if args.verbose else " — --verbose for each book's own output"))
return 1
print(f"{len(results)} book(s) built" + (" and checked" if args.check else ""))
return 0

View File

@@ -0,0 +1,32 @@
"""
python3 -m docgen validate <ir.json>
Check a document at the boundary. Also checks that `ir/model.py` and
`ir/schema.json` still agree, since a document can only be as right as the
contract it is checked against.
"""
import argparse
from pathlib import Path
from .common import PROG, Abort, read_json
def main(argv) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} validate")
p.add_argument("ir", type=Path)
args = p.parse_args(argv)
from ..ir.validate import check, check_model_matches_schema
drift = check_model_matches_schema()
if drift:
raise Abort("model.py and schema.json disagree:\n " + "\n ".join(drift))
data = read_json(args.ir)
problems = check(data)
if problems:
raise Abort(f"{args.ir}: {len(problems)} problem(s)\n " + "\n ".join(problems))
print(f"{args.ir}: ok — {len(data['nodes'])} nodes, {len(data['edges'])} edges, "
f"schema v{data['meta'].get('schema_version')}")
return 0

View File

@@ -1,18 +1,19 @@
""" python3 -m docgen.ops <ir.json> [views...] [-o out.json]
"""
python3 -m docgen view <ir.json> [views...] [-o out.json]
Views compose, left to right, in the order given on the command line."""
IR -> a smaller IR. Views compose, applied in a fixed order: overview, the drops,
only, subtree, around, depth. Each produces a document that still validates.
"""
import argparse
import json
import sys
from pathlib import Path
from ..ir import check
from . import filter as F
from .common import PROG, Abort, emit, read_json
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.ops")
def main(argv) -> int:
p = argparse.ArgumentParser(prog=f"{PROG} view")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path)
p.add_argument("--overview", action="store_true",
@@ -32,12 +33,12 @@ def main(argv=None):
help="Report what this will look like, and write nothing.")
args = p.parse_args(argv)
try:
ir = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
from ..ir import check
from ..ops import filter as F
# Read without validating first: a view is often how an oversized document
# is made readable, and the result is validated before it is written.
ir = read_json(args.ir)
before = (len(ir["nodes"]), len(ir["edges"]))
if args.overview:
@@ -60,42 +61,29 @@ def main(argv=None):
ir = F.collapse_to_depth(ir, args.depth)
if args.shape:
sh = F.shape(ir)
for k, v in sh.items():
for k, v in F.shape(ir).items():
print(f" {k:<14} {v}")
return 0
if args.split:
if not args.output:
print("Error: --split needs -o DIRECTORY", file=sys.stderr)
return 1
raise Abort("--split needs -o DIRECTORY")
args.output.mkdir(parents=True, exist_ok=True)
for name, part in F.split(ir).items():
(args.output / f"{name}.json").write_text(json.dumps(part, indent=2) + "\n")
target = args.output / f"{name}.json"
target.write_text(json.dumps(part, indent=2) + "\n")
sh = F.shape(part)
print(f" {name:<16} {sh['nodes']:>4} nodes {sh['edges']:>4} edges "
f"-> {args.output / (name + '.json')}")
print(f" {name:<16} {sh['nodes']:>4} nodes {sh['edges']:>4} edges -> {target}")
return 0
problems = check(ir)
if problems:
# A view that produces an invalid document is a bug in the view, and it
# must not be written out for an emitter to trip over later.
print(f"Error: the view produced an invalid IR ({len(problems)}):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
raise Abort(f"the view produced an invalid IR ({len(problems)}):\n "
+ "\n ".join(problems[:5]))
text = json.dumps(ir, indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
print(f" {before[0]} nodes, {before[1]} edges -> "
f"{len(ir['nodes'])} nodes, {len(ir['edges'])} edges -> {args.output}")
else:
sys.stdout.write(text)
emit(json.dumps(ir, indent=2) + "\n", args.output,
f" {before[0]} nodes, {before[1]} edges -> "
f"{len(ir['nodes'])} nodes, {len(ir['edges'])} edges -> {args.output}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,45 @@
# A run file: every book a project builds, rebuilt with one command.
#
# make run CONFIG=docgen.example.toml
# python3 -m docgen run docgen.example.toml --only schema --check
# python3 -m docgen run docgen.example.toml --list # resolved, nothing built
#
# Copy it next to the project it describes and edit the books. Paths are relative
# to THIS FILE, not to where the command runs — so the same file builds the same
# books from any directory. Unknown keys are refused rather than ignored.
# Optional. Where the repo holding station/tools/modelgen is, for reading
# OpenAPI. $DOCGEN_REFERENCE wins over this when it is set.
# reference = "../../"
[defaults]
out = "out/run" # each book lands in <out>/<name> unless it sets `out`
style = "lucid"
theme = "dark"
# A book's own value REPLACES a default, for every key — exclude included.
exclude = ["__pycache__", "node_modules"]
# A tree of Python — here, docgen's own extractors. A book may not be written
# inside the tree it reads (the next run would read the last run's output), so a
# `root` of "." would need an `out` outside this folder.
[[book]]
name = "extractors"
root = "extractors"
# A database schema, as modelgen writes it.
[[book]]
name = "schema"
schema = "fixtures/shop.json"
# An OpenAPI document. Needs pyyaml and the reference repo; without them this
# one book fails and says why, and the others still build.
[[book]]
name = "api"
openapi = "fixtures/orders.yaml"
theme = "lucid"
# Another tree read with tree-sitter instead (C#, TypeScript):
# [[book]]
# name = "frontend"
# root = "../frontend/src"
# reader = "code"

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -1,386 +1,406 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="886pt" height="2251pt" viewBox="0 0 886 2251">
<rect width="886" height="2251" fill="#0a0a0a"/>
<rect x="28.0" y="46.0" width="74.0" height="247.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.site — 494 lines</title></rect>
<rect x="28.0" y="182.0" width="74.0" height="27.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._ledger" data-kind="function" class="blk"><title>_ledger — function, 54 lines</title></rect>
<rect x="28.0" y="210.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._slots" data-kind="function" class="blk"><title>_slots — function, 13 lines</title></rect>
<rect x="28.0" y="217.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._fill" data-kind="function" class="blk"><title>_fill — function, 4 lines</title></rect>
<rect x="28.0" y="220.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sidebar" data-kind="function" class="blk"><title>_sidebar — function, 17 lines</title></rect>
<rect x="28.0" y="230.0" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sections" data-kind="function" class="blk"><title>_sections — function, 15 lines</title></rect>
<rect x="28.0" y="238.5" width="74.0" height="48.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.emit" data-kind="function" class="blk"><title>emit — function, 96 lines</title></rect>
<rect x="28.0" y="287.5" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.write" data-kind="function" class="blk"><title>write — function, 10 lines</title></rect>
<rect x="110.0" y="46.0" width="74.0" height="158.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.explore — 316 lines</title></rect>
<rect x="110.0" y="65.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._is_schema" data-kind="function" class="blk"><title>_is_schema — function, 4 lines</title></rect>
<rect x="110.0" y="68.5" width="74.0" height="24.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._neighbourhood_svgs" data-kind="function" class="blk"><title>_neighbourhood_svgs — function, 48 lines</title></rect>
<rect x="117.0" y="75.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
<rect x="117.0" y="78.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one#L66" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
<rect x="110.0" y="93.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._facts" data-kind="function" class="blk"><title>_facts — function, 27 lines</title></rect>
<rect x="110.0" y="108.0" width="74.0" height="86.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.emit" data-kind="function" class="blk"><title>emit — function, 172 lines</title></rect>
<rect x="110.0" y="195.0" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.write" data-kind="function" class="blk"><title>write — function, 17 lines</title></rect>
<rect x="192.0" y="46.0" width="74.0" height="146.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.dot — 292 lines</title></rect>
<rect x="192.0" y="64.5" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.RenderError" data-kind="class" class="blk"><title>RenderError — class, 2 lines</title></rect>
<rect x="192.0" y="66.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._esc" data-kind="function" class="blk"><title>_esc — function, 2 lines</title></rect>
<rect x="192.0" y="68.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._attrs" data-kind="function" class="blk"><title>_attrs — function, 3 lines</title></rect>
<rect x="192.0" y="71.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._style_words" data-kind="function" class="blk"><title>_style_words — function, 9 lines</title></rect>
<rect x="192.0" y="76.5" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._node_attrs" data-kind="function" class="blk"><title>_node_attrs — function, 24 lines</title></rect>
<rect x="192.0" y="89.5" width="74.0" height="52.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.emit" data-kind="function" class="blk"><title>emit — function, 105 lines</title></rect>
<rect x="199.0" y="103.0" width="60.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot.emit.write" data-kind="function" class="blk"><title>write — function, 29 lines</title></rect>
<rect x="192.0" y="143.0" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._within" data-kind="function" class="blk"><title>_within — function, 8 lines</title></rect>
<rect x="192.0" y="148.0" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._endpoints" data-kind="function" class="blk"><title>_endpoints — function, 41 lines</title></rect>
<rect x="199.0" y="153.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.walk" data-kind="function" class="blk"><title>walk — function, 4 lines</title></rect>
<rect x="199.0" y="157.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.collapsed" data-kind="function" class="blk"><title>collapsed — function, 2 lines</title></rect>
<rect x="199.0" y="158.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.first_leaf" data-kind="function" class="blk"><title>first_leaf — function, 4 lines</title></rect>
<rect x="192.0" y="169.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._safe" data-kind="function" class="blk"><title>_safe — function, 2 lines</title></rect>
<rect x="192.0" y="171.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._q" data-kind="function" class="blk"><title>_q — function, 2 lines</title></rect>
<rect x="192.0" y="175.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.have_graphviz" data-kind="function" class="blk"><title>have_graphviz — function, 2 lines</title></rect>
<rect x="192.0" y="177.0" width="74.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.render" data-kind="function" class="blk"><title>render — function, 29 lines</title></rect>
<rect x="274.0" y="46.0" width="74.0" height="139.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.notebook — 279 lines</title></rect>
<rect x="274.0" y="67.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._cell" data-kind="function" class="blk"><title>_cell — function, 13 lines</title></rect>
<rect x="274.0" y="74.5" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._example" data-kind="function" class="blk"><title>_example — function, 26 lines</title></rect>
<rect x="274.0" y="108.0" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._params_cell" data-kind="function" class="blk"><title>_params_cell — function, 18 lines</title></rect>
<rect x="274.0" y="118.0" width="74.0" height="20.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_cell" data-kind="function" class="blk"><title>_call_cell — function, 40 lines</title></rect>
<rect x="274.0" y="139.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_md" data-kind="function" class="blk"><title>_call_md — function, 26 lines</title></rect>
<rect x="274.0" y="153.0" width="74.0" height="26.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.build" data-kind="function" class="blk"><title>build — function, 52 lines</title></rect>
<rect x="274.0" y="180.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.emit" data-kind="function" class="blk"><title>emit — function, 3 lines</title></rect>
<rect x="274.0" y="182.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.write" data-kind="function" class="blk"><title>write — function, 5 lines</title></rect>
<rect x="356.0" y="46.0" width="74.0" height="139.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.minimap — 278 lines</title></rect>
<rect x="356.0" y="77.5" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._files" data-kind="function" class="blk"><title>_files — function, 57 lines</title></rect>
<rect x="363.0" y="81.5" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.declared" data-kind="function" class="blk"><title>declared — function, 18 lines</title></rect>
<rect x="363.0" y="91.0" width="60.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.build" data-kind="function" class="blk"><title>build — function, 11 lines</title></rect>
<rect x="356.0" y="107.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._bands" data-kind="function" class="blk"><title>_bands — function, 7 lines</title></rect>
<rect x="356.0" y="111.5" width="74.0" height="9.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._blocks" data-kind="function" class="blk"><title>_blocks — function, 19 lines</title></rect>
<rect x="356.0" y="122.0" width="74.0" height="60.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.emit" data-kind="function" class="blk"><title>emit — function, 120 lines</title></rect>
<rect x="356.0" y="183.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.marks_to_labels" data-kind="function" class="blk"><title>marks_to_labels — function, 3 lines</title></rect>
<rect x="438.0" y="46.0" width="74.0" height="130.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.erd — 260 lines</title></rect>
<rect x="438.0" y="73.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._truncate" data-kind="function" class="blk"><title>_truncate — function, 3 lines</title></rect>
<rect x="438.0" y="75.5" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._tables" data-kind="function" class="blk"><title>_tables — function, 20 lines</title></rect>
<rect x="438.0" y="86.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._card_height" data-kind="function" class="blk"><title>_card_height — function, 3 lines</title></rect>
<rect x="438.0" y="89.0" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.layout" data-kind="function" class="blk"><title>layout — function, 20 lines</title></rect>
<rect x="438.0" y="100.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._field_y" data-kind="function" class="blk"><title>_field_y — function, 3 lines</title></rect>
<rect x="438.0" y="102.5" width="74.0" height="73.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.emit" data-kind="function" class="blk"><title>emit — function, 146 lines</title></rect>
<rect x="520.0" y="46.0" width="74.0" height="82.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.index — 164 lines</title></rect>
<rect x="520.0" y="63.0" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._tree" data-kind="function" class="blk"><title>_tree — function, 8 lines</title></rect>
<rect x="520.0" y="68.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._anchor" data-kind="function" class="blk"><title>_anchor — function, 5 lines</title></rect>
<rect x="520.0" y="71.5" width="74.0" height="42.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_markdown" data-kind="function" class="blk"><title>to_markdown — function, 84 lines</title></rect>
<rect x="527.0" y="83.0" width="60.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_markdown.walk" data-kind="function" class="blk"><title>walk — function, 27 lines</title></rect>
<rect x="520.0" y="114.5" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_sidebar" data-kind="function" class="blk"><title>to_sidebar — function, 26 lines</title></rect>
<rect x="527.0" y="118.0" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_sidebar.build" data-kind="function" class="blk"><title>build — function, 13 lines</title></rect>
<rect x="602.0" y="46.0" width="74.0" height="43.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.cli_dot — 87 lines</title></rect>
<rect x="602.0" y="52.5" width="74.0" height="36.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.cli_dot.main" data-kind="function" class="blk"><title>main — function, 73 lines</title></rect>
<rect x="684.0" y="46.0" width="74.0" height="40.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.auto — 80 lines</title></rect>
<rect x="684.0" y="57.5" width="74.0" height="28.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.auto.main" data-kind="function" class="blk"><title>main — function, 56 lines</title></rect>
<rect x="766.0" y="46.0" width="74.0" height="37.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.cli_notebook — 74 lines</title></rect>
<rect x="766.0" y="54.0" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.cli_notebook.main" data-kind="function" class="blk"><title>main — function, 57 lines</title></rect>
<rect x="28.0" y="331.0" width="74.0" height="37.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.cli_site — 74 lines</title></rect>
<rect x="28.0" y="339.0" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.cli_site.main" data-kind="function" class="blk"><title>main — function, 57 lines</title></rect>
<rect x="110.0" y="331.0" width="74.0" height="25.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.cli_erd — 51 lines</title></rect>
<rect x="110.0" y="337.0" width="74.0" height="19.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.cli_erd.main" data-kind="function" class="blk"><title>main — function, 38 lines</title></rect>
<rect x="192.0" y="331.0" width="74.0" height="25.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.cli_minimap — 51 lines</title></rect>
<rect x="192.0" y="337.5" width="74.0" height="18.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.cli_minimap.main" data-kind="function" class="blk"><title>main — function, 37 lines</title></rect>
<rect x="274.0" y="331.0" width="74.0" height="24.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.cli_explore — 48 lines</title></rect>
<rect x="274.0" y="337.0" width="74.0" height="17.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.cli_explore.main" data-kind="function" class="blk"><title>main — function, 35 lines</title></rect>
<rect x="356.0" y="331.0" width="74.0" height="22.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.cli_index — 44 lines</title></rect>
<rect x="356.0" y="336.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.cli_index.main" data-kind="function" class="blk"><title>main — function, 32 lines</title></rect>
<rect x="438.0" y="331.0" width="74.0" height="18.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.__main__ — 37 lines</title></rect>
<rect x="438.0" y="333.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.__main__.main" data-kind="function" class="blk"><title>main — function, 27 lines</title></rect>
<rect x="538.0" y="331.0" width="74.0" height="1007.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.selftest — 2014 lines</title></rect>
<rect x="538.0" y="375.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.check" data-kind="function" class="blk"><title>check — function, 5 lines</title></rect>
<rect x="538.0" y="379.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._err" data-kind="function" class="blk"><title>_err — function, 7 lines</title></rect>
<rect x="538.0" y="383.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.skip" data-kind="function" class="blk"><title>skip — function, 3 lines</title></rect>
<rect x="538.0" y="386.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.build_tree" data-kind="function" class="blk"><title>build_tree — function, 5 lines</title></rect>
<rect x="538.0" y="515.5" width="74.0" height="97.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._harvesting" data-kind="function" class="blk"><title>_harvesting — function, 194 lines</title></rect>
<rect x="538.0" y="858.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._entry" data-kind="function" class="blk"><title>_entry — function, 7 lines</title></rect>
<rect x="538.0" y="1111.0" width="74.0" height="139.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._books" data-kind="function" class="blk"><title>_books — function, 279 lines</title></rect>
<rect x="538.0" y="1255.5" width="74.0" height="76.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._standalone" data-kind="function" class="blk"><title>_standalone — function, 153 lines</title></rect>
<rect x="620.0" y="331.0" width="74.0" height="158.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book — 316 lines</title></rect>
<rect x="620.0" y="360.0" width="74.0" height="10.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.Step" data-kind="class" class="blk"><title>Step — class, 20 lines</title></rect>
<rect x="627.0" y="365.0" width="60.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Step.to_dict" data-kind="function" class="blk"><title>to_dict — function, 10 lines</title></rect>
<rect x="620.0" y="381.5" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book._unit_counts" data-kind="function" class="blk"><title>_unit_counts — function, 30 lines</title></rect>
<rect x="620.0" y="397.5" width="74.0" height="89.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.Book" data-kind="class" class="blk"><title>Book — class, 178 lines</title></rect>
<rect x="627.0" y="399.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.__init__" data-kind="function" class="blk"><title>__init__ — function, 17 lines</title></rect>
<rect x="627.0" y="409.0" width="60.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.step" data-kind="function" class="blk"><title>step — function, 14 lines</title></rect>
<rect x="627.0" y="417.5" width="60.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.measure" data-kind="function" class="blk"><title>measure — function, 21 lines</title></rect>
<rect x="627.0" y="428.5" width="60.0" height="31.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.compare" data-kind="function" class="blk"><title>compare — function, 62 lines</title></rect>
<rect x="627.0" y="461.0" width="60.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.close" data-kind="function" class="blk"><title>close — function, 27 lines</title></rect>
<rect x="627.0" y="475.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.to_dict" data-kind="function" class="blk"><title>to_dict — function, 17 lines</title></rect>
<rect x="627.0" y="484.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.write" data-kind="function" class="blk"><title>write — function, 5 lines</title></rect>
<rect x="620.0" y="487.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book._tree_bytes" data-kind="function" class="blk"><title>_tree_bytes — function, 2 lines</title></rect>
<rect x="702.0" y="331.0" width="74.0" height="97.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style — 194 lines</title></rect>
<rect x="702.0" y="353.5" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.style.StyleError" data-kind="class" class="blk"><title>StyleError — class, 2 lines</title></rect>
<rect x="702.0" y="355.5" width="74.0" height="66.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.style.Style" data-kind="class" class="blk"><title>Style — class, 133 lines</title></rect>
<rect x="709.0" y="357.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.__init__" data-kind="function" class="blk"><title>__init__ — function, 17 lines</title></rect>
<rect x="709.0" y="367.5" width="60.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.load" data-kind="function" class="blk"><title>load — function, 12 lines</title></rect>
<rect x="709.0" y="374.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.available" data-kind="function" class="blk"><title>available — function, 2 lines</title></rect>
<rect x="709.0" y="376.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.themes" data-kind="function" class="blk"><title>themes — function, 2 lines</title></rect>
<rect x="709.0" y="378.5" width="60.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.validate" data-kind="function" class="blk"><title>validate — function, 32 lines</title></rect>
<rect x="709.0" y="396.0" width="60.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style._resolve" data-kind="function" class="blk"><title>_resolve — function, 12 lines</title></rect>
<rect x="709.0" y="402.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style._lookup" data-kind="function" class="blk"><title>_lookup — function, 3 lines</title></rect>
<rect x="709.0" y="404.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.node" data-kind="function" class="blk"><title>node — function, 2 lines</title></rect>
<rect x="709.0" y="406.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.group" data-kind="function" class="blk"><title>group — function, 2 lines</title></rect>
<rect x="709.0" y="407.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.edge" data-kind="function" class="blk"><title>edge — function, 2 lines</title></rect>
<rect x="709.0" y="409.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.graph" data-kind="function" class="blk"><title>graph — function, 2 lines</title></rect>
<rect x="709.0" y="410.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.geom" data-kind="function" class="blk"><title>geom — function, 2 lines</title></rect>
<rect x="709.0" y="412.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.slot" data-kind="function" class="blk"><title>slot — function, 2 lines</title></rect>
<rect x="709.0" y="413.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.domain_slot" data-kind="function" class="blk"><title>domain_slot — function, 13 lines</title></rect>
<rect x="709.0" y="420.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.limits" data-kind="function" class="blk"><title>limits — function, 3 lines</title></rect>
<rect x="702.0" y="423.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.harvest" data-kind="function" class="blk"><title>harvest — function, 9 lines</title></rect>
<rect x="784.0" y="331.0" width="74.0" height="54.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.reference — 108 lines</title></rect>
<rect x="784.0" y="354.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference._candidates" data-kind="function" class="blk"><title>_candidates — function, 7 lines</title></rect>
<rect x="784.0" y="358.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.root" data-kind="function" class="blk"><title>root — function, 9 lines</title></rect>
<rect x="784.0" y="364.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.station_tools" data-kind="function" class="blk"><title>station_tools — function, 4 lines</title></rect>
<rect x="784.0" y="367.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.describe" data-kind="function" class="blk"><title>describe — function, 10 lines</title></rect>
<rect x="784.0" y="373.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.on_path" data-kind="function" class="blk"><title>on_path — function, 13 lines</title></rect>
<rect x="784.0" y="380.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.missing" data-kind="function" class="blk"><title>missing — function, 8 lines</title></rect>
<rect x="28.0" y="1376.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops — 28 lines</title></rect>
<rect x="110.0" y="1376.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.notebook — 15 lines</title></rect>
<rect x="192.0" y="1376.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters — 12 lines</title></rect>
<rect x="274.0" y="1376.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.lab — 11 lines</title></rect>
<rect x="356.0" y="1376.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir — 7 lines</title></rect>
<rect x="438.0" y="1376.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors — 2 lines</title></rect>
<rect x="538.0" y="1376.0" width="74.0" height="141.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.code — 282 lines</title></rect>
<rect x="538.0" y="1423.5" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.code.MissingParser" data-kind="class" class="blk"><title>MissingParser — class, 2 lines</title></rect>
<rect x="538.0" y="1425.5" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._parser" data-kind="function" class="blk"><title>_parser — function, 21 lines</title></rect>
<rect x="538.0" y="1437.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._name" data-kind="function" class="blk"><title>_name — function, 10 lines</title></rect>
<rect x="538.0" y="1443.0" width="74.0" height="17.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._walk" data-kind="function" class="blk"><title>_walk — function, 34 lines</title></rect>
<rect x="538.0" y="1461.0" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract_file" data-kind="function" class="blk"><title>extract_file — function, 27 lines</title></rect>
<rect x="538.0" y="1475.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._count_errors" data-kind="function" class="blk"><title>_count_errors — function, 5 lines</title></rect>
<rect x="538.0" y="1479.0" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract" data-kind="function" class="blk"><title>extract — function, 75 lines</title></rect>
<rect x="620.0" y="1376.0" width="74.0" height="134.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.usage — 269 lines</title></rect>
<rect x="620.0" y="1408.0" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._template" data-kind="function" class="blk"><title>_template — function, 27 lines</title></rect>
<rect x="620.0" y="1422.5" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._body" data-kind="function" class="blk"><title>_body — function, 10 lines</title></rect>
<rect x="620.0" y="1428.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._shape" data-kind="function" class="blk"><title>_shape — function, 15 lines</title></rect>
<rect x="620.0" y="1437.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._graphql" data-kind="function" class="blk"><title>_graphql — function, 11 lines</title></rect>
<rect x="620.0" y="1443.5" width="74.0" height="66.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage.extract" data-kind="function" class="blk"><title>extract — function, 133 lines</title></rect>
<rect x="702.0" y="1376.0" width="74.0" height="95.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.openapi — 191 lines</title></rect>
<rect x="702.0" y="1391.5" width="74.0" height="14.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._modelgen" data-kind="function" class="blk"><title>_modelgen — function, 28 lines</title></rect>
<rect x="702.0" y="1406.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._type_name" data-kind="function" class="blk"><title>_type_name — function, 6 lines</title></rect>
<rect x="702.0" y="1410.5" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._refs" data-kind="function" class="blk"><title>_refs — function, 43 lines</title></rect>
<rect x="702.0" y="1433.0" width="74.0" height="38.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi.extract" data-kind="function" class="blk"><title>extract — function, 76 lines</title></rect>
<rect x="784.0" y="1376.0" width="74.0" height="80.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.db — 161 lines</title></rect>
<rect x="784.0" y="1395.5" width="74.0" height="41.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.from_schema_dict" data-kind="function" class="blk"><title>from_schema_dict — function, 82 lines</title></rect>
<rect x="784.0" y="1437.5" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._relation" data-kind="function" class="blk"><title>_relation — function, 10 lines</title></rect>
<rect x="784.0" y="1443.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._plain_type" data-kind="function" class="blk"><title>_plain_type — function, 6 lines</title></rect>
<rect x="784.0" y="1447.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._dedupe" data-kind="function" class="blk"><title>_dedupe — function, 9 lines</title></rect>
<rect x="784.0" y="1453.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.extract" data-kind="function" class="blk"><title>extract — function, 6 lines</title></rect>
<rect x="28.0" y="1555.0" width="74.0" height="20.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.code_main — 40 lines</title></rect>
<rect x="28.0" y="1559.0" width="74.0" height="15.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code_main.main" data-kind="function" class="blk"><title>main — function, 31 lines</title></rect>
<rect x="110.0" y="1555.0" width="74.0" height="18.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python — 37 lines</title></rect>
<rect x="110.0" y="1564.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.extract" data-kind="function" class="blk"><title>extract — function, 14 lines</title></rect>
<rect x="192.0" y="1555.0" width="74.0" height="16.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.usage_main — 33 lines</title></rect>
<rect x="192.0" y="1559.0" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage_main.main" data-kind="function" class="blk"><title>main — function, 24 lines</title></rect>
<rect x="274.0" y="1555.0" width="74.0" height="16.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.db_main — 32 lines</title></rect>
<rect x="274.0" y="1560.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db_main.main" data-kind="function" class="blk"><title>main — function, 21 lines</title></rect>
<rect x="356.0" y="1555.0" width="74.0" height="15.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.openapi_main — 30 lines</title></rect>
<rect x="356.0" y="1559.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi_main.main" data-kind="function" class="blk"><title>main — function, 21 lines</title></rect>
<rect x="438.0" y="1555.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.__main__ — 26 lines</title></rect>
<rect x="438.0" y="1557.7" width="74.0" height="8.6" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.__main__.main" data-kind="function" class="blk"><title>main — function, 16 lines</title></rect>
<rect x="538.0" y="1555.0" width="74.0" height="175.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.build351 lines</title></rect>
<rect x="538.0" y="1592.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.extract" data-kind="function" class="blk"><title>extract — function, 27 lines</title></rect>
<rect x="538.0" y="1607.0" width="74.0" height="34.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.spec_from" data-kind="function" class="blk"><title>spec_from — function, 68 lines</title></rect>
<rect x="538.0" y="1642.0" width="74.0" height="22.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build._load_cell" data-kind="function" class="blk"><title>_load_cell — function, 44 lines</title></rect>
<rect x="538.0" y="1665.0" width="74.0" height="65.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.run" data-kind="function" class="blk"><title>run — function, 130 lines</title></rect>
<rect x="545.0" y="1671.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.build.run.say" data-kind="function" class="blk"><title>say — function, 3 lines</title></rect>
<rect x="620.0" y="1555.0" width="74.0" height="153.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.checks — 306 lines</title></rect>
<rect x="620.0" y="1585.5" width="74.0" height="13.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Loaded" data-kind="class" class="blk"><title>Loaded — class, 26 lines</title></rect>
<rect x="627.0" y="1587.0" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.__init__" data-kind="function" class="blk"><title>__init__ — function, 15 lines</title></rect>
<rect x="627.0" y="1595.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.larder" data-kind="function" class="blk"><title>larder — function, 2 lines</title></rect>
<rect x="627.0" y="1597.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.measure" data-kind="function" class="blk"><title>measure — function, 2 lines</title></rect>
<rect x="620.0" y="1599.5" width="74.0" height="18.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Report" data-kind="class" class="blk"><title>Report — class, 37 lines</title></rect>
<rect x="627.0" y="1601.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.__init__" data-kind="function" class="blk"><title>__init__ — function, 2 lines</title></rect>
<rect x="627.0" y="1602.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.note" data-kind="function" class="blk"><title>note — function, 2 lines</title></rect>
<rect x="627.0" y="1604.0" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.check" data-kind="function" class="blk"><title>check — function, 8 lines</title></rect>
<rect x="627.0" y="1608.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.skip" data-kind="function" class="blk"><title>skip — function, 3 lines</title></rect>
<rect x="627.0" y="1610.5" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.total" data-kind="function" class="blk"><title>total — function, 15 lines</title></rect>
<rect x="620.0" y="1619.0" width="74.0" height="41.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.generated" data-kind="function" class="blk"><title>generated — function, 83 lines</title></rect>
<rect x="620.0" y="1661.5" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.custom" data-kind="function" class="blk"><title>custom — function, 26 lines</title></rect>
<rect x="620.0" y="1675.5" width="74.0" height="16.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks._run_cells" data-kind="function" class="blk"><title>_run_cells — function, 33 lines</title></rect>
<rect x="620.0" y="1693.0" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.main" data-kind="function" class="blk"><title>main — function, 25 lines</title></rect>
<rect x="702.0" y="1555.0" width="74.0" height="98.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.larder — 196 lines</title></rect>
<rect x="702.0" y="1586.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder._count" data-kind="function" class="blk"><title>_count — function, 13 lines</title></rect>
<rect x="702.0" y="1594.0" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.redact" data-kind="function" class="blk"><title>redact — function, 32 lines</title></rect>
<rect x="702.0" y="1611.5" width="74.0" height="38.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.larder.Larder" data-kind="class" class="blk"><title>Larder — class, 77 lines</title></rect>
<rect x="709.0" y="1618.5" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 7 lines</title></rect>
<rect x="709.0" y="1623.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.read" data-kind="function" class="blk"><title>read — function, 3 lines</title></rect>
<rect x="709.0" y="1625.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.fail" data-kind="function" class="blk"><title>fail — function, 3 lines</title></rect>
<rect x="709.0" y="1627.0" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.to_dict" data-kind="function" class="blk"><title>to_dict — function, 13 lines</title></rect>
<rect x="709.0" y="1634.5" width="60.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.from_dict" data-kind="function" class="blk"><title>from_dict — function, 10 lines</title></rect>
<rect x="709.0" y="1640.0" width="60.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.line" data-kind="function" class="blk"><title>line — function, 20 lines</title></rect>
<rect x="702.0" y="1651.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.of" data-kind="function" class="blk"><title>of — function, 3 lines</title></rect>
<rect x="784.0" y="1555.0" width="74.0" height="40.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.__main__ — 81 lines</title></rect>
<rect x="784.0" y="1562.0" width="74.0" height="31.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.__main__.main" data-kind="function" class="blk"><title>main — function, 62 lines</title></rect>
<rect x="28.0" y="1768.5" width="74.0" height="118.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.collect — 236 lines</title></rect>
<rect x="28.0" y="1785.0" width="74.0" height="5.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Definition" data-kind="class" class="blk"><title>Definition — class, 10 lines</title></rect>
<rect x="28.0" y="1791.5" width="74.0" height="6.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Module" data-kind="class" class="blk"><title>Module — class, 12 lines</title></rect>
<rect x="28.0" y="1798.5" width="74.0" height="33.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._Collector" data-kind="class" class="blk"><title>_Collector — class, 67 lines</title></rect>
<rect x="35.0" y="1800.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
<rect x="35.0" y="1803.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector._define" data-kind="function" class="blk"><title>_define — function, 17 lines</title></rect>
<rect x="35.0" y="1812.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ClassDef" data-kind="function" class="blk"><title>visit_ClassDef — function, 5 lines</title></rect>
<rect x="35.0" y="1815.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_FunctionDef" data-kind="function" class="blk"><title>visit_FunctionDef — function, 5 lines</title></rect>
<rect x="35.0" y="1820.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_Import" data-kind="function" class="blk"><title>visit_Import — function, 8 lines</title></rect>
<rect x="35.0" y="1825.0" width="60.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ImportFrom" data-kind="function" class="blk"><title>visit_ImportFrom — function, 14 lines</title></rect>
<rect x="28.0" y="1833.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._first_line" data-kind="function" class="blk"><title>_first_line — function, 5 lines</title></rect>
<rect x="28.0" y="1836.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._name_of" data-kind="function" class="blk"><title>_name_of — function, 15 lines</title></rect>
<rect x="28.0" y="1845.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._resolve_relative" data-kind="function" class="blk"><title>_resolve_relative — function, 16 lines</title></rect>
<rect x="28.0" y="1854.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.module_name" data-kind="function" class="blk"><title>module_name — function, 26 lines</title></rect>
<rect x="28.0" y="1868.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect_file" data-kind="function" class="blk"><title>collect_file — function, 21 lines</title></rect>
<rect x="28.0" y="1879.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect" data-kind="function" class="blk"><title>collect — function, 13 lines</title></rect>
<rect x="110.0" y="1768.5" width="74.0" height="92.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.resolve — 184 lines</title></rect>
<rect x="110.0" y="1782.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._id_for" data-kind="function" class="blk"><title>_id_for — function, 2 lines</title></rect>
<rect x="110.0" y="1784.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._resolve" data-kind="function" class="blk"><title>_resolve — function, 32 lines</title></rect>
<rect x="110.0" y="1801.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.larder_of" data-kind="function" class="blk"><title>larder_of — function, 17 lines</title></rect>
<rect x="110.0" y="1811.0" width="74.0" height="49.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.to_ir" data-kind="function" class="blk"><title>to_ir — function, 98 lines</title></rect>
<rect x="117.0" y="1840.5" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.resolve.to_ir._point_at" data-kind="function" class="blk"><title>_point_at — function, 9 lines</title></rect>
<rect x="192.0" y="1768.5" width="74.0" height="19.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.__main__ — 38 lines</title></rect>
<rect x="192.0" y="1773.5" width="74.0" height="11.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.__main__.main" data-kind="function" class="blk"><title>main — function, 23 lines</title></rect>
<rect x="292.0" y="1768.5" width="74.0" height="156.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.validate — 313 lines</title></rect>
<rect x="292.0" y="1790.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.validate.IRError" data-kind="class" class="blk"><title>IRError — class, 2 lines</title></rect>
<rect x="292.0" y="1792.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._schema" data-kind="function" class="blk"><title>_schema — function, 2 lines</title></rect>
<rect x="292.0" y="1794.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._props" data-kind="function" class="blk"><title>_props — function, 5 lines</title></rect>
<rect x="292.0" y="1797.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._fields" data-kind="function" class="blk"><title>_fields — function, 4 lines</title></rect>
<rect x="292.0" y="1800.5" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check" data-kind="function" class="blk"><title>check — function, 108 lines</title></rect>
<rect x="292.0" y="1862.0" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._check_larder" data-kind="function" class="blk"><title>_check_larder — function, 58 lines</title></rect>
<rect x="292.0" y="1892.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.validate" data-kind="function" class="blk"><title>validate — function, 6 lines</title></rect>
<rect x="292.0" y="1896.0" width="74.0" height="11.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check_model_matches_schema" data-kind="function" class="blk"><title>check_model_matches_schema — function, 23 lines</title></rect>
<rect x="292.0" y="1908.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.main" data-kind="function" class="blk"><title>main — function, 32 lines</title></rect>
<rect x="374.0" y="1768.5" width="74.0" height="82.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.model — 165 lines</title></rect>
<rect x="374.0" y="1785.0" width="74.0" height="19.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Meta" data-kind="class" class="blk"><title>Meta — class, 38 lines</title></rect>
<rect x="381.0" y="1795.0" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Meta.to_dict" data-kind="function" class="blk"><title>to_dict — function, 18 lines</title></rect>
<rect x="374.0" y="1805.5" width="74.0" height="10.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Node" data-kind="class" class="blk"><title>Node — class, 21 lines</title></rect>
<rect x="381.0" y="1810.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 3 lines</title></rect>
<rect x="381.0" y="1812.0" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.to_dict" data-kind="function" class="blk"><title>to_dict — function, 8 lines</title></rect>
<rect x="374.0" y="1817.5" width="74.0" height="7.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Edge" data-kind="class" class="blk"><title>Edge — class, 15 lines</title></rect>
<rect x="381.0" y="1821.5" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Edge.to_dict" data-kind="function" class="blk"><title>to_dict — function, 7 lines</title></rect>
<rect x="374.0" y="1826.5" width="74.0" height="24.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Graph" data-kind="class" class="blk"><title>Graph — class, 48 lines</title></rect>
<rect x="381.0" y="1831.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.node" data-kind="function" class="blk"><title>node — function, 4 lines</title></rect>
<rect x="381.0" y="1833.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.edge" data-kind="function" class="blk"><title>edge — function, 4 lines</title></rect>
<rect x="381.0" y="1836.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.has" data-kind="function" class="blk"><title>has — function, 2 lines</title></rect>
<rect x="381.0" y="1838.5" width="60.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.to_dict" data-kind="function" class="blk"><title>to_dict — function, 16 lines</title></rect>
<rect x="381.0" y="1847.5" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.from_dict" data-kind="function" class="blk"><title>from_dict — function, 6 lines</title></rect>
<rect x="456.0" y="1768.5" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.__main__ — 9 lines</title></rect>
<rect x="556.0" y="1768.5" width="74.0" height="242.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops.filter — 485 lines</title></rect>
<rect x="556.0" y="1786.5" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter._rebuild" data-kind="function" class="blk"><title>_rebuild — function, 57 lines</title></rect>
<rect x="563.0" y="1791.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.surviving_parent" data-kind="function" class="blk"><title>surviving_parent — function, 5 lines</title></rect>
<rect x="563.0" y="1798.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.lift" data-kind="function" class="blk"><title>lift — function, 6 lines</title></rect>
<rect x="556.0" y="1816.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_kinds" data-kind="function" class="blk"><title>drop_kinds — function, 14 lines</title></rect>
<rect x="556.0" y="1824.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.only_kinds" data-kind="function" class="blk"><title>only_kinds — function, 14 lines</title></rect>
<rect x="556.0" y="1832.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_stdlib" data-kind="function" class="blk"><title>drop_stdlib — function, 13 lines</title></rect>
<rect x="556.0" y="1839.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_external" data-kind="function" class="blk"><title>drop_external — function, 3 lines</title></rect>
<rect x="556.0" y="1842.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.subtree" data-kind="function" class="blk"><title>subtree — function, 13 lines</title></rect>
<rect x="556.0" y="1849.5" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.neighbourhood" data-kind="function" class="blk"><title>neighbourhood — function, 45 lines</title></rect>
<rect x="556.0" y="1873.0" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.collapse_to_depth" data-kind="function" class="blk"><title>collapse_to_depth — function, 18 lines</title></rect>
<rect x="563.0" y="1878.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.collapse_to_depth.level" data-kind="function" class="blk"><title>level — function, 6 lines</title></rect>
<rect x="556.0" y="1883.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_builtins" data-kind="function" class="blk"><title>drop_builtins — function, 14 lines</title></rect>
<rect x="556.0" y="1891.0" width="74.0" height="17.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.overview" data-kind="function" class="blk"><title>overview — function, 35 lines</title></rect>
<rect x="556.0" y="1909.5" width="74.0" height="36.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.shape" data-kind="function" class="blk"><title>shape — function, 72 lines</title></rect>
<rect x="563.0" y="1933.5" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.shape.rank_of" data-kind="function" class="blk"><title>rank_of — function, 9 lines</title></rect>
<rect x="556.0" y="1946.5" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.split" data-kind="function" class="blk"><title>split — function, 24 lines</title></rect>
<rect x="556.0" y="1959.5" width="74.0" height="51.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.classify" data-kind="function" class="blk"><title>classify — function, 102 lines</title></rect>
<rect x="638.0" y="1768.5" width="74.0" height="51.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops.__main__ — 102 lines</title></rect>
<rect x="638.0" y="1775.0" width="74.0" height="42.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.__main__.main" data-kind="function" class="blk"><title>main — function, 84 lines</title></rect>
<rect x="738.0" y="1768.5" width="74.0" height="111.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.extract — 222 lines</title></rect>
<rect x="738.0" y="1799.0" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._values_from" data-kind="function" class="blk"><title>_values_from — function, 17 lines</title></rect>
<rect x="738.0" y="1808.5" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._normalise" data-kind="function" class="blk"><title>_normalise — function, 45 lines</title></rect>
<rect x="738.0" y="1832.0" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._svg_files" data-kind="function" class="blk"><title>_svg_files — function, 25 lines</title></rect>
<rect x="738.0" y="1845.5" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.harvest" data-kind="function" class="blk"><title>harvest — function, 43 lines</title></rect>
<rect x="738.0" y="1868.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
<rect x="738.0" y="1872.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.summarise" data-kind="function" class="blk"><title>summarise — function, 13 lines</title></rect>
<rect x="28.0" y="2049.0" width="74.0" height="105.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.tokens — 211 lines</title></rect>
<rect x="28.0" y="2077.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._top" data-kind="function" class="blk"><title>_top — function, 2 lines</title></rect>
<rect x="28.0" y="2079.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._mode" data-kind="function" class="blk"><title>_mode — function, 3 lines</title></rect>
<rect x="28.0" y="2081.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._luminance" data-kind="function" class="blk"><title>_luminance — function, 6 lines</title></rect>
<rect x="28.0" y="2085.5" width="74.0" height="50.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.derive" data-kind="function" class="blk"><title>derive — function, 101 lines</title></rect>
<rect x="35.0" y="2107.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.tokens.derive.accent" data-kind="function" class="blk"><title>accent — function, 2 lines</title></rect>
<rect x="28.0" y="2137.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.geometry" data-kind="function" class="blk"><title>geometry — function, 14 lines</title></rect>
<rect x="28.0" y="2145.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
<rect x="28.0" y="2149.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.from_folder" data-kind="function" class="blk"><title>from_folder — function, 9 lines</title></rect>
<rect x="128.0" y="2049.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen — 2 lines</title></rect>
<rect x="228.0" y="2049.0" width="74.0" height="75.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.lab.pg_probe — 150 lines</title></rect>
<rect x="228.0" y="2084.5" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.probe" data-kind="function" class="blk"><title>probe — function, 30 lines</title></rect>
<rect x="228.0" y="2106.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe._simplify" data-kind="function" class="blk"><title>_simplify — function, 3 lines</title></rect>
<rect x="228.0" y="2109.0" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.main" data-kind="function" class="blk"><title>main — function, 25 lines</title></rect>
<rect x="328.0" y="2049.0" width="74.0" height="132.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.notebook.spec — 264 lines</title></rect>
<rect x="328.0" y="2077.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec._step" data-kind="function" class="blk"><title>_step — function, 4 lines</title></rect>
<rect x="328.0" y="2080.0" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.from_ir" data-kind="function" class="blk"><title>from_ir — function, 108 lines</title></rect>
<rect x="335.0" y="2087.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.notebook.spec.from_ir._order" data-kind="function" class="blk"><title>_order — function, 5 lines</title></rect>
<rect x="328.0" y="2135.0" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.scaffold" data-kind="function" class="blk"><title>scaffold — function, 20 lines</title></rect>
<rect x="328.0" y="2146.0" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.merge" data-kind="function" class="blk"><title>merge — function, 58 lines</title></rect>
<rect x="328.0" y="2176.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.load" data-kind="function" class="blk"><title>load — function, 2 lines</title></rect>
<rect x="328.0" y="2178.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.dump" data-kind="function" class="blk"><title>dump — function, 5 lines</title></rect>
<text x="28" y="40" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.emitters</text>
<text x="28" y="325" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.emitters</text>
<text x="538" y="325" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen</text>
<text x="28" y="1370" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen</text>
<text x="538" y="1370" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors</text>
<text x="28" y="1549" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors</text>
<text x="538" y="1549" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.book</text>
<text x="28" y="1762" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors.python</text>
<text x="292" y="1762" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ir</text>
<text x="556" y="1762" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ops</text>
<text x="738" y="1762" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.style</text>
<text x="28" y="2043" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.style</text>
<text x="128" y="2043" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">(root)</text>
<text x="228" y="2043" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.lab</text>
<text x="328" y="2043" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.notebook</text>
<text x="28" y="302" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">site</text>
<text x="110" y="213" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">explore</text>
<text x="192" y="201" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">dot</text>
<text x="274" y="194" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notebook</text>
<text x="356" y="194" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">minimap</text>
<text x="438" y="185" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">erd</text>
<text x="520" y="137" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">index</text>
<text x="602" y="98" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli_dot</text>
<text x="684" y="95" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">auto</text>
<text x="766" y="92" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli_noteboo</text>
<text x="28" y="377" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli_site</text>
<text x="110" y="366" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli_erd</text>
<text x="192" y="366" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli_minimap</text>
<text x="274" y="364" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli_explore</text>
<text x="356" y="362" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli_index</text>
<text x="438" y="358" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
<text x="538" y="1347" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">selftest</text>
<text x="620" y="498" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">book</text>
<text x="702" y="437" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">style</text>
<text x="784" y="394" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">reference</text>
<text x="28" y="1399" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">ops</text>
<text x="110" y="1399" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notebook</text>
<text x="192" y="1399" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">emitters</text>
<text x="274" y="1399" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">lab</text>
<text x="356" y="1399" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">ir</text>
<text x="438" y="1399" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extractors</text>
<text x="538" y="1526" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">code</text>
<text x="620" y="1520" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">usage</text>
<text x="702" y="1480" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">openapi</text>
<text x="784" y="1466" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">db</text>
<text x="28" y="1584" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">code_main</text>
<text x="110" y="1582" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">python</text>
<text x="192" y="1580" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">usage_main</text>
<text x="274" y="1580" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">db_main</text>
<text x="356" y="1579" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">openapi_mai</text>
<text x="438" y="1578" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
<text x="538" y="1740" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">build</text>
<text x="620" y="1717" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">checks</text>
<text x="702" y="1662" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">larder</text>
<text x="784" y="1604" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
<text x="28" y="1896" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">collect</text>
<text x="110" y="1870" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">resolve</text>
<text x="192" y="1796" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
<text x="292" y="1934" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">validate</text>
<text x="374" y="1860" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">model</text>
<text x="456" y="1792" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
<text x="556" y="2020" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">filter</text>
<text x="638" y="1828" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
<text x="738" y="1888" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extract</text>
<text x="28" y="2164" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">tokens</text>
<text x="128" y="2072" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">docgen</text>
<text x="228" y="2133" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">pg_probe</text>
<text x="328" y="2190" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">spec</text>
<rect x="28" y="2230.0" width="9" height="9" rx="2" fill="#1a1a1a"/>
<text x="41" y="2238.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">module</text>
<rect x="86" y="2230.0" width="9" height="9" rx="2" fill="#1d4ed8"/>
<text x="99" y="2238.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">class</text>
<rect x="138" y="2230.0" width="9" height="9" rx="2" fill="#d4a574"/>
<text x="151" y="2238.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">interface</text>
<rect x="214" y="2230.0" width="9" height="9" rx="2" fill="#15803d"/>
<text x="227" y="2238.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">function</text>
<text x="858" y="2238.0" text-anchor="end" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">53 files · 9,752 lines · 1px ≈ 2.0 lines</text>
<svg xmlns="http://www.w3.org/2000/svg" width="886pt" height="2238pt" viewBox="0 0 886 2238">
<rect width="886" height="2238" fill="#0a0a0a"/>
<rect x="28.0" y="46.0" width="74.0" height="1164.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.selftest — 2328 lines</title></rect>
<rect x="28.0" y="94.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.own_py_files" data-kind="function" class="blk"><title>own_py_files — function, 3 lines</title></rect>
<rect x="28.0" y="96.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.check" data-kind="function" class="blk"><title>check — function, 5 lines</title></rect>
<rect x="28.0" y="99.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._err" data-kind="function" class="blk"><title>_err — function, 7 lines</title></rect>
<rect x="28.0" y="104.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.skip" data-kind="function" class="blk"><title>skip — function, 3 lines</title></rect>
<rect x="28.0" y="106.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.build_tree" data-kind="function" class="blk"><title>build_tree — function, 5 lines</title></rect>
<rect x="28.0" y="236.0" width="74.0" height="97.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._harvesting" data-kind="function" class="blk"><title>_harvesting — function, 194 lines</title></rect>
<rect x="28.0" y="578.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._entry" data-kind="function" class="blk"><title>_entry — function, 7 lines</title></rect>
<rect x="28.0" y="831.5" width="74.0" height="139.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._books" data-kind="function" class="blk"><title>_books — function, 279 lines</title></rect>
<rect x="28.0" y="976.0" width="74.0" height="76.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._standalone" data-kind="function" class="blk"><title>_standalone — function, 153 lines</title></rect>
<rect x="28.0" y="1057.5" width="74.0" height="44.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._layout" data-kind="function" class="blk"><title>_layout — function, 89 lines</title></rect>
<rect x="28.0" y="1106.5" width="74.0" height="97.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._runfiles" data-kind="function" class="blk"><title>_runfiles — function, 195 lines</title></rect>
<rect x="35.0" y="1112.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.selftest._runfiles.write" data-kind="function" class="blk"><title>write — function, 4 lines</title></rect>
<rect x="35.0" y="1115.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.selftest._runfiles.problems" data-kind="function" class="blk"><title>problems — function, 6 lines</title></rect>
<rect x="110.0" y="46.0" width="74.0" height="158.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book — 316 lines</title></rect>
<rect x="110.0" y="75.0" width="74.0" height="10.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.Step" data-kind="class" class="blk"><title>Stepclass, 20 lines</title></rect>
<rect x="117.0" y="80.0" width="60.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Step.to_dict" data-kind="function" class="blk"><title>to_dict — function, 10 lines</title></rect>
<rect x="110.0" y="96.5" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book._unit_counts" data-kind="function" class="blk"><title>_unit_counts — function, 30 lines</title></rect>
<rect x="110.0" y="112.5" width="74.0" height="89.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.Book" data-kind="class" class="blk"><title>Book — class, 178 lines</title></rect>
<rect x="117.0" y="114.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.__init__" data-kind="function" class="blk"><title>__init__ — function, 17 lines</title></rect>
<rect x="117.0" y="124.0" width="60.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.step" data-kind="function" class="blk"><title>step — function, 14 lines</title></rect>
<rect x="117.0" y="132.5" width="60.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.measure" data-kind="function" class="blk"><title>measure — function, 21 lines</title></rect>
<rect x="117.0" y="143.5" width="60.0" height="31.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.compare" data-kind="function" class="blk"><title>compare — function, 62 lines</title></rect>
<rect x="117.0" y="176.0" width="60.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.close" data-kind="function" class="blk"><title>close — function, 27 lines</title></rect>
<rect x="117.0" y="190.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.to_dict" data-kind="function" class="blk"><title>to_dict — function, 17 lines</title></rect>
<rect x="117.0" y="199.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Book.write" data-kind="function" class="blk"><title>write — function, 5 lines</title></rect>
<rect x="110.0" y="202.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book._tree_bytes" data-kind="function" class="blk"><title>_tree_bytes — function, 2 lines</title></rect>
<rect x="192.0" y="46.0" width="74.0" height="97.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style — 194 lines</title></rect>
<rect x="192.0" y="68.5" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.style.StyleError" data-kind="class" class="blk"><title>StyleError — class, 2 lines</title></rect>
<rect x="192.0" y="70.5" width="74.0" height="66.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.style.Style" data-kind="class" class="blk"><title>Style — class, 133 lines</title></rect>
<rect x="199.0" y="72.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.__init__" data-kind="function" class="blk"><title>__init__ — function, 17 lines</title></rect>
<rect x="199.0" y="82.5" width="60.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.load" data-kind="function" class="blk"><title>load — function, 12 lines</title></rect>
<rect x="199.0" y="89.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.available" data-kind="function" class="blk"><title>available — function, 2 lines</title></rect>
<rect x="199.0" y="91.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.themes" data-kind="function" class="blk"><title>themes — function, 2 lines</title></rect>
<rect x="199.0" y="93.5" width="60.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.validate" data-kind="function" class="blk"><title>validate — function, 32 lines</title></rect>
<rect x="199.0" y="111.0" width="60.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style._resolve" data-kind="function" class="blk"><title>_resolve — function, 12 lines</title></rect>
<rect x="199.0" y="117.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style._lookup" data-kind="function" class="blk"><title>_lookup — function, 3 lines</title></rect>
<rect x="199.0" y="119.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.node" data-kind="function" class="blk"><title>node — function, 2 lines</title></rect>
<rect x="199.0" y="121.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.group" data-kind="function" class="blk"><title>group — function, 2 lines</title></rect>
<rect x="199.0" y="122.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.edge" data-kind="function" class="blk"><title>edge — function, 2 lines</title></rect>
<rect x="199.0" y="124.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.graph" data-kind="function" class="blk"><title>graph — function, 2 lines</title></rect>
<rect x="199.0" y="125.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.geom" data-kind="function" class="blk"><title>geom — function, 2 lines</title></rect>
<rect x="199.0" y="127.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.slot" data-kind="function" class="blk"><title>slot — function, 2 lines</title></rect>
<rect x="199.0" y="128.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.domain_slot" data-kind="function" class="blk"><title>domain_slot — function, 13 lines</title></rect>
<rect x="199.0" y="135.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.Style.limits" data-kind="function" class="blk"><title>limits — function, 3 lines</title></rect>
<rect x="192.0" y="138.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.harvest" data-kind="function" class="blk"><title>harvest — function, 9 lines</title></rect>
<rect x="274.0" y="46.0" width="74.0" height="54.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.reference — 108 lines</title></rect>
<rect x="274.0" y="69.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference._candidates" data-kind="function" class="blk"><title>_candidates — function, 7 lines</title></rect>
<rect x="274.0" y="73.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.root" data-kind="function" class="blk"><title>root — function, 9 lines</title></rect>
<rect x="274.0" y="79.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.station_tools" data-kind="function" class="blk"><title>station_tools — function, 4 lines</title></rect>
<rect x="274.0" y="82.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.describe" data-kind="function" class="blk"><title>describe — function, 10 lines</title></rect>
<rect x="274.0" y="88.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.on_path" data-kind="function" class="blk"><title>on_path — function, 13 lines</title></rect>
<rect x="274.0" y="95.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.missing" data-kind="function" class="blk"><title>missing — function, 8 lines</title></rect>
<rect x="356.0" y="46.0" width="74.0" height="38.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli — 76 lines</title></rect>
<rect x="356.0" y="68.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.usage" data-kind="function" class="blk"><title>usage — function, 7 lines</title></rect>
<rect x="356.0" y="73.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.main" data-kind="function" class="blk"><title>main — function, 21 lines</title></rect>
<rect x="438.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops — 28 lines</title></rect>
<rect x="520.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.notebook — 15 lines</title></rect>
<rect x="602.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters — 12 lines</title></rect>
<rect x="684.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.lab — 11 lines</title></rect>
<rect x="766.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.__main__ — 9 lines</title></rect>
<rect x="28.0" y="1248.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir — 7 lines</title></rect>
<rect x="110.0" y="1248.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors — 2 lines</title></rect>
<rect x="210.0" y="1248.0" width="74.0" height="126.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.emit — 253 lines</title></rect>
<rect x="210.0" y="1260.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit._parser" data-kind="function" class="blk"><title>_parser — function, 7 lines</title></rect>
<rect x="210.0" y="1264.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit._size" data-kind="function" class="blk"><title>_size — function, 3 lines</title></rect>
<rect x="210.0" y="1267.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.auto" data-kind="function" class="blk"><title>auto — function, 16 lines</title></rect>
<rect x="210.0" y="1276.0" width="74.0" height="19.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.dot" data-kind="function" class="blk"><title>dot — function, 38 lines</title></rect>
<rect x="210.0" y="1296.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.erd" data-kind="function" class="blk"><title>erd — function, 11 lines</title></rect>
<rect x="210.0" y="1302.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.index" data-kind="function" class="blk"><title>index — function, 14 lines</title></rect>
<rect x="210.0" y="1310.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.minimap" data-kind="function" class="blk"><title>minimap — function, 14 lines</title></rect>
<rect x="210.0" y="1318.5" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.notebook" data-kind="function" class="blk"><title>notebook — function, 41 lines</title></rect>
<rect x="210.0" y="1340.0" width="74.0" height="15.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.site" data-kind="function" class="blk"><title>site — function, 31 lines</title></rect>
<rect x="210.0" y="1356.5" width="74.0" height="11.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.explore" data-kind="function" class="blk"><title>explore — function, 22 lines</title></rect>
<rect x="210.0" y="1370.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.main" data-kind="function" class="blk"><title>main — function, 7 lines</title></rect>
<rect x="292.0" y="1248.0" width="74.0" height="59.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.extract — 119 lines</title></rect>
<rect x="292.0" y="1257.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract._parser" data-kind="function" class="blk"><title>_parser — function, 5 lines</title></rect>
<rect x="292.0" y="1261.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract._counts" data-kind="function" class="blk"><title>_counts — function, 3 lines</title></rect>
<rect x="292.0" y="1263.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.python" data-kind="function" class="blk"><title>python — function, 15 lines</title></rect>
<rect x="292.0" y="1272.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.code" data-kind="function" class="blk"><title>code — function, 14 lines</title></rect>
<rect x="292.0" y="1280.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.db" data-kind="function" class="blk"><title>db — function, 11 lines</title></rect>
<rect x="292.0" y="1286.5" width="74.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.openapi" data-kind="function" class="blk"><title>openapi — function, 12 lines</title></rect>
<rect x="292.0" y="1293.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.usage" data-kind="function" class="blk"><title>usage — function, 15 lines</title></rect>
<rect x="292.0" y="1303.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.main" data-kind="function" class="blk"><title>main — function, 7 lines</title></rect>
<rect x="374.0" y="1248.0" width="74.0" height="45.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.view — 90 lines</title></rect>
<rect x="374.0" y="1255.0" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.view.main" data-kind="function" class="blk"><title>main — function, 75 lines</title></rect>
<rect x="456.0" y="1248.0" width="74.0" height="40.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.common — 80 lines</title></rect>
<rect x="456.0" y="1258.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.cli.common.Abort" data-kind="class" class="blk"><title>Abort — class, 2 lines</title></rect>
<rect x="456.0" y="1260.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.read_json" data-kind="function" class="blk"><title>read_json — function, 5 lines</title></rect>
<rect x="456.0" y="1263.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.read_ir" data-kind="function" class="blk"><title>read_ir — function, 11 lines</title></rect>
<rect x="456.0" y="1270.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.load_style" data-kind="function" class="blk"><title>load_style — function, 7 lines</title></rect>
<rect x="456.0" y="1274.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.style_args" data-kind="function" class="blk"><title>style_args — function, 3 lines</title></rect>
<rect x="456.0" y="1277.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.emit" data-kind="function" class="blk"><title>emit — function, 16 lines</title></rect>
<rect x="456.0" y="1286.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.dump_ir" data-kind="function" class="blk"><title>dump_ir — function, 3 lines</title></rect>
<rect x="538.0" y="1248.0" width="74.0" height="40.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.run — 80 lines</title></rect>
<rect x="538.0" y="1258.0" width="74.0" height="29.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.run.main" data-kind="function" class="blk"><title>main — function, 59 lines</title></rect>
<rect x="620.0" y="1248.0" width="74.0" height="37.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.book — 74 lines</title></rect>
<rect x="620.0" y="1258.5" width="74.0" height="26.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.book.main" data-kind="function" class="blk"><title>main — function, 52 lines</title></rect>
<rect x="702.0" y="1248.0" width="74.0" height="25.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.check — 51 lines</title></rect>
<rect x="702.0" y="1258.0" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.check.main" data-kind="function" class="blk"><title>main — function, 30 lines</title></rect>
<rect x="784.0" y="1248.0" width="74.0" height="16.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.validate — 33 lines</title></rect>
<rect x="784.0" y="1255.0" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.validate.main" data-kind="function" class="blk"><title>main — function, 18 lines</title></rect>
<rect x="28.0" y="1412.5" width="74.0" height="247.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.site — 494 lines</title></rect>
<rect x="28.0" y="1548.5" width="74.0" height="27.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._ledger" data-kind="function" class="blk"><title>_ledger — function, 54 lines</title></rect>
<rect x="28.0" y="1576.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._slots" data-kind="function" class="blk"><title>_slots — function, 13 lines</title></rect>
<rect x="28.0" y="1584.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._fill" data-kind="function" class="blk"><title>_fill — function, 4 lines</title></rect>
<rect x="28.0" y="1587.0" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sidebar" data-kind="function" class="blk"><title>_sidebar — function, 17 lines</title></rect>
<rect x="28.0" y="1596.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sections" data-kind="function" class="blk"><title>_sections — function, 15 lines</title></rect>
<rect x="28.0" y="1605.0" width="74.0" height="48.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.emit" data-kind="function" class="blk"><title>emit — function, 96 lines</title></rect>
<rect x="28.0" y="1654.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.write" data-kind="function" class="blk"><title>write — function, 10 lines</title></rect>
<rect x="110.0" y="1412.5" width="74.0" height="158.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.explore — 316 lines</title></rect>
<rect x="110.0" y="1432.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._is_schema" data-kind="function" class="blk"><title>_is_schema — function, 4 lines</title></rect>
<rect x="110.0" y="1435.0" width="74.0" height="24.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._neighbourhood_svgs" data-kind="function" class="blk"><title>_neighbourhood_svgs — function, 48 lines</title></rect>
<rect x="117.0" y="1441.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
<rect x="117.0" y="1445.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one#L66" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
<rect x="110.0" y="1460.0" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._facts" data-kind="function" class="blk"><title>_facts — function, 27 lines</title></rect>
<rect x="110.0" y="1474.5" width="74.0" height="86.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.emit" data-kind="function" class="blk"><title>emit — function, 172 lines</title></rect>
<rect x="110.0" y="1561.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.write" data-kind="function" class="blk"><title>write — function, 17 lines</title></rect>
<rect x="192.0" y="1412.5" width="74.0" height="146.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.dot — 292 lines</title></rect>
<rect x="192.0" y="1431.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.RenderError" data-kind="class" class="blk"><title>RenderError — class, 2 lines</title></rect>
<rect x="192.0" y="1433.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._esc" data-kind="function" class="blk"><title>_esc — function, 2 lines</title></rect>
<rect x="192.0" y="1435.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._attrs" data-kind="function" class="blk"><title>_attrs — function, 3 lines</title></rect>
<rect x="192.0" y="1437.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._style_words" data-kind="function" class="blk"><title>_style_words — function, 9 lines</title></rect>
<rect x="192.0" y="1443.0" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._node_attrs" data-kind="function" class="blk"><title>_node_attrs — function, 24 lines</title></rect>
<rect x="192.0" y="1456.0" width="74.0" height="52.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.emit" data-kind="function" class="blk"><title>emit — function, 105 lines</title></rect>
<rect x="199.0" y="1469.5" width="60.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot.emit.write" data-kind="function" class="blk"><title>write — function, 29 lines</title></rect>
<rect x="192.0" y="1509.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._within" data-kind="function" class="blk"><title>_within — function, 8 lines</title></rect>
<rect x="192.0" y="1514.5" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._endpoints" data-kind="function" class="blk"><title>_endpoints — function, 41 lines</title></rect>
<rect x="199.0" y="1519.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.walk" data-kind="function" class="blk"><title>walk — function, 4 lines</title></rect>
<rect x="199.0" y="1523.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.collapsed" data-kind="function" class="blk"><title>collapsed — function, 2 lines</title></rect>
<rect x="199.0" y="1525.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.first_leaf" data-kind="function" class="blk"><title>first_leaf — function, 4 lines</title></rect>
<rect x="192.0" y="1536.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._safe" data-kind="function" class="blk"><title>_safe — function, 2 lines</title></rect>
<rect x="192.0" y="1538.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._q" data-kind="function" class="blk"><title>_q — function, 2 lines</title></rect>
<rect x="192.0" y="1541.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.have_graphviz" data-kind="function" class="blk"><title>have_graphviz — function, 2 lines</title></rect>
<rect x="192.0" y="1543.5" width="74.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.render" data-kind="function" class="blk"><title>render — function, 29 lines</title></rect>
<rect x="274.0" y="1412.5" width="74.0" height="139.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.notebook — 279 lines</title></rect>
<rect x="274.0" y="1433.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._cell" data-kind="function" class="blk"><title>_cell — function, 13 lines</title></rect>
<rect x="274.0" y="1441.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._example" data-kind="function" class="blk"><title>_example — function, 26 lines</title></rect>
<rect x="274.0" y="1474.5" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._params_cell" data-kind="function" class="blk"><title>_params_cell — function, 18 lines</title></rect>
<rect x="274.0" y="1484.5" width="74.0" height="20.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_cell" data-kind="function" class="blk"><title>_call_cell — function, 40 lines</title></rect>
<rect x="274.0" y="1505.5" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_md" data-kind="function" class="blk"><title>_call_md — function, 26 lines</title></rect>
<rect x="274.0" y="1519.5" width="74.0" height="26.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.build" data-kind="function" class="blk"><title>build — function, 52 lines</title></rect>
<rect x="274.0" y="1546.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.emit" data-kind="function" class="blk"><title>emit — function, 3 lines</title></rect>
<rect x="274.0" y="1549.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.write" data-kind="function" class="blk"><title>write — function, 5 lines</title></rect>
<rect x="356.0" y="1412.5" width="74.0" height="139.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.minimap — 278 lines</title></rect>
<rect x="356.0" y="1444.0" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._files" data-kind="function" class="blk"><title>_files — function, 57 lines</title></rect>
<rect x="363.0" y="1448.0" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.declared" data-kind="function" class="blk"><title>declared — function, 18 lines</title></rect>
<rect x="363.0" y="1457.5" width="60.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.build" data-kind="function" class="blk"><title>build — function, 11 lines</title></rect>
<rect x="356.0" y="1473.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._bands" data-kind="function" class="blk"><title>_bands — function, 7 lines</title></rect>
<rect x="356.0" y="1478.0" width="74.0" height="9.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._blocks" data-kind="function" class="blk"><title>_blocks — function, 19 lines</title></rect>
<rect x="356.0" y="1488.5" width="74.0" height="60.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.emit" data-kind="function" class="blk"><title>emit — function, 120 lines</title></rect>
<rect x="356.0" y="1549.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.marks_to_labels" data-kind="function" class="blk"><title>marks_to_labels — function, 3 lines</title></rect>
<rect x="438.0" y="1412.5" width="74.0" height="130.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.erd — 260 lines</title></rect>
<rect x="438.0" y="1439.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._truncate" data-kind="function" class="blk"><title>_truncate — function, 3 lines</title></rect>
<rect x="438.0" y="1442.0" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._tables" data-kind="function" class="blk"><title>_tables — function, 20 lines</title></rect>
<rect x="438.0" y="1453.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._card_height" data-kind="function" class="blk"><title>_card_height — function, 3 lines</title></rect>
<rect x="438.0" y="1455.5" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.layout" data-kind="function" class="blk"><title>layout — function, 20 lines</title></rect>
<rect x="438.0" y="1466.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._field_y" data-kind="function" class="blk"><title>_field_y — function, 3 lines</title></rect>
<rect x="438.0" y="1469.0" width="74.0" height="73.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.emit" data-kind="function" class="blk"><title>emit — function, 146 lines</title></rect>
<rect x="520.0" y="1412.5" width="74.0" height="82.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.index — 164 lines</title></rect>
<rect x="520.0" y="1429.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._tree" data-kind="function" class="blk"><title>_tree — function, 8 lines</title></rect>
<rect x="520.0" y="1434.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._anchor" data-kind="function" class="blk"><title>_anchor — function, 5 lines</title></rect>
<rect x="520.0" y="1438.0" width="74.0" height="42.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_markdown" data-kind="function" class="blk"><title>to_markdown — function, 84 lines</title></rect>
<rect x="527.0" y="1449.5" width="60.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_markdown.walk" data-kind="function" class="blk"><title>walk — function, 27 lines</title></rect>
<rect x="520.0" y="1481.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_sidebar" data-kind="function" class="blk"><title>to_sidebar — function, 26 lines</title></rect>
<rect x="527.0" y="1484.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_sidebar.build" data-kind="function" class="blk"><title>build — function, 13 lines</title></rect>
<rect x="602.0" y="1412.5" width="74.0" height="32.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.auto — 64 lines</title></rect>
<rect x="602.0" y="1426.0" width="74.0" height="18.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.auto.draw" data-kind="function" class="blk"><title>draw — function, 36 lines</title></rect>
<rect x="702.0" y="1412.5" width="74.0" height="141.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.code — 282 lines</title></rect>
<rect x="702.0" y="1460.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.code.MissingParser" data-kind="class" class="blk"><title>MissingParser — class, 2 lines</title></rect>
<rect x="702.0" y="1462.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._parser" data-kind="function" class="blk"><title>_parser — function, 21 lines</title></rect>
<rect x="702.0" y="1473.5" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._name" data-kind="function" class="blk"><title>_name — function, 10 lines</title></rect>
<rect x="702.0" y="1479.5" width="74.0" height="17.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._walk" data-kind="function" class="blk"><title>_walk — function, 34 lines</title></rect>
<rect x="702.0" y="1497.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract_file" data-kind="function" class="blk"><title>extract_filefunction, 27 lines</title></rect>
<rect x="702.0" y="1512.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._count_errors" data-kind="function" class="blk"><title>_count_errors — function, 5 lines</title></rect>
<rect x="702.0" y="1515.5" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract" data-kind="function" class="blk"><title>extract — function, 75 lines</title></rect>
<rect x="784.0" y="1412.5" width="74.0" height="134.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.usage — 269 lines</title></rect>
<rect x="784.0" y="1444.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._template" data-kind="function" class="blk"><title>_template — function, 27 lines</title></rect>
<rect x="784.0" y="1459.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._body" data-kind="function" class="blk"><title>_body — function, 10 lines</title></rect>
<rect x="784.0" y="1465.0" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._shape" data-kind="function" class="blk"><title>_shape — function, 15 lines</title></rect>
<rect x="784.0" y="1473.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._graphql" data-kind="function" class="blk"><title>_graphql — function, 11 lines</title></rect>
<rect x="784.0" y="1480.0" width="74.0" height="66.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage.extract" data-kind="function" class="blk"><title>extract — function, 133 lines</title></rect>
<rect x="28.0" y="1697.5" width="74.0" height="95.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.openapi — 191 lines</title></rect>
<rect x="28.0" y="1713.0" width="74.0" height="14.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._modelgen" data-kind="function" class="blk"><title>_modelgen — function, 28 lines</title></rect>
<rect x="28.0" y="1728.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._type_name" data-kind="function" class="blk"><title>_type_name — function, 6 lines</title></rect>
<rect x="28.0" y="1732.0" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._refs" data-kind="function" class="blk"><title>_refs — function, 43 lines</title></rect>
<rect x="28.0" y="1754.5" width="74.0" height="38.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi.extract" data-kind="function" class="blk"><title>extract — function, 76 lines</title></rect>
<rect x="110.0" y="1697.5" width="74.0" height="80.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.db — 161 lines</title></rect>
<rect x="110.0" y="1717.0" width="74.0" height="41.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.from_schema_dict" data-kind="function" class="blk"><title>from_schema_dict — function, 82 lines</title></rect>
<rect x="110.0" y="1759.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._relation" data-kind="function" class="blk"><title>_relation — function, 10 lines</title></rect>
<rect x="110.0" y="1765.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._plain_type" data-kind="function" class="blk"><title>_plain_type — function, 6 lines</title></rect>
<rect x="110.0" y="1769.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._dedupe" data-kind="function" class="blk"><title>_dedupe — function, 9 lines</title></rect>
<rect x="110.0" y="1774.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.extract" data-kind="function" class="blk"><title>extract — function, 6 lines</title></rect>
<rect x="192.0" y="1697.5" width="74.0" height="18.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python — 37 lines</title></rect>
<rect x="192.0" y="1707.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.extract" data-kind="function" class="blk"><title>extract — function, 14 lines</title></rect>
<rect x="292.0" y="1697.5" width="74.0" height="190.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.build — 381 lines</title></rect>
<rect x="292.0" y="1735.0" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.extract" data-kind="function" class="blk"><title>extract — function, 27 lines</title></rect>
<rect x="292.0" y="1749.5" width="74.0" height="34.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.spec_from" data-kind="function" class="blk"><title>spec_from — function, 68 lines</title></rect>
<rect x="292.0" y="1784.5" width="74.0" height="22.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build._load_cell" data-kind="function" class="blk"><title>_load_cell — function, 44 lines</title></rect>
<rect x="292.0" y="1811.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.clear" data-kind="function" class="blk"><title>clear — function, 26 lines</title></rect>
<rect x="292.0" y="1825.0" width="74.0" height="62.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.run" data-kind="function" class="blk"><title>run — function, 125 lines</title></rect>
<rect x="299.0" y="1831.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.build.run.say" data-kind="function" class="blk"><title>say — function, 3 lines</title></rect>
<rect x="374.0" y="1697.5" width="74.0" height="164.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.config — 328 lines</title></rect>
<rect x="374.0" y="1738.0" width="74.0" height="3.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.ConfigError" data-kind="class" class="blk"><title>ConfigError — class, 6 lines</title></rect>
<rect x="381.0" y="1739.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.ConfigError.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
<rect x="374.0" y="1742.5" width="74.0" height="7.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.Entry" data-kind="class" class="blk"><title>Entry — class, 14 lines</title></rect>
<rect x="381.0" y="1748.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.Entry.line" data-kind="function" class="blk"><title>line — function, 2 lines</title></rect>
<rect x="374.0" y="1751.0" width="74.0" height="8.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.RunFile" data-kind="class" class="blk"><title>RunFile — class, 17 lines</title></rect>
<rect x="381.0" y="1753.5" width="60.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.RunFile.select" data-kind="function" class="blk"><title>select — function, 12 lines</title></rect>
<rect x="374.0" y="1760.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._path" data-kind="function" class="blk"><title>_path — function, 6 lines</title></rect>
<rect x="374.0" y="1764.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._within" data-kind="function" class="blk"><title>_within — function, 3 lines</title></rect>
<rect x="374.0" y="1767.0" width="74.0" height="65.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.load" data-kind="function" class="blk"><title>load — function, 130 lines</title></rect>
<rect x="381.0" y="1799.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.load.pick" data-kind="function" class="blk"><title>pick — function, 3 lines</title></rect>
<rect x="374.0" y="1833.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.apply_reference" data-kind="function" class="blk"><title>apply_reference — function, 11 lines</title></rect>
<rect x="374.0" y="1839.5" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.run" data-kind="function" class="blk"><title>run — function, 43 lines</title></rect>
<rect x="456.0" y="1697.5" width="74.0" height="137.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.checks — 275 lines</title></rect>
<rect x="456.0" y="1728.0" width="74.0" height="13.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Loaded" data-kind="class" class="blk"><title>Loaded — class, 26 lines</title></rect>
<rect x="463.0" y="1729.5" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.__init__" data-kind="function" class="blk"><title>__init__ — function, 15 lines</title></rect>
<rect x="463.0" y="1738.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.larder" data-kind="function" class="blk"><title>larder — function, 2 lines</title></rect>
<rect x="463.0" y="1740.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.measure" data-kind="function" class="blk"><title>measure — function, 2 lines</title></rect>
<rect x="456.0" y="1742.0" width="74.0" height="18.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Report" data-kind="class" class="blk"><title>Report — class, 37 lines</title></rect>
<rect x="463.0" y="1743.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.__init__" data-kind="function" class="blk"><title>__init__ — function, 2 lines</title></rect>
<rect x="463.0" y="1745.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.note" data-kind="function" class="blk"><title>note — function, 2 lines</title></rect>
<rect x="463.0" y="1746.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.check" data-kind="function" class="blk"><title>check — function, 8 lines</title></rect>
<rect x="463.0" y="1751.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.skip" data-kind="function" class="blk"><title>skip — function, 3 lines</title></rect>
<rect x="463.0" y="1753.0" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.total" data-kind="function" class="blk"><title>total — function, 15 lines</title></rect>
<rect x="456.0" y="1761.5" width="74.0" height="41.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.generated" data-kind="function" class="blk"><title>generated — function, 83 lines</title></rect>
<rect x="456.0" y="1804.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.custom" data-kind="function" class="blk"><title>custom — function, 26 lines</title></rect>
<rect x="456.0" y="1818.0" width="74.0" height="16.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks._run_cells" data-kind="function" class="blk"><title>_run_cells — function, 33 lines</title></rect>
<rect x="538.0" y="1697.5" width="74.0" height="98.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.larder — 196 lines</title></rect>
<rect x="538.0" y="1729.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder._count" data-kind="function" class="blk"><title>_count — function, 13 lines</title></rect>
<rect x="538.0" y="1736.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.redact" data-kind="function" class="blk"><title>redact — function, 32 lines</title></rect>
<rect x="538.0" y="1754.0" width="74.0" height="38.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.larder.Larder" data-kind="class" class="blk"><title>Larder — class, 77 lines</title></rect>
<rect x="545.0" y="1761.0" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 7 lines</title></rect>
<rect x="545.0" y="1765.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.read" data-kind="function" class="blk"><title>read — function, 3 lines</title></rect>
<rect x="545.0" y="1767.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.fail" data-kind="function" class="blk"><title>fail — function, 3 lines</title></rect>
<rect x="545.0" y="1769.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.to_dict" data-kind="function" class="blk"><title>to_dict — function, 13 lines</title></rect>
<rect x="545.0" y="1777.0" width="60.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.from_dict" data-kind="function" class="blk"><title>from_dict — function, 10 lines</title></rect>
<rect x="545.0" y="1782.5" width="60.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.line" data-kind="function" class="blk"><title>line — function, 20 lines</title></rect>
<rect x="538.0" y="1793.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.of" data-kind="function" class="blk"><title>of — function, 3 lines</title></rect>
<rect x="638.0" y="1697.5" width="74.0" height="118.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.collect — 236 lines</title></rect>
<rect x="638.0" y="1714.0" width="74.0" height="5.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Definition" data-kind="class" class="blk"><title>Definition — class, 10 lines</title></rect>
<rect x="638.0" y="1720.5" width="74.0" height="6.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Module" data-kind="class" class="blk"><title>Module — class, 12 lines</title></rect>
<rect x="638.0" y="1727.5" width="74.0" height="33.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._Collector" data-kind="class" class="blk"><title>_Collector — class, 67 lines</title></rect>
<rect x="645.0" y="1729.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
<rect x="645.0" y="1732.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector._define" data-kind="function" class="blk"><title>_define — function, 17 lines</title></rect>
<rect x="645.0" y="1741.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ClassDef" data-kind="function" class="blk"><title>visit_ClassDef — function, 5 lines</title></rect>
<rect x="645.0" y="1744.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_FunctionDef" data-kind="function" class="blk"><title>visit_FunctionDef — function, 5 lines</title></rect>
<rect x="645.0" y="1749.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_Import" data-kind="function" class="blk"><title>visit_Import — function, 8 lines</title></rect>
<rect x="645.0" y="1754.0" width="60.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ImportFrom" data-kind="function" class="blk"><title>visit_ImportFrom — function, 14 lines</title></rect>
<rect x="638.0" y="1762.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._first_line" data-kind="function" class="blk"><title>_first_line — function, 5 lines</title></rect>
<rect x="638.0" y="1765.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._name_of" data-kind="function" class="blk"><title>_name_of — function, 15 lines</title></rect>
<rect x="638.0" y="1774.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._resolve_relative" data-kind="function" class="blk"><title>_resolve_relative — function, 16 lines</title></rect>
<rect x="638.0" y="1783.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.module_name" data-kind="function" class="blk"><title>module_name — function, 26 lines</title></rect>
<rect x="638.0" y="1797.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect_file" data-kind="function" class="blk"><title>collect_file — function, 21 lines</title></rect>
<rect x="638.0" y="1808.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect" data-kind="function" class="blk"><title>collect — function, 13 lines</title></rect>
<rect x="720.0" y="1697.5" width="74.0" height="92.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.resolve — 184 lines</title></rect>
<rect x="720.0" y="1711.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._id_for" data-kind="function" class="blk"><title>_id_for — function, 2 lines</title></rect>
<rect x="720.0" y="1713.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._resolve" data-kind="function" class="blk"><title>_resolve — function, 32 lines</title></rect>
<rect x="720.0" y="1730.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.larder_of" data-kind="function" class="blk"><title>larder_of — function, 17 lines</title></rect>
<rect x="720.0" y="1740.0" width="74.0" height="49.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.to_ir" data-kind="function" class="blk"><title>to_ir — function, 98 lines</title></rect>
<rect x="727.0" y="1769.5" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.resolve.to_ir._point_at" data-kind="function" class="blk"><title>_point_at — function, 9 lines</title></rect>
<rect x="28.0" y="1926.0" width="74.0" height="139.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.validate — 278 lines</title></rect>
<rect x="28.0" y="1947.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.validate.IRError" data-kind="class" class="blk"><title>IRError — class, 2 lines</title></rect>
<rect x="28.0" y="1949.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._schema" data-kind="function" class="blk"><title>_schema — function, 2 lines</title></rect>
<rect x="28.0" y="1951.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._props" data-kind="function" class="blk"><title>_props — function, 5 lines</title></rect>
<rect x="28.0" y="1954.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._fields" data-kind="function" class="blk"><title>_fields — function, 4 lines</title></rect>
<rect x="28.0" y="1957.5" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check" data-kind="function" class="blk"><title>check — function, 108 lines</title></rect>
<rect x="28.0" y="2019.0" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._check_larder" data-kind="function" class="blk"><title>_check_larder — function, 58 lines</title></rect>
<rect x="28.0" y="2049.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.validate" data-kind="function" class="blk"><title>validate — function, 6 lines</title></rect>
<rect x="28.0" y="2053.0" width="74.0" height="11.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check_model_matches_schema" data-kind="function" class="blk"><title>check_model_matches_schema — function, 23 lines</title></rect>
<rect x="110.0" y="1926.0" width="74.0" height="82.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.model — 165 lines</title></rect>
<rect x="110.0" y="1942.5" width="74.0" height="19.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Meta" data-kind="class" class="blk"><title>Meta — class, 38 lines</title></rect>
<rect x="117.0" y="1952.5" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Meta.to_dict" data-kind="function" class="blk"><title>to_dict — function, 18 lines</title></rect>
<rect x="110.0" y="1963.0" width="74.0" height="10.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Node" data-kind="class" class="blk"><title>Node — class, 21 lines</title></rect>
<rect x="117.0" y="1967.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 3 lines</title></rect>
<rect x="117.0" y="1969.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.to_dict" data-kind="function" class="blk"><title>to_dict — function, 8 lines</title></rect>
<rect x="110.0" y="1975.0" width="74.0" height="7.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Edge" data-kind="class" class="blk"><title>Edge — class, 15 lines</title></rect>
<rect x="117.0" y="1979.0" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Edge.to_dict" data-kind="function" class="blk"><title>to_dict — function, 7 lines</title></rect>
<rect x="110.0" y="1984.0" width="74.0" height="24.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Graph" data-kind="class" class="blk"><title>Graph — class, 48 lines</title></rect>
<rect x="117.0" y="1988.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.node" data-kind="function" class="blk"><title>node — function, 4 lines</title></rect>
<rect x="117.0" y="1991.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.edge" data-kind="function" class="blk"><title>edge — function, 4 lines</title></rect>
<rect x="117.0" y="1993.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.has" data-kind="function" class="blk"><title>has — function, 2 lines</title></rect>
<rect x="117.0" y="1996.0" width="60.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.to_dict" data-kind="function" class="blk"><title>to_dict — function, 16 lines</title></rect>
<rect x="117.0" y="2005.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.from_dict" data-kind="function" class="blk"><title>from_dict — function, 6 lines</title></rect>
<rect x="210.0" y="1926.0" width="74.0" height="111.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.extract — 222 lines</title></rect>
<rect x="210.0" y="1956.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._values_from" data-kind="function" class="blk"><title>_values_from — function, 17 lines</title></rect>
<rect x="210.0" y="1966.0" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._normalise" data-kind="function" class="blk"><title>_normalise — function, 45 lines</title></rect>
<rect x="210.0" y="1989.5" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._svg_files" data-kind="function" class="blk"><title>_svg_files — function, 25 lines</title></rect>
<rect x="210.0" y="2003.0" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.harvest" data-kind="function" class="blk"><title>harvest — function, 43 lines</title></rect>
<rect x="210.0" y="2025.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
<rect x="210.0" y="2030.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.summarise" data-kind="function" class="blk"><title>summarise — function, 13 lines</title></rect>
<rect x="292.0" y="1926.0" width="74.0" height="105.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.tokens — 211 lines</title></rect>
<rect x="292.0" y="1954.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._top" data-kind="function" class="blk"><title>_top — function, 2 lines</title></rect>
<rect x="292.0" y="1956.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._mode" data-kind="function" class="blk"><title>_mode — function, 3 lines</title></rect>
<rect x="292.0" y="1958.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._luminance" data-kind="function" class="blk"><title>_luminance — function, 6 lines</title></rect>
<rect x="292.0" y="1962.5" width="74.0" height="50.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.derive" data-kind="function" class="blk"><title>derive — function, 101 lines</title></rect>
<rect x="299.0" y="1984.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.tokens.derive.accent" data-kind="function" class="blk"><title>accent — function, 2 lines</title></rect>
<rect x="292.0" y="2014.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.geometry" data-kind="function" class="blk"><title>geometry — function, 14 lines</title></rect>
<rect x="292.0" y="2022.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
<rect x="292.0" y="2026.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.from_folder" data-kind="function" class="blk"><title>from_folder — function, 9 lines</title></rect>
<rect x="392.0" y="1926.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen — 2 lines</title></rect>
<rect x="492.0" y="1926.0" width="74.0" height="75.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.lab.pg_probe — 150 lines</title></rect>
<rect x="492.0" y="1961.5" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.probe" data-kind="function" class="blk"><title>probe — function, 30 lines</title></rect>
<rect x="492.0" y="1983.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe._simplify" data-kind="function" class="blk"><title>_simplify — function, 3 lines</title></rect>
<rect x="492.0" y="1986.0" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.main" data-kind="function" class="blk"><title>main — function, 25 lines</title></rect>
<rect x="592.0" y="1926.0" width="74.0" height="132.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.notebook.spec — 264 lines</title></rect>
<rect x="592.0" y="1954.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec._step" data-kind="function" class="blk"><title>_step — function, 4 lines</title></rect>
<rect x="592.0" y="1957.0" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.from_ir" data-kind="function" class="blk"><title>from_ir — function, 108 lines</title></rect>
<rect x="599.0" y="1964.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.notebook.spec.from_ir._order" data-kind="function" class="blk"><title>_order — function, 5 lines</title></rect>
<rect x="592.0" y="2012.0" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.scaffold" data-kind="function" class="blk"><title>scaffold — function, 20 lines</title></rect>
<rect x="592.0" y="2023.0" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.merge" data-kind="function" class="blk"><title>merge — function, 58 lines</title></rect>
<rect x="592.0" y="2053.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.load" data-kind="function" class="blk"><title>load — function, 2 lines</title></rect>
<rect x="592.0" y="2055.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.dump" data-kind="function" class="blk"><title>dump — function, 5 lines</title></rect>
<rect x="692.0" y="1926.0" width="74.0" height="242.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops.filter — 485 lines</title></rect>
<rect x="692.0" y="1944.0" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter._rebuild" data-kind="function" class="blk"><title>_rebuild — function, 57 lines</title></rect>
<rect x="699.0" y="1948.5" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.surviving_parent" data-kind="function" class="blk"><title>surviving_parent — function, 5 lines</title></rect>
<rect x="699.0" y="1955.5" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.lift" data-kind="function" class="blk"><title>lift — function, 6 lines</title></rect>
<rect x="692.0" y="1973.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_kinds" data-kind="function" class="blk"><title>drop_kinds — function, 14 lines</title></rect>
<rect x="692.0" y="1981.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.only_kinds" data-kind="function" class="blk"><title>only_kinds — function, 14 lines</title></rect>
<rect x="692.0" y="1989.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_stdlib" data-kind="function" class="blk"><title>drop_stdlib — function, 13 lines</title></rect>
<rect x="692.0" y="1997.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_external" data-kind="function" class="blk"><title>drop_external — function, 3 lines</title></rect>
<rect x="692.0" y="1999.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.subtree" data-kind="function" class="blk"><title>subtree — function, 13 lines</title></rect>
<rect x="692.0" y="2007.0" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.neighbourhood" data-kind="function" class="blk"><title>neighbourhood — function, 45 lines</title></rect>
<rect x="692.0" y="2030.5" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.collapse_to_depth" data-kind="function" class="blk"><title>collapse_to_depth — function, 18 lines</title></rect>
<rect x="699.0" y="2035.5" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.collapse_to_depth.level" data-kind="function" class="blk"><title>level — function, 6 lines</title></rect>
<rect x="692.0" y="2040.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_builtins" data-kind="function" class="blk"><title>drop_builtins — function, 14 lines</title></rect>
<rect x="692.0" y="2048.5" width="74.0" height="17.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.overview" data-kind="function" class="blk"><title>overview — function, 35 lines</title></rect>
<rect x="692.0" y="2067.0" width="74.0" height="36.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.shape" data-kind="function" class="blk"><title>shape — function, 72 lines</title></rect>
<rect x="699.0" y="2091.0" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.shape.rank_of" data-kind="function" class="blk"><title>rank_of — function, 9 lines</title></rect>
<rect x="692.0" y="2104.0" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.split" data-kind="function" class="blk"><title>split — function, 24 lines</title></rect>
<rect x="692.0" y="2117.0" width="74.0" height="51.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.classify" data-kind="function" class="blk"><title>classify — function, 102 lines</title></rect>
<text x="28" y="40" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen</text>
<text x="28" y="1242" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen</text>
<text x="210" y="1242" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.cli</text>
<text x="28" y="1406" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.emitters</text>
<text x="702" y="1406" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors</text>
<text x="28" y="1692" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors</text>
<text x="292" y="1692" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.book</text>
<text x="638" y="1692" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors.python</text>
<text x="28" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ir</text>
<text x="210" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.style</text>
<text x="392" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">(root)</text>
<text x="492" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.lab</text>
<text x="592" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.notebook</text>
<text x="692" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ops</text>
<text x="28" y="1219" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">selftest</text>
<text x="110" y="213" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">book</text>
<text x="192" y="152" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">style</text>
<text x="274" y="109" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">reference</text>
<text x="356" y="93" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli</text>
<text x="438" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">ops</text>
<text x="520" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notebook</text>
<text x="602" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">emitters</text>
<text x="684" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">lab</text>
<text x="766" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
<text x="28" y="1271" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">ir</text>
<text x="110" y="1271" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extractors</text>
<text x="210" y="1384" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">emit</text>
<text x="292" y="1316" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extract</text>
<text x="374" y="1302" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">view</text>
<text x="456" y="1297" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">common</text>
<text x="538" y="1297" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">run</text>
<text x="620" y="1294" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">book</text>
<text x="702" y="1282" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">check</text>
<text x="784" y="1274" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">validate</text>
<text x="28" y="1668" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">site</text>
<text x="110" y="1580" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">explore</text>
<text x="192" y="1568" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">dot</text>
<text x="274" y="1561" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notebook</text>
<text x="356" y="1560" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">minimap</text>
<text x="438" y="1552" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">erd</text>
<text x="520" y="1504" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">index</text>
<text x="602" y="1454" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">auto</text>
<text x="702" y="1562" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">code</text>
<text x="784" y="1556" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">usage</text>
<text x="28" y="1802" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">openapi</text>
<text x="110" y="1787" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">db</text>
<text x="192" y="1725" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">python</text>
<text x="292" y="1897" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">build</text>
<text x="374" y="1870" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">config</text>
<text x="456" y="1844" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">checks</text>
<text x="538" y="1804" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">larder</text>
<text x="638" y="1824" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">collect</text>
<text x="720" y="1798" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">resolve</text>
<text x="28" y="2074" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">validate</text>
<text x="110" y="2018" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">model</text>
<text x="210" y="2046" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extract</text>
<text x="292" y="2040" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">tokens</text>
<text x="392" y="1949" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">docgen</text>
<text x="492" y="2010" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">pg_probe</text>
<text x="592" y="2067" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">spec</text>
<text x="692" y="2178" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">filter</text>
<rect x="28" y="2217.5" width="9" height="9" rx="2" fill="#1a1a1a"/>
<text x="41" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">module</text>
<rect x="86" y="2217.5" width="9" height="9" rx="2" fill="#1d4ed8"/>
<text x="99" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">class</text>
<rect x="138" y="2217.5" width="9" height="9" rx="2" fill="#d4a574"/>
<text x="151" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">interface</text>
<rect x="214" y="2217.5" width="9" height="9" rx="2" fill="#15803d"/>
<text x="227" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">function</text>
<text x="858" y="2225.5" text-anchor="end" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">47 files · 10,350 lines · 1px ≈ 2.0 lines</text>
</svg>

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -41,6 +41,7 @@
<div class="group">Reference</div>
<a href="#standalone">Standalone</a>
<a href="#runs">Run files</a>
<a href="#commands">Commands</a>
<a href="#deps">Dependencies</a>
<a href="#testing">Testing</a>
@@ -117,6 +118,11 @@
<pre><code>make book SRC=/path/to/repo BOOK=out/book/mine
make check BOOK=out/book/mine</code></pre>
<p>
More than one, rebuilt together, is a <a href="#runs">run file</a>:
<code>make run CONFIG=docgen.toml</code>.
</p>
<pre><code> larder /path/to/repo — 45 files read, 2 failed, 12 packages
book 312 nodes · 244 edges · 26 external · 8 artifacts
ok 45 file(s) read produced 45 module(s)
@@ -128,13 +134,13 @@ make check BOOK=out/book/mine</code></pre>
</p>
<pre><code><span class="c"># 1. read something</span>
python3 -m docgen.extractors.python --root ../station/tools/histgen -o ir.json
python3 -m docgen extract python --root ../station/tools/histgen -o ir.json
<span class="c"># 2. narrow it to a useful view</span>
python3 -m docgen.ops ir.json --overview -o view.json
python3 -m docgen view ir.json --overview -o view.json
<span class="c"># 3. draw whatever its structure asks for</span>
python3 -m docgen.emitters auto view.json -o out/</code></pre>
python3 -m docgen emit auto view.json -o out/</code></pre>
<p>Or through the Makefile, which is a thin wrapper over exactly those:</p>
@@ -281,7 +287,7 @@ make self <span class="c"># docgen's book of soleprint
or the picture is smaller than the source and says nothing about it.
</p>
<p>
<code>python3 -m docgen.book</code> exits 1 when a reconciliation fails. The
<code>python3 -m docgen book</code> exits 1 when a reconciliation fails. The
book is still written — the evidence is the point — but a build that lost input
should fail a pipeline rather than pass quietly.
</p>
@@ -383,7 +389,7 @@ make check BOOK=out/book/station</code></pre>
and it reads its field lists out of <code>schema.json</code> so the schema and
the dataclasses cannot drift apart.
</p>
<pre><code>python3 -m docgen.ir ir.json</code></pre>
<pre><code>python3 -m docgen validate ir.json</code></pre>
<p>
It catches what a schema cannot: an edge naming a node that does not exist, a
containment cycle, a duplicate id, and a visual field smuggled into
@@ -416,7 +422,7 @@ make check BOOK=out/book/station</code></pre>
<tr><td><code>flat</code></td><td>the index</td><td>most nodes have no relationships: that is a list</td></tr>
</table>
<pre><code>$ python3 -m docgen.emitters auto view.json -o out/
<pre><code>$ python3 -m docgen emit auto view.json -o out/
sheet -&gt; index
109 nodes sit at one level; any layered engine draws that as a
strip. Split it, scope it, or read it as an index</code></pre>
@@ -537,10 +543,10 @@ make check BOOK=out/book/station</code></pre>
the same narrowing.
</p>
<pre><code>python3 -m docgen.ops ir.json --overview -o view.json
python3 -m docgen.ops ir.json --around docgen.ir --hops 2 -o view.json
python3 -m docgen.ops ir.json --split -o parts/
python3 -m docgen.ops ir.json --shape <span class="c"># what will this look like?</span></code></pre>
<pre><code>python3 -m docgen view ir.json --overview -o view.json
python3 -m docgen view ir.json --around docgen.ir --hops 2 -o view.json
python3 -m docgen view ir.json --split -o parts/
python3 -m docgen view ir.json --shape <span class="c"># what will this look like?</span></code></pre>
<p>
All of them are IR&rarr;IR, all composable, and each produces a document that
@@ -738,8 +744,8 @@ python3 -m docgen.ops ir.json --shape <span class="c"># what will this
<b>absent</b> — it is an addition, never a dependency.
</div>
<pre><code>python3 -m docgen.emitters notebook ir.json --scaffold overlay.json
python3 -m docgen.emitters notebook ir.json --overlay overlay.json -o walkthrough.ipynb</code></pre>
<pre><code>python3 -m docgen emit notebook ir.json --scaffold overlay.json
python3 -m docgen emit notebook ir.json --overlay overlay.json -o walkthrough.ipynb</code></pre>
<!-- ─────────────────────────────────────────────────────────────── -->
<h2 id="style">Style &amp; colour</h2>
@@ -802,6 +808,7 @@ python3 -m docgen.emitters notebook ir.json --overlay overlay.json -o walkthroug
<pre><code>cp -r docgen /somewhere/else
cd /somewhere/else/docgen
make doctor <span class="c"># what this machine has</span>
make sync <span class="c"># optional: every group, from uv.lock</span>
make check <span class="c"># the suite, from the copy</span>
make book SRC=/path/to/any/repo BOOK=out/book/theirs</code></pre>
@@ -865,19 +872,116 @@ make doctor
<table>
<tr><th>context</th><th>checks</th><th>skipped</th></tr>
<tr><td>in the repo</td><td>250</td><td>tree-sitter (259 with it)</td></tr>
<tr><td>copied out</td><td>242</td><td>tree-sitter, OpenAPI, the in-place case</td></tr>
<tr><td>copied out, <code>DOCGEN_REFERENCE</code> set</td><td>249</td>
<td>tree-sitter, the in-place case</td></tr>
<tr><td>in the repo, synced</td><td>287</td><td></td></tr>
<tr><td>copied out, bare <code>python3</code></td><td>270</td><td>tree-sitter, OpenAPI, the in-place case</td></tr>
<tr><td>copied out, synced</td><td>279</td><td>OpenAPI, the in-place case</td></tr>
<tr><td>copied out, synced, <code>DOCGEN_REFERENCE</code> set</td><td>286</td>
<td>the in-place case — correctly, there is no repo above</td></tr>
</table>
<!-- ─────────────────────────────────────────────────────────────── -->
<h2 id="runs">Run files</h2>
<p>
A book gets rebuilt many times — after a merge, before a release, whenever the
source moves — and each one is a source, an output directory and a handful of
options. Typed out every time, those drift: one run excludes
<code>migrations</code>, the next forgets. A <b>run file</b> writes them down
once, so the rebuild is exact.
</p>
<pre><code><span class="c"># docgen.toml — kept beside the project it describes</span>
reference = "../soleprint" <span class="c"># optional; $DOCGEN_REFERENCE wins</span>
[defaults]
out = "out/book" <span class="c"># each book goes to &lt;out&gt;/&lt;name&gt;</span>
theme = "dark"
exclude = ["migrations", "tests"]
[[book]]
name = "station"
root = "../soleprint/station"
[[book]]
name = "frontend"
root = "../web/src"
reader = "code" <span class="c"># C# / TypeScript, through tree-sitter</span>
[[book]]
name = "orders-api"
openapi = "specs/orders.yaml"
overlay = "overlays/orders.json"
out = "/srv/docs/orders"</code></pre>
<pre><code>make run CONFIG=docgen.toml
make run CONFIG=docgen.toml ONLY=station,frontend CHECK=1
python3 -m docgen run docgen.toml --list <span class="c"># resolved paths, nothing built</span></code></pre>
<pre><code>docgen.toml — 3 book(s)
ok station /home/me/proj/out/book/station
ok frontend /home/me/proj/out/book/frontend
FAIL orders-api ImportError: modelgen is not reachable — …
1 of 3 book(s) did not hold — --verbose for each book's own output</code></pre>
<p>
Each book names <b>exactly one</b> source — <code>root</code>,
<code>schema</code>, <code>openapi</code> or <code>har</code> — the same choice
the <code>book</code> command makes you take. One failing book never stops the
rest, and the run exits 1 if any book failed to build, lost input between its
two ends, or failed its checks. <code>docgen.example.toml</code> ships with
docgen and runs against its own fixtures.
</p>
<h3>The rules, each one a thing that goes wrong otherwise</h3>
<ul>
<li><b>Paths are relative to the run file</b>, not to wherever the command ran.
Otherwise the same file builds different books from different
directories.</li>
<li><b>A book's value replaces the default, for every key</b> — including
<code>exclude</code>, where merging looks tempting. One rule nobody has to
remember beats a clever one somebody has to look up. A default
<code>exclude</code> simply does not apply to a schema or a spec.</li>
<li><b>Unknown keys are refused, not ignored</b>, all of them at once.
<code>exlude = [...]</code> silently doing nothing is how a rebuild quietly
starts reading <code>node_modules</code>.</li>
<li><b>A book may not be written inside the tree it reads</b> — the next run
would read the last run's output — and <b>two books may not share an output
directory</b>, where the second would clear the first every time.</li>
<li><b>The environment beats the file.</b> A <code>reference</code> in the run
file is used only when <code>$DOCGEN_REFERENCE</code> is not set, which is
rig's precedence for every setting it has.</li>
</ul>
<div class="note">
<b>A rebuild replaces the previous build, and nothing else.</b> Before writing,
a book removes exactly what a build writes — <code>book.json</code>,
<code>notebook.ipynb</code>, <code>steps/</code>, <code>site/</code>,
<code>explore/</code> — and only from a directory that already holds a
<code>book.json</code>. Without it a graph that is now <code>graph.md</code> kept
its old <code>graph.svg</code>, unlisted in the ledger and still on the page.
A hand-written <code>checks.py</code> or <code>overlay.json</code> is never
touched, and neither is a directory docgen did not write.
</div>
<p>
TOML because a run file is written by people: <code>tomllib</code> is the
stdlib, it takes comments — the reason a book excludes something belongs next
to the exclusion — and it is already the format of the
<code>pyproject.toml</code> beside it. The IR and the style files stay JSON,
because those are data that tools write.
</p>
<!-- ─────────────────────────────────────────────────────────────── -->
<h2 id="commands">Commands</h2>
<h3>Make</h3>
<table>
<tr><th>target</th><th>does</th></tr>
<tr><td><code>make sync</code></td><td><code>.venv</code> with every optional group, from <code>uv.lock</code></td></tr>
<tr><td><code>make book SRC=…</code></td><td><b>one whole operation</b>, measured at both ends</td></tr>
<tr><td><code>make run CONFIG=…</code></td><td>every book a <a href="#runs">run file</a> lists</td></tr>
<tr><td><code>make check</code></td><td>docgen's own suite, offline, nothing installed</td></tr>
<tr><td><code>make check BOOK=…</code></td><td>one book's own level — generated and custom</td></tr>
<tr><td><code>make doctor</code></td><td>what this machine has and what it is missing</td></tr>
@@ -896,8 +1000,10 @@ make doctor
<p>
Variables: <code>SRC</code>, <code>OUT</code>, <code>SCHEMA</code>,
<code>OPENAPI</code>, <code>HAR</code>, <code>BOOK</code>, <code>SLUG</code>,
<code>READER</code>, <code>OVERLAY</code>, <code>STYLE</code>,
<code>THEME</code>, <code>SCALE</code>, <code>DEPTH</code>, <code>PY</code>.
<code>READER</code>, <code>OVERLAY</code>, <code>CONFIG</code>,
<code>ONLY</code>, <code>CHECK</code>, <code>STYLE</code>, <code>THEME</code>,
<code>SCALE</code>, <code>PY</code>. <code>PY</code> is the synced
<code>.venv</code> when there is one and <code>python3</code> when there is not.
The Makefile derives its own package name from where it sits, so the folder can
be copied anywhere and renamed and still work.
</p>
@@ -909,49 +1015,100 @@ make doctor
property the spine exists to preserve, not to replace.
</p>
<h3>Modules</h3>
<pre><code>python3 -m docgen.book --root SRC -o out/book/slug <span class="c"># the whole operation</span>
python3 -m docgen.book.checks out/book/slug <span class="c"># that book's level</span>
<h3>The command line</h3>
<p>
One entry point, <code>python3 -m docgen &lt;command&gt;</code>, run from the
directory <em>above</em> the folder (or with it on <code>PYTHONPATH</code>, which
is what the Makefile does). Each command is plain argparse and takes
<code>--help</code>.
</p>
python3 -m docgen.extractors.python --root SRC -o ir.json
python3 -m docgen.extractors code --root SRC -o ir.json
python3 -m docgen.extractors db --schema schema.json -o ir.json
python3 -m docgen.extractors openapi --spec spec.yaml -o ir.json
python3 -m docgen.extractors usage --har session.har -o ir.json
<pre><code>python3 -m docgen book --root SRC -o out/book/slug <span class="c"># the whole operation</span>
python3 -m docgen run docgen.toml <span class="c"># every book a run file lists</span>
python3 -m docgen check <span class="c"># docgen's own suite</span>
python3 -m docgen check out/book/slug <span class="c"># that book's level</span>
python3 -m docgen.ir ir.json <span class="c"># validate</span>
python3 -m docgen.ops ir.json --overview -o view.json
python3 -m docgen extract python --root SRC -o ir.json
python3 -m docgen extract code --root SRC -o ir.json
python3 -m docgen extract db --schema schema.json -o ir.json
python3 -m docgen extract openapi --spec spec.yaml -o ir.json
python3 -m docgen extract usage --har session.har -o ir.json
python3 -m docgen.emitters auto view.json -o out/
python3 -m docgen.emitters index ir.json -o index.md
python3 -m docgen.emitters dot view.json -o graph.svg --theme lucid
python3 -m docgen.emitters erd ir.json -o schema.svg
python3 -m docgen.emitters minimap ir.json -o map.svg --scale 0.5
python3 -m docgen.emitters notebook ir.json -o book.ipynb --overlay overlay.json
python3 -m docgen.emitters site view.json -o site/
python3 -m docgen.emitters explore ir.json -o explore/</code></pre>
python3 -m docgen validate ir.json
python3 -m docgen view ir.json --overview -o view.json
python3 -m docgen emit auto view.json -o out/
python3 -m docgen emit index ir.json -o index.md
python3 -m docgen emit dot view.json -o graph.svg --theme lucid
python3 -m docgen emit erd ir.json -o schema.svg
python3 -m docgen emit minimap ir.json -o map.svg --scale 0.5
python3 -m docgen emit notebook ir.json -o book.ipynb --overlay overlay.json
python3 -m docgen emit site view.json -o site/
python3 -m docgen emit explore ir.json -o explore/</code></pre>
<h3>Where the command line lives</h3>
<p>
All of it in <code>cli/</code>, and nowhere else. <code>extractors/</code>,
<code>ops/</code>, <code>emitters/</code> and <code>book/</code> hold library
code only — no argparse, no <code>__main__.py</code>, no
<code>cli_dot.py</code> sitting next to <code>dot.py</code>. Two kinds of file
in one folder make every folder answer two questions, and the command-line half
is the half that grows copies: eight <code>cli_*</code> files each
re-implemented "read the IR, validate it, load the style" by hand, and had begun
to word their errors differently.
</p>
<pre><code>cli/
├── __init__.py <span class="c">the command table and the one exit path</span>
├── common.py <span class="c">read and validate an IR, load a style, write or print</span>
├── book.py run.py check.py
└── extract.py validate.py view.py emit.py</code></pre>
<p>
A command reports an expected failure by raising, and <code>cli.main</code>
turns it into the only exit path: one <code>Error:</code> line on stderr, exit 1,
never a traceback. Doing that split also surfaced a third copy of "which drawing
does this graph want" — in the <code>auto</code> command, the <code>site</code>
command and the book — which is now <code>emitters.auto.draw()</code>, returning
content and leaving the file name to the caller.
</p>
<!-- ─────────────────────────────────────────────────────────────── -->
<h2 id="deps">Dependencies</h2>
<p>
Everything below is optional. The stdlib covers the whole structural path — see <a href="#standalone">standalone</a>
for the one seam out of the folder.
The core is <strong>standard library only</strong>, Python 3.11 or later —
<code>pyproject.toml</code> declares no required dependency at all. Everything
else is an optional <b>dependency group</b>, pinned in a committed
<code>uv.lock</code>, and reported by <code>make doctor</code>; when something is
missing you lose exactly one capability and get told what to install. See
<a href="#standalone">standalone</a> for the one seam out of the folder.
</p>
<pre><code>make sync <span class="c"># uv sync --all-groups → .venv/</span>
uv sync --group code <span class="c"># or just the one you need</span>
make lock <span class="c"># after editing pyproject.toml</span></code></pre>
<p>
The core is <strong>standard library only</strong>. Everything else is optional
and reported by <code>make doctor</code>; when something is missing you lose
exactly one capability and get told what to install.
<code>[tool.uv] package = false</code>, the same choice
<code>station/tools/dataconvert</code> makes: this is a folder of modules run in
place, not something to install. The lock pins what the optional groups resolve
to; the folder still runs on a bare <code>python3</code> without it.
</p>
<table>
<tr><th>needs</th><th>for</th><th>without it</th></tr>
<tr><td><code>graphviz</code> (binary)</td><td>rendering DOT to SVG</td>
<td>ERD, minimap, index and notebooks still work</td></tr>
<tr><td><code>tree_sitter</code> + grammars</td><td>C#, TypeScript, TSX</td>
<tr><td>group <code>code</code> — tree-sitter + grammars</td><td>C#, TypeScript, TSX</td>
<td>Python only</td></tr>
<tr><td><code>networkx</code></td><td><code>lab/</code> experiments</td>
<tr><td>group <code>openapi</code> — pyyaml</td><td>reading OpenAPI (with the reference repo)</td>
<td>the other four sources</td></tr>
<tr><td>group <code>harvest</code> — lxml</td><td>a theme from exported diagrams</td>
<td>the shipped themes</td></tr>
<tr><td>group <code>lab</code> — networkx</td><td><code>lab/</code> experiments</td>
<td>nothing — nothing depends on it yet</td></tr>
<tr><td><code>node</code></td><td>testing the browser pages</td>
<td>those checks skip</td></tr>
@@ -979,16 +1136,19 @@ python3 -m docgen.emitters explore ir.json -o explore/</code></pre>
<tr><th>command</th><th>asks about</th><th>fails?</th></tr>
<tr><td><code>make doctor</code></td><td>the machine — what is installed</td>
<td>never; it reports</td></tr>
<tr><td><code>make check</code></td><td>docgen — 259 checks</td>
<tr><td><code>make check</code></td><td>docgen — 287 checks</td>
<td>exit 1</td></tr>
<tr><td><code>make check BOOK=&lt;dir&gt;</code></td><td>that one book</td>
<td>exit 1</td></tr>
</table>
<p>
All 259 with both optional dependencies installed, 244 with neither — the suite
skips rather than fails when tree-sitter, lxml or the OpenAPI reader is absent.
See <a href="#standalone">standalone</a> for the counts outside the repo.
All 287 after <code>make sync</code>, 272 with nothing optional installed — the
suite skips rather than fails when tree-sitter, lxml or the OpenAPI reader is
absent. See <a href="#standalone">standalone</a> for the counts outside the repo.
Among them, two that keep this page's structure true: nothing outside
<code>cli/</code> imports argparse, and every import the suite allows is a group
in <code>pyproject.toml</code> and pinned in <code>uv.lock</code>.
</p>
<h3>The book level, where custom checks live</h3>

View File

@@ -1,36 +0,0 @@
""" python3 -m docgen.emitters <emitter> <ir.json> [options]"""
import sys
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if not argv:
print("usage: python3 -m docgen.emitters <auto|dot|index|erd|notebook|site|minimap|explore> <ir.json> [-o OUT] [--style NAME] [--theme NAME]",
file=sys.stderr)
return 2
name, rest = argv[0], argv[1:]
if name == "dot":
from .cli_dot import main as run
elif name == "index":
from .cli_index import main as run
elif name == "erd":
from .cli_erd import main as run
elif name == "auto":
from .auto import main as run
elif name == "notebook":
from .cli_notebook import main as run
elif name == "site":
from .cli_site import main as run
elif name == "minimap":
from .cli_minimap import main as run
elif name == "explore":
from .cli_explore import main as run
else:
print(f"Error: no emitter {name!r} — have: auto, dot, index, erd, notebook, site, minimap, explore", file=sys.stderr)
return 1
return run(rest)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,79 +1,63 @@
"""
Draw it the way its structure asks to be drawn.
python3 -m docgen.emitters auto ir.json -o out/
`ops.classify` reads the structure and names an emitter; this runs it. The whole
point is that nobody should have to know that a schema wants cards and a module
graph wants ranks — or discover it from a 235:1 image.
When the answer is "this is not a diagram", it says so and writes the index,
When the answer is "this is not a diagram", it says so and produces the index,
because that *is* the right artifact for a flat list of peers.
"""
import argparse
import json
import sys
from pathlib import Path
## One function, three callers
from ..ir import check
from ..ops import classify
from ..style import Style, StyleError
This branch — erd, or index, or dot through Graphviz — used to be written out
three times: in the `auto` command, in the `site` command choosing its picture,
and in the book choosing its graph step. Three copies of "which drawing does this
graph want" is three chances for them to disagree about the answer, so it lives
here and returns content rather than writing it. Where the file goes, and what
to call it, is the caller's business.
"""
from ..ops import classify
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters auto")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, help="Directory to write into.")
p.add_argument("--style", default="lucid")
p.add_argument("--theme", default=None)
p.add_argument("--force", help="Use this emitter regardless of what fits.")
args = p.parse_args(argv)
# What each emitter produces, so a caller can name the file without knowing
# which emitter ran.
SUFFIX = {"erd": ".svg", "dot": ".svg", "index": ".md"}
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)}):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
def draw(ir: dict, style, *, force: str | None = None) -> dict:
"""The drawing this graph asks for, as content.
verdict = classify(data)
chosen = args.force or verdict["emitter"]
print(f" {verdict['kind']:<8} -> {chosen}")
print(f" {verdict['why']}")
Returns::
out_dir = args.output or Path(".")
out_dir.mkdir(parents=True, exist_ok=True)
stem = args.ir.stem
{"verdict": <classify's verdict>,
"emitter": "erd" | "dot" | "index",
"suffix": ".svg" | ".md",
"content": str | bytes | None, # None when it could not be drawn
"why": why this emitter, or why nothing was drawn}
try:
style = Style.load(args.style, theme=args.theme)
except StyleError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
Never raises for a missing Graphviz. A layered graph on a machine without
`dot` is a normal state — the book records the step as skipped and the site
goes text-only — so it is reported in `why` with `content` None.
"""
verdict = classify(ir)
chosen = force or verdict["emitter"]
out = {"verdict": verdict, "emitter": chosen, "suffix": SUFFIX.get(chosen, ".svg"),
"content": None, "why": verdict["why"]}
if chosen == "erd":
from .erd import emit as erd_emit
path = out_dir / f"{stem}.svg"
path.write_text(erd_emit(data, style))
from .erd import emit
out["content"] = emit(ir, style)
elif chosen == "index":
from .index import to_markdown
path = out_dir / f"{stem}.md"
path.write_text(to_markdown(data))
out["content"] = to_markdown(ir)
elif chosen == "dot":
from .dot import emit, have_graphviz, render
if not have_graphviz():
out["why"] = "Graphviz is not installed — nothing drawn (apt install graphviz)"
else:
opts = verdict.get("options") or {}
out["content"] = render(emit(ir, style, rankdir=opts.get("rankdir")))
else:
from .dot import emit as dot_emit, render
path = out_dir / f"{stem}.svg"
opts = verdict.get("options") or {}
path.write_bytes(render(dot_emit(data, style, rankdir=opts.get("rankdir"))))
print(f" {path}")
return 0
raise ValueError(f"no emitter {chosen!r} — have: erd, dot, index")
return out

View File

@@ -1,86 +0,0 @@
""" python3 -m docgen.emitters dot <ir.json> [-o out.svg] [--style lucid] [--theme dark]"""
import argparse
import json
import sys
from pathlib import Path
from ..ir import check
from ..ops import shape
from ..style import Style, StyleError
from .dot import RenderError, emit, render
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters dot")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, help="Write here. .dot or .svg by suffix.")
p.add_argument("--style", default="lucid")
p.add_argument("--theme", default=None)
p.add_argument("--max-depth", type=int, default=None)
p.add_argument("--quiet", "-q", action="store_true", help="Do not warn about shape.")
args = p.parse_args(argv)
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)} problem(s)):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
try:
style = Style.load(args.style, theme=args.theme)
except StyleError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
# Aspect ratio is a property of the graph, not of the renderer: a layered
# engine puts one dependency level in one row, so the widest level is the
# width. Say so before writing the file, because the alternative is finding
# out from a 13671pt image — and the fix is never a layout flag, it is a
# smaller question.
if not args.quiet:
sh = shape(data)
if sh["widest_level"] > 20 or sh["nodes"] > 60:
est = sh["widest_level"] / max(sh["levels"], 1)
print(
f" note: {sh['nodes']} nodes, {sh['levels']} levels, widest level "
f"{sh['widest_level']} — this will render roughly {est:.0f}:1.",
file=sys.stderr,
)
print(
" Around 20 nodes is where it stops being a diagram. Try "
"`ops --split`,\n `--around <id> --hops 2`, or `--subtree <id>`. "
"Layout flags will not fix it.",
file=sys.stderr,
)
if sh["isolated"] > sh["nodes"] // 3:
print(
f" {sh['isolated']} of {sh['nodes']} nodes have no edges; they are "
"laid out side by side.",
file=sys.stderr,
)
dot_text = emit(data, style, max_depth=args.max_depth)
if not args.output:
sys.stdout.write(dot_text)
return 0
args.output.parent.mkdir(parents=True, exist_ok=True)
if args.output.suffix == ".dot":
args.output.write_text(dot_text)
else:
try:
args.output.write_bytes(render(dot_text, fmt=args.output.suffix.lstrip(".") or "svg"))
except RenderError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
print(f" {args.style}/{style.theme:6} {args.output}")
return 0

View File

@@ -1,50 +0,0 @@
""" python3 -m docgen.emitters erd <ir.json> [-o out.svg] [--theme dark]"""
import argparse
import json
import sys
from pathlib import Path
from ..ir import check
from ..style import Style, StyleError
from .erd import emit
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters erd")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path)
p.add_argument("--style", default="lucid")
p.add_argument("--theme", default=None)
args = p.parse_args(argv)
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)} problem(s)):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
try:
style = Style.load(args.style, theme=args.theme)
svg = emit(data, style)
except (StyleError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(svg)
import re
m = re.search(r'width="(\d+)pt" height="(\d+)pt"', svg)
size = f"{m.group(1)}x{m.group(2)} {int(m.group(1))/int(m.group(2)):.1f}:1" if m else ""
print(f" erd/{style.theme:6} {args.output} {size}")
else:
sys.stdout.write(svg)
return 0

View File

@@ -1,47 +0,0 @@
""" python3 -m docgen.emitters explore <ir.json> -o DIR [--scale 0.55] [--hops 1]"""
import argparse
import json
import sys
from pathlib import Path
from ..ir import check
from ..style import Style, StyleError
from .explore import write
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters explore")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, required=True, help="Directory.")
p.add_argument("--style", default="lucid")
p.add_argument("--theme", default=None)
p.add_argument("--scale", type=float, default=0.55)
p.add_argument("--width", type=int, default=1100)
p.add_argument("--hops", type=int, default=1)
p.add_argument("--title", default="")
args = p.parse_args(argv)
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)}):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
try:
style = Style.load(args.style, theme=args.theme)
path = write(data, style, args.output, scale=args.scale, width=args.width,
hops=args.hops, title=args.title)
except (StyleError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
graphs = len(list((args.output / "graphs").glob("*.svg"))) if (args.output / "graphs").exists() else 0
print(f" explore {path} {graphs} neighbourhood diagram(s)")
return 0

View File

@@ -1,43 +0,0 @@
""" python3 -m docgen.emitters index <ir.json> [-o out.md|out.json]"""
import argparse
import json
import sys
from pathlib import Path
from ..ir import check
from .index import to_markdown, to_sidebar
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters index")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, help=".md for the document, .json for a sidebar.")
p.add_argument("--title", default="")
args = p.parse_args(argv)
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)} problem(s)):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
if args.output and args.output.suffix == ".json":
text = json.dumps(to_sidebar(data), indent=2) + "\n"
else:
text = to_markdown(data, title=args.title)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
print(f" index {args.output}")
else:
sys.stdout.write(text)
return 0

View File

@@ -1,50 +0,0 @@
""" python3 -m docgen.emitters minimap <ir.json> [-o out.svg] [--scale 0.55]"""
import argparse
import json
import re
import sys
from pathlib import Path
from ..ir import check
from ..style import Style, StyleError
from .minimap import emit
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters minimap")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path)
p.add_argument("--style", default="lucid")
p.add_argument("--theme", default=None)
p.add_argument("--scale", type=float, default=0.55, help="Pixels per source line.")
p.add_argument("--width", type=int, default=1180, help="Wrap a shelf past this.")
args = p.parse_args(argv)
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)}):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
try:
style = Style.load(args.style, theme=args.theme)
svg = emit(data, style, scale=args.scale, target_width=args.width)
except (StyleError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(svg)
m = re.search(r'width="(\d+)pt" height="(\d+)pt"', svg)
print(f" minimap {args.output} {m.group(1)}x{m.group(2)}" if m else "")
else:
sys.stdout.write(svg)
return 0

View File

@@ -1,73 +0,0 @@
""" python3 -m docgen.emitters notebook <ir.json> [-o out.ipynb] [--overlay f.json]
--spec-out FILE write the generated spec (the base), for reading/diffing
--scaffold FILE write a blank overlay listing every step id
"""
import argparse
import json
import sys
from pathlib import Path
from ..ir import check
from ..notebook import dump, from_ir, merge, scaffold
from .notebook import emit
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters notebook")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path)
p.add_argument("--overlay", type=Path, help="Hand-written additions, re-applied.")
p.add_argument("--spec-out", type=Path, help="Write the generated spec too.")
p.add_argument("--scaffold", type=Path, help="Write a blank overlay and stop.")
p.add_argument("--base-url", default="https://api.example.invalid")
args = p.parse_args(argv)
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)}):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
base = from_ir(data, base_url=args.base_url)
if args.scaffold:
dump(scaffold(base), args.scaffold)
print(f" overlay {args.scaffold} {len(base['steps'])} step(s), none filled in")
return 0
overlay = None
if args.overlay:
if args.overlay.exists():
overlay = json.loads(args.overlay.read_text())
else:
print(f" note: no overlay at {args.overlay} — generating the base only",
file=sys.stderr)
spec, drift = merge(base, overlay)
for d in drift:
# The base moved under the overlay. Worth saying out loud; not a reason
# to refuse to build the document.
print(f" drift: {d}", file=sys.stderr)
if args.spec_out:
dump(spec, args.spec_out)
print(f" spec {args.spec_out}")
text = emit(spec)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
n = len(json.loads(text)["cells"])
extra = f", {len(drift)} drift" if drift else ""
print(f" notebook {args.output} {len(spec['steps'])} steps, {n} cells{extra}")
else:
sys.stdout.write(text)
return 0

View File

@@ -1,73 +0,0 @@
""" python3 -m docgen.emitters site <ir.json> -o DIR [--theme lucid]
Writes index.html, viewer.html, site.css and the graph — self-contained, offline.
"""
import argparse
import json
import sys
from pathlib import Path
from ..ir import check
from ..ops import classify
from ..style import Style, StyleError
from .site import write
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.emitters site")
p.add_argument("ir", type=Path)
p.add_argument("--output", "-o", type=Path, required=True, help="Directory.")
p.add_argument("--style", default="lucid")
p.add_argument("--theme", default=None)
p.add_argument("--title", default="")
p.add_argument("--no-graph", action="store_true")
args = p.parse_args(argv)
try:
data = json.loads(args.ir.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {args.ir}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"Error: {args.ir} is not a valid IR ({len(problems)}):", file=sys.stderr)
for pr in problems[:5]:
print(f" {pr}", file=sys.stderr)
return 1
try:
style = Style.load(args.style, theme=args.theme)
except StyleError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
args.output.mkdir(parents=True, exist_ok=True)
graph_name = None
if not args.no_graph:
# Whatever the structure asks for, so the page carries the right picture.
verdict = classify(data)
if verdict["emitter"] == "erd":
from .erd import emit as draw
(args.output / "graph.svg").write_text(draw(data, style))
graph_name = "graph.svg"
elif verdict["emitter"] == "dot":
from .dot import emit as dot_emit, have_graphviz, render
if have_graphviz():
opts = verdict.get("options") or {}
(args.output / "graph.svg").write_bytes(
render(dot_emit(data, style, rankdir=opts.get("rankdir")))
)
graph_name = "graph.svg"
else:
print(" note: graphviz absent — the site is text only", file=sys.stderr)
else:
print(f" note: {verdict['kind']}{verdict['why']}", file=sys.stderr)
print(" no diagram on the page; the index is the artifact", file=sys.stderr)
files = write(data, style, args.output, graph=graph_name, title=args.title)
for f in files:
print(f" site {f}")
if graph_name:
print(f" site {args.output / graph_name}")
return 0

View File

@@ -13,8 +13,8 @@ for positions, or re-deriving containment from edges. It is not, because
`parent` is containment and `kind` is meaning, and that is all a table of
contents needs.
python3 -m docgen.emitters index ir.json # markdown to stdout
python3 -m docgen.emitters index ir.json -o x.json # sidebar JSON
python3 -m docgen emit index ir.json # markdown to stdout
python3 -m docgen emit index ir.json -o x.json # sidebar JSON
## What it reports that a diagram cannot

View File

@@ -1,25 +0,0 @@
""" python3 -m docgen.extractors <db|openapi|usage|code> [options]"""
import sys
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if argv and argv[0] == "openapi":
from .openapi_main import main as run
return run(argv[1:])
if argv and argv[0] == "code":
from .code_main import main as run
return run(argv[1:])
if argv and argv[0] == "usage":
from .usage_main import main as run
return run(argv[1:])
if argv and argv[0] == "db":
from .db_main import main as run
return run(argv[1:])
from .db_main import main as run # bare form stays the db one, as before
return run(argv)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,7 +1,7 @@
"""
Source in several languages -> IR structure, via **tree-sitter**.
python3 -m docgen.extractors code --root src/ --lang auto -o ir.json
python3 -m docgen extract code --root src/ -o ir.json
Adopts a parser rather than writing one, which is the rule the brief sets and
the reason this handles generics, strings containing braces, nested types and

View File

@@ -1,39 +0,0 @@
""" python3 -m docgen.extractors code --root src/ [--ext .cs] [-o ir.json]"""
import argparse
import json
import sys
from pathlib import Path
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.extractors code")
p.add_argument("--root", "-s", required=True, type=Path)
p.add_argument("--output", "-o", type=Path)
p.add_argument("--ext", action="append", default=[],
help="Limit to these extensions. Default: every registered one.")
p.add_argument("--exclude", action="append", default=[])
args = p.parse_args(argv)
from .code import LANGUAGES, MissingParser, extract
try:
ir = extract(args.root, suffixes=args.ext or None, exclude=tuple(args.exclude))
except MissingParser as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except (NotADirectoryError, OSError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
text = json.dumps(ir.to_dict(), indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
from collections import Counter
c = Counter(n.kind for n in ir.nodes)
print(f"{len(ir.nodes)} nodes -> {args.output} "
+ " · ".join(f"{v} {k}" for k, v in sorted(c.items(), key=lambda kv: -kv[1])))
else:
sys.stdout.write(text)
return 0

View File

@@ -9,7 +9,7 @@ reading it means this extractor works for every source modelgen supports
(Django, SQLAlchemy, OpenAPI, CSV/ODS, a live database) without knowing about
any of them.
python3 -m docgen.extractors.db --schema cfg/sample/.../graphgen/schema.json
python3 -m docgen extract db --schema cfg/sample/.../graphgen/schema.json
Connecting to a live database is **not here**. `modelgen from-db --url ...`
reflects via SQLAlchemy's Inspector across dialects and writes the schema.json

View File

@@ -1,31 +0,0 @@
""" python3 -m docgen.extractors.db --schema path/to/schema.json [-o ir.json]"""
import argparse
import json
import sys
from pathlib import Path
from .db import extract
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.extractors.db")
p.add_argument("--schema", "-s", required=True, type=Path,
help="A graphgen-compatible schema.json, as modelgen emits.")
p.add_argument("--output", "-o", type=Path)
args = p.parse_args(argv)
try:
ir = extract(args.schema)
except (OSError, json.JSONDecodeError, KeyError) as e:
print(f"Error: could not read {args.schema}: {e}", file=sys.stderr)
return 1
text = json.dumps(ir.to_dict(), indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
print(f"{len(ir.nodes)} nodes, {len(ir.edges)} edges -> {args.output}")
else:
sys.stdout.write(text)
return 0

View File

@@ -5,7 +5,7 @@ Adapts `modelgen/loader/extract/openapi.py`, which already parses OpenAPI 3.x
and Swagger 2.0 and resolves `$ref`. This turns its output into IR nodes; it
does not re-parse anything.
python3 -m docgen.extractors.openapi --spec petstore.yaml -o ir.json
python3 -m docgen extract openapi --spec petstore.yaml -o ir.json
## Why this one matters more than it looks

View File

@@ -1,29 +0,0 @@
""" python3 -m docgen.extractors.openapi --spec petstore.yaml [-o ir.json]"""
import argparse
import json
import sys
from pathlib import Path
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.extractors.openapi")
p.add_argument("--spec", "-s", required=True, type=Path)
p.add_argument("--output", "-o", type=Path)
args = p.parse_args(argv)
from .openapi import extract
try:
ir = extract(args.spec)
except (OSError, ImportError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
text = json.dumps(ir.to_dict(), indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
eps = sum(1 for n in ir.nodes if n.kind == "endpoint")
print(f"{len(ir.nodes)} nodes ({eps} endpoints), {len(ir.edges)} edges -> {args.output}")
else:
sys.stdout.write(text)
return 0

View File

@@ -1,37 +0,0 @@
""" python3 -m docgen.extractors.python --root PATH [--exclude NAME ...]"""
import argparse
import json
import sys
from pathlib import Path
from . import extract
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.extractors.python")
p.add_argument("--root", "-s", required=True, type=Path, help="Tree to read.")
p.add_argument("--output", "-o", type=Path, help="Where to write. Default stdout.")
p.add_argument("--exclude", action="append", default=[], help="Directory name to skip.")
args = p.parse_args(argv)
try:
ir = extract(args.root, exclude=tuple(args.exclude))
except (NotADirectoryError, OSError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
text = json.dumps(ir.to_dict(), indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
skipped = sum(1 for n in ir.nodes if n.attrs.get("error"))
print(f"{len(ir.nodes)} nodes, {len(ir.edges)} edges -> {args.output}"
+ (f" ({skipped} file(s) unparsed)" if skipped else ""))
else:
sys.stdout.write(text)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -5,7 +5,7 @@ Reads a **HAR** file — the format browser devtools, mitmproxy, Charles and
Insomnia all export. Standard, JSON, stdlib-parseable, and already sitting on
most people's disk after ten minutes of using the thing they want documented.
python3 -m docgen.extractors usage --har session.har -o ir.json
python3 -m docgen extract usage --har session.har -o ir.json
## Why this exists

View File

@@ -1,32 +0,0 @@
""" python3 -m docgen.extractors usage --har session.har [-o ir.json]"""
import argparse
import json
import sys
from pathlib import Path
def main(argv=None):
p = argparse.ArgumentParser(prog="python3 -m docgen.extractors usage")
p.add_argument("--har", "-s", required=True, type=Path,
help="A HAR recording, as devtools/mitmproxy/Charles export.")
p.add_argument("--output", "-o", type=Path)
args = p.parse_args(argv)
from .usage import extract
try:
ir = extract(args.har)
except (OSError, json.JSONDecodeError, KeyError) as e:
print(f"Error: could not read {args.har}: {e}", file=sys.stderr)
return 1
text = json.dumps(ir.to_dict(), indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)
eps = sum(1 for n in ir.nodes if n.kind == "endpoint")
ops = sum(1 for n in ir.nodes if n.kind == "operation")
print(f"{len(ir.nodes)} nodes ({eps} endpoints, {ops} graphql), "
f"{len(ir.edges)} sequence edges -> {args.output}")
else:
sys.stdout.write(text)
return 0

View File

@@ -0,0 +1,12 @@
{
"source": "postgresql",
"models": {
"users": {"doc": "People who buy.", "fields": {"id": {"type": "int", "pk": true}, "email": {"type": "str"}}},
"orders": {"doc": "One purchase.", "fields": {"id": {"type": "int", "pk": true}, "user": {"type": "FK:users"}, "total": {"type": "float"}}},
"items": {"doc": "A line on an order.", "fields": {"id": {"type": "int", "pk": true}, "order": {"type": "FK:orders"}, "sku": {"type": "str"}}}
},
"relationships": [
{"from": "orders", "to": "users", "from_field": "user"},
{"from": "items", "to": "orders", "from_field": "order"}
]
}

View File

@@ -1,8 +0,0 @@
""" python3 -m docgen.ir <ir.json> — validate a document at the boundary."""
import sys
from .validate import main
if __name__ == "__main__":
sys.exit(main())

View File

@@ -5,7 +5,7 @@ Called wherever the IR crosses between layers: after an extractor writes one,
before an emitter reads one. That is the whole point of having one format — the
check is in one place instead of every consumer defending itself.
python3 -m docgen.ir path/to/ir.json
python3 -m docgen validate path/to/ir.json
Stdlib only. This is not a general JSON Schema engine and does not want to be;
it reads the field lists **out of `schema.json`** so the two cannot drift, then
@@ -23,7 +23,6 @@ that produce a broken diagram.
import json
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
@@ -276,37 +275,3 @@ def check_model_matches_schema() -> list[str]:
for extra in actual - declared:
problems.append(f"{cls.__name__} has {extra!r}; schema does not declare it")
return problems
def main(argv=None) -> int:
argv = sys.argv[1:] if argv is None else argv
if not argv:
print("usage: python3 -m docgen.ir <ir.json>", file=sys.stderr)
return 2
drift = check_model_matches_schema()
if drift:
print("model.py and schema.json disagree:", file=sys.stderr)
for d in drift:
print(f" {d}", file=sys.stderr)
return 1
path = Path(argv[0])
try:
data = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"Error: could not read {path}: {e}", file=sys.stderr)
return 1
problems = check(data)
if problems:
print(f"{path}: {len(problems)} problem(s)", file=sys.stderr)
for p in problems:
print(f" {p}", file=sys.stderr)
return 1
print(
f"{path}: ok — {len(data['nodes'])} nodes, {len(data['edges'])} edges, "
f"schema v{data['meta'].get('schema_version')}"
)
return 0

View File

@@ -2,7 +2,7 @@
Views over the IR. Filtering is not an emitter concern — it belongs here, once,
so the index, the diagram and the diff all narrow the same way.
python3 -m docgen.ops ir.json --drop-stdlib --only class -o smaller.json
python3 -m docgen view ir.json --drop-stdlib --only class -o smaller.json
"""
from .filter import (

View File

@@ -0,0 +1,32 @@
[project]
name = "docgen"
version = "0.2.0"
description = "Source artifacts into one graph IR, then into diagrams, indexes, notebooks and sites"
readme = "README.md"
# 3.11 for tomllib, which reads run files. Nothing else needs more than 3.10.
requires-python = ">=3.11"
# None. The whole structural path — extraction, the IR, views, every emitter but
# the rendered diagram, the book and its checks — is the stdlib. Everything below
# is optional, and the suite skips rather than fails without it.
dependencies = []
[dependency-groups]
# C# and TypeScript, through tree-sitter. Python never needs it: it uses `ast`.
code = [
"tree-sitter>=0.23",
"tree-sitter-c-sharp>=0.23",
"tree-sitter-typescript>=0.23",
"tree-sitter-python>=0.23",
]
# Harvesting a theme from exported SVG/PDF diagrams (style/extract.py).
harvest = ["lxml>=5.0"]
# Reading OpenAPI. Also needs the reference repo's modelgen — see reference.py.
openapi = ["pyyaml>=6.0"]
# Sanctioned experiments in lab/. Nothing outside lab/ imports it.
lab = ["networkx>=3.2"]
[tool.uv]
# A folder of modules run in place, not an installable package — the same choice
# dataconvert makes. Copy the folder anywhere and it still works; the lock pins
# what the optional groups resolve to.
package = false

View File

@@ -87,6 +87,17 @@ FIXTURE = {
}
# docgen's own Python — what a source scan should read. Not `HERE.rglob`: once
# `make sync` has run, `.venv/` under this folder holds thousands of third-party
# modules, and the standalone check reported lxml's own imports as docgen's.
NOT_OURS = {"__pycache__", ".venv", "venv", "node_modules", "out"}
def own_py_files():
return sorted(p for p in HERE.rglob("*.py")
if not NOT_OURS & set(p.relative_to(HERE).parts))
def check(name, condition, detail=""):
(PASS if condition else FAIL).append(name)
print(f" {'ok ' if condition else 'FAIL'} {name}"
@@ -1875,7 +1886,7 @@ def _standalone():
stdlib = set(sys.stdlib_module_names)
own = {PKG}
files = sorted(p for p in HERE.rglob("*.py") if "__pycache__" not in p.parts)
files = own_py_files()
foreign, seam_leaks = [], []
for path in files:
@@ -2005,6 +2016,309 @@ def _standalone():
_standalone()
# --------------------------------------------------------------------------
print("\n17. one command line, and library code that has none")
def _layout():
"""Library packages hold library code; the command line lives in cli/.
The rule exists because two kinds of file in one folder — `cli_dot.py` next
to `dot.py` — make every folder answer two questions, and the command-line
half is the half that grows copies. It decays by accretion: one convenient
`argparse` in a library module and the split is gone. So it is read from the
source rather than trusted.
"""
cli_mod = __import__(f"{PKG}.cli", fromlist=["*"])
# Where command-line code may live. lab/ is sanctioned experiments, and the
# suite is itself a script.
allowed = {"cli", "lab"}
offenders = []
for path in own_py_files():
rel = path.relative_to(HERE)
if rel.parts[0] in allowed or rel.as_posix() in ("__main__.py", "selftest.py"):
continue
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
names = []
if isinstance(node, ast.Import):
names = [a.name for a in node.names]
elif isinstance(node, ast.ImportFrom) and not node.level:
names = [node.module or ""]
if "argparse" in names:
offenders.append(rel.as_posix())
check(
"nothing outside cli/ parses a command line",
not offenders,
f"{sorted(set(offenders))} import argparse — command-line code belongs in cli/",
)
stray = sorted(
p.relative_to(HERE).as_posix() for p in own_py_files()
if (p.name == "__main__.py" and p.parent != HERE)
or p.name.startswith("cli_") or p.stem.endswith("_main")
)
check(
"one entry point — no __main__.py, cli_*.py or *_main.py in the packages",
not stray,
f"found {stray}",
)
missing = []
for name, (module, _) in cli_mod.COMMANDS.items():
try:
mod = __import__(f"{PKG}.cli.{module}", fromlist=["*"])
except ImportError as e:
missing.append(f"{name}: {e}")
continue
if not callable(getattr(mod, "main", None)):
missing.append(f"{name}: no main()")
check("every listed command has a module with main()", not missing, "; ".join(missing))
# How a failure reaches the user is part of the interface: one line on
# stderr, exit 1, never a traceback. Run as a real process, the way a shell
# or a Makefile sees it.
env = dict(__import__("os").environ, PYTHONPATH=str(HERE.parent))
run = lambda *a: _sub.run([sys.executable, "-m", PKG, *a], capture_output=True,
text=True, env=env)
bare = run()
check("with no command it prints the commands and exits 2",
bare.returncode == 2 and "emit" in bare.stdout and "run" in bare.stdout,
f"exit {bare.returncode}")
bad = run("validate", str(Path(tmp.name) / "no-such.json"))
check(
"an expected failure is one line on stderr, exit 1, no traceback",
bad.returncode == 1 and bad.stderr.startswith("Error:")
and "Traceback" not in bad.stderr and bad.stderr.count("\n") == 1,
f"exit {bad.returncode}: {bad.stderr[:120]!r}",
)
# The structure-picks-the-drawing branch used to be written three times —
# the `auto` command, the `site` command, the book. Now it is `draw()`, and
# the book reaching past it to the concrete emitters would be the first copy.
# Parsed, not grepped, for the reason section 2 gives: the docstring naming
# `emitters.dot` is prose, and the first version of this check failed on it.
imported = set()
for node in ast.walk(ast.parse((HERE / "book" / "build.py").read_text())):
if isinstance(node, ast.ImportFrom) and node.module:
imported |= {f"{node.module}.{a.name}" for a in node.names}
check(
"the book chooses its drawing through draw(), not by hand",
"emitters.auto.draw" in imported
and not any(m.startswith(("emitters.erd", "emitters.dot")) for m in imported),
f"imports {sorted(m for m in imported if m.startswith('emitters'))}",
)
_layout()
# --------------------------------------------------------------------------
print("\n18. run files — every book, rebuilt with one command")
def _runfiles():
cfg = __import__(f"{PKG}.book.config", fromlist=["*"])
build_mod = __import__(f"{PKG}.book.build", fromlist=["*"])
ref_mod = __import__(f"{PKG}.reference", fromlist=["*"])
import os as _os
base = Path(tmp.name) / "runfiles"
(base / "src" / "app").mkdir(parents=True, exist_ok=True)
(base / "src" / "app" / "__init__.py").write_text('"""App."""\n')
(base / "src" / "app" / "models.py").write_text('"""Models."""\n\n\nclass User:\n pass\n')
(base / "shop.json").write_text((HERE / "fixtures" / "shop.json").read_text())
def write(text, name="docgen.toml"):
path = base / name
path.write_text(text)
return path
def problems(text):
try:
cfg.load(write(text, "bad.toml"))
except cfg.ConfigError as e:
return e.problems
return []
example = cfg.load(HERE / "docgen.example.toml")
check(
"the shipped example loads",
[b.name for b in example.books] == ["extractors", "schema", "api"],
f"got {[b.name for b in example.books]}",
)
good = write(
'[defaults]\nout = "books"\nexclude = ["vendor"]\n\n'
'[[book]]\nname = "app"\nroot = "src"\n\n'
'[[book]]\nname = "shop"\nschema = "shop.json"\n\n'
'[[book]]\nname = "narrow"\nroot = "src/app"\nout = "elsewhere/narrow"\nexclude = []\n'
)
# Relative to the FILE. Loaded from another working directory, a run file
# that resolved against cwd would build different books from the same text.
previous = _os.getcwd()
try:
_os.chdir(Path(tmp.name))
rf = cfg.load(good)
finally:
_os.chdir(previous)
by = {b.name: b for b in rf.books}
check(
"paths resolve against the run file, not the working directory",
by["app"].source == base / "src" and by["app"].out == base / "books" / "app",
f"source {by['app'].source}, out {by['app'].out}",
)
check("a book's own out overrides <defaults.out>/<name>",
by["narrow"].out == base / "elsewhere" / "narrow")
check(
"a book's value replaces the default — exclude included, no merging",
by["app"].exclude == ("vendor",) and by["narrow"].exclude == (),
f"app {by['app'].exclude}, narrow {by['narrow'].exclude}",
)
check(
"an inherited exclude does not apply to a non-tree source, and is not an error",
by["shop"].exclude == () and by["shop"].kind == "db",
)
# Refused, not ignored — and every problem at once, not the first.
found = problems(
'colour = "red"\n[defaults]\nstyel = "x"\n\n'
'[[book]]\nname = "a"\nroot = "src"\nexlude = ["x"]\n'
)
check(
"unknown keys are refused at every level, all reported together",
len(found) == 3 and all("unknown" in f for f in found),
f"got {found}",
)
check(
"a book names exactly one source",
any("exactly one source" in f for f in problems(
'[[book]]\nname = "a"\nroot = "src"\nschema = "shop.json"\n')),
)
check(
"an explicit exclude on a non-tree source is refused",
any("only applies to a root" in f for f in problems(
'[[book]]\nname = "a"\nschema = "shop.json"\nexclude = ["x"]\n')),
)
# The next run would read the last run's output: a rebuild that changes on
# every rebuild.
check(
"a book written inside the tree it reads is refused",
any("inside the tree it reads" in f for f in problems(
'[[book]]\nname = "a"\nroot = "src"\nout = "src/book"\n')),
)
check(
"two books writing to one directory are refused",
any("both write to" in f for f in problems(
'[[book]]\nname = "a"\nroot = "src"\nout = "same"\n\n'
'[[book]]\nname = "b"\nschema = "shop.json"\nout = "same"\n')),
"the second would clear the first on every run, silently",
)
try:
rf.select(["app", "nope"])
selected = False
except cfg.ConfigError:
selected = True
check("--only with an unknown name is an error, not an empty run", selected)
# The caller's env beats the file — rig's precedence rule for every setting.
saved = _os.environ.pop(ref_mod.ENV_VAR, None)
try:
with_ref = cfg.load(write(
f'reference = "{base}"\n[[book]]\nname = "a"\nroot = "src"\n', "ref.toml"))
check("a run file can name the reference repo",
cfg.apply_reference(with_ref) == str(base)
and _os.environ.get(ref_mod.ENV_VAR) == str(base))
_os.environ[ref_mod.ENV_VAR] = "/from/the/environment"
check("but the environment wins over the file",
cfg.apply_reference(with_ref) is None
and _os.environ[ref_mod.ENV_VAR] == "/from/the/environment")
finally:
_os.environ.pop(ref_mod.ENV_VAR, None)
if saved is not None:
_os.environ[ref_mod.ENV_VAR] = saved
# -- running it ----------------------------------------------------------
broken = write(
'[[book]]\nname = "app"\nroot = "src"\nout = "run/app"\n\n'
'[[book]]\nname = "bad"\nhar = "not-a-har.har"\nout = "run/bad"\n\n'
'[[book]]\nname = "shop"\nschema = "shop.json"\nout = "run/shop"\n',
"run.toml",
)
(base / "not-a-har.har").write_text("this is not json")
results = {r["name"]: r for r in cfg.run(cfg.load(broken), check=True)}
check(
"one broken book does not stop the others",
results["app"]["ok"] and results["shop"]["ok"]
and not results["bad"]["ok"] and results["bad"]["error"],
{k: (v["ok"], v["error"]) for k, v in results.items()},
)
check("each built book passed its own level", not results["app"]["checks_failed"])
# Rerunning is the point of a run file, so a rerun must not leave the last
# run's artifacts beside the new ledger, and must not touch what a person wrote.
app = base / "run" / "app"
(app / "steps" / "stale.svg").write_text("<svg/>")
(app / "checks.py").write_text("def checks(book, check, note, skip):\n pass\n")
again = {r["name"]: r for r in cfg.run(cfg.load(broken), ["app"])}
check(
"a rebuild removes the previous build's outputs",
again["app"]["ok"] and not (app / "steps" / "stale.svg").exists(),
"a stale graph.svg would go on showing in the site, unlisted in the ledger",
)
check("...and keeps the hand-written checks.py", (app / "checks.py").exists())
foreign = base / "not-a-book"
foreign.mkdir(exist_ok=True)
(foreign / "steps").mkdir(exist_ok=True)
(foreign / "site").mkdir(exist_ok=True)
check(
"clear() never touches a directory it did not write",
build_mod.clear(foreign) == [] and (foreign / "steps").exists(),
"a mistyped `out` must not delete somebody's `site/`",
)
# -- the environment the lock pins ---------------------------------------
import tomllib as _toml
project = _toml.loads((HERE / "pyproject.toml").read_text())
check(
"pyproject declares no required dependencies",
project["project"]["dependencies"] == [],
"the structural path is the stdlib; everything else is an optional group",
)
check(
"it requires the Python that reads run files",
project["project"]["requires-python"] == ">=3.11",
"tomllib is 3.11+",
)
# A module the suite allows docgen to import must be a dependency somebody
# can install from the lock, or the allow-list and pyproject have drifted.
declared = {d.split(">")[0].split("=")[0].strip().lower().replace("-", "_")
for group in project["dependency-groups"].values() for d in group}
import_to_dist = {"tree_sitter": "tree_sitter", "tree_sitter_c_sharp": "tree_sitter_c_sharp",
"tree_sitter_typescript": "tree_sitter_typescript",
"tree_sitter_python": "tree_sitter_python",
"lxml": "lxml", "yaml": "pyyaml", "networkx": "networkx"}
undeclared = sorted(m for m, dist in import_to_dist.items() if dist not in declared)
check("every optional import is a declared dependency group", not undeclared,
f"{undeclared} are allowed but not in pyproject")
lock = HERE / "uv.lock"
locked = set()
if lock.exists():
locked = {pkg["name"].replace("-", "_")
for pkg in _toml.loads(lock.read_text()).get("package", [])}
check(
"uv.lock pins every declared dependency",
lock.exists() and declared <= locked,
f"not in the lock: {sorted(declared - locked)} — run `make lock`",
)
_runfiles()
print(f"{len(PASS)} passed, {len(FAIL)} failed, {len(SKIP)} skipped")
if FAIL:
print("\nfailed:")

321
soleprint/atlas2/docgen/uv.lock generated Normal file
View File

@@ -0,0 +1,321 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "docgen"
version = "0.2.0"
source = { virtual = "." }
[package.dev-dependencies]
code = [
{ name = "tree-sitter" },
{ name = "tree-sitter-c-sharp" },
{ name = "tree-sitter-python" },
{ name = "tree-sitter-typescript" },
]
harvest = [
{ name = "lxml" },
]
lab = [
{ name = "networkx" },
]
openapi = [
{ name = "pyyaml" },
]
[package.metadata]
[package.metadata.requires-dev]
code = [
{ name = "tree-sitter", specifier = ">=0.23" },
{ name = "tree-sitter-c-sharp", specifier = ">=0.23" },
{ name = "tree-sitter-python", specifier = ">=0.23" },
{ name = "tree-sitter-typescript", specifier = ">=0.23" },
]
harvest = [{ name = "lxml", specifier = ">=5.0" }]
lab = [{ name = "networkx", specifier = ">=3.2" }]
openapi = [{ name = "pyyaml", specifier = ">=6.0" }]
[[package]]
name = "lxml"
version = "6.1.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/ad/28ecd7cb894d172f3c9c80a075eeeb2017ac62e3632cee05a5f9493547eb/lxml-6.1.3.tar.gz", hash = "sha256:45222d94ddd511536f3b2f7d9deae3b2339b4ce0f075f1ca25703b07cad9dd21", size = 4211198, upload-time = "2026-09-02T14:48:02.287Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/f1/95133bde7af7afb1f5ba6090b674d826b7a518318bba54bbbb633b27865a/lxml-6.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c66f858b82497173f73366795fc6ee8171620e75a338506d6b2e7bc16f5fca11", size = 8563141, upload-time = "2026-09-02T14:46:42.334Z" },
{ url = "https://files.pythonhosted.org/packages/80/54/5a79ee2181ac773ee13e48205411845feec69e1c3d097e985c1343171712/lxml-6.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:032a0a97eed428bd143c75a11118238546424ceb2fa311cca5f073aa44658dc4", size = 4613690, upload-time = "2026-09-02T14:46:45.253Z" },
{ url = "https://files.pythonhosted.org/packages/ab/29/8c24672f56807f119312f073f24204368574bd16b384ede861b5104b3a2b/lxml-6.1.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a579dfb9c835f8ab47f4b8ed33440cbc75b806b73297208e6ec2a33e903740b", size = 4935630, upload-time = "2026-09-02T14:46:48.071Z" },
{ url = "https://files.pythonhosted.org/packages/71/69/ce2436d854c848c19fc9287143991f3fc76b8b4e9a0dbba8452e51dff264/lxml-6.1.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49fbc2682a9306135b7ec49e93f97f9c26689b9b7f96ed2742d8d6497e994d13", size = 5079033, upload-time = "2026-09-02T14:46:50.483Z" },
{ url = "https://files.pythonhosted.org/packages/91/ec/b66f66f6499ad800265d57540b51e6632e3232d3526f42f2f8fd4b14e0ea/lxml-6.1.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea2c01cdb16dc12156e455007c406dfaaece0c89aa4ba0e3b47586779f951d41", size = 5012298, upload-time = "2026-09-02T14:46:52.603Z" },
{ url = "https://files.pythonhosted.org/packages/94/2a/25d128872f4d51753542bfc3feb482c2ea7c8a2d6d81a0bc5c6a00779ed4/lxml-6.1.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:527195c188d7d0af748cd48d220ab8cdc5cb99be3d49ac4d9be7324d8abf9bc0", size = 5211431, upload-time = "2026-09-02T14:46:54.722Z" },
{ url = "https://files.pythonhosted.org/packages/75/b2/0a41bbef074a556110f84fafb6d8c2998293c7d3bfbe1ce74515bc65393b/lxml-6.1.3-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:20384c2bbcbf87180c8c61eb60869699c1ec0cd09b62cfd13804022d860b0867", size = 5343417, upload-time = "2026-09-02T14:46:57.46Z" },
{ url = "https://files.pythonhosted.org/packages/7b/cd/16116c3f91791aeeeab1cbe6e7eb6e646f127be7b0158b262eb526a21a0c/lxml-6.1.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:424aa5657141d306ba9ad1baab4b2c0a0719040075ee6c66aee9bb2dea2b5054", size = 4673219, upload-time = "2026-09-02T14:46:59.604Z" },
{ url = "https://files.pythonhosted.org/packages/dd/bb/4dff849f443ef70221676aec938bc41e8bae6430aa2ca13b041319e14b98/lxml-6.1.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4736e6c87e603146d8949d8501da621ad20c31015060d3fcf95ace2859f3e3e6", size = 5281246, upload-time = "2026-09-02T14:47:02.375Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ac/4aa7dd059420bfd35278c7fe819e9d319ee36a0453b7bbde1907a7832d91/lxml-6.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6374e9e382e5a98c9c5e66d41b357b470da1c54bce30f17f9dc4bcc58436cc1c", size = 5055451, upload-time = "2026-09-02T14:47:05.883Z" },
{ url = "https://files.pythonhosted.org/packages/de/44/20d90cf6f4234de9cd9eeb4f519419885fdb087fa80d073c7b57be342021/lxml-6.1.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:22eec57e26c418cde02c051ce9914a365e52a7f135a565c6f0480242aeebab48", size = 4722694, upload-time = "2026-09-02T14:47:08.461Z" },
{ url = "https://files.pythonhosted.org/packages/f0/0e/6bee12325e53dd6613fe1e107def07583b6182ade03e94bfef8976622e44/lxml-6.1.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8753b8d51dbc86fd335ee31fcf7f3658e9f5c016d4edfb23f76ad295f4b8c9d0", size = 5269179, upload-time = "2026-09-02T14:47:10.647Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5d/54d269ce5cd0787c0424d9cef449ee794d4097725d13dd2acd6181c44e9c/lxml-6.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:207dfc3d47cf0e575e643bbc140dacc8863b39abaa1e5307cd64c7f2365b8a12", size = 5235559, upload-time = "2026-09-02T14:47:13.932Z" },
{ url = "https://files.pythonhosted.org/packages/e4/f7/5a3095f187f1bec293591616a1677781acc265c5b313c009f8a19c471a09/lxml-6.1.3-cp311-cp311-win32.whl", hash = "sha256:18293f8a8d8b6a8e71ef37706b659e3846a4261232158167b1ddf35f6994f633", size = 3600377, upload-time = "2026-09-02T14:47:15.957Z" },
{ url = "https://files.pythonhosted.org/packages/45/5a/15531a0d307c96282fe8b639b3d74e8bd783e4ab4cb2b0781146ac4161b8/lxml-6.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:7ae4949f212a53b007dbc355884fda122545c5764a54256c9217e419a62a6559", size = 4032700, upload-time = "2026-09-02T14:47:18.566Z" },
{ url = "https://files.pythonhosted.org/packages/12/f9/8de76314955545ceaaa7c0305017b8aaa217905dee59c62c0e2c1e44a68f/lxml-6.1.3-cp311-cp311-win_arm64.whl", hash = "sha256:2123e5aa075ac20d23c7af489255efd129cbfe190dbe88fd42598cc9df3199b6", size = 3674431, upload-time = "2026-09-02T14:47:22.186Z" },
{ url = "https://files.pythonhosted.org/packages/dd/1f/a180b57d9eeabaab77f9d5aa30356898ea749c4795596a8f66d1eb6bef2e/lxml-6.1.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c0710ac085a157b593c38fbcacd950f15c4afa8e2057527185875ab302752bc", size = 8602094, upload-time = "2026-09-02T14:47:26.054Z" },
{ url = "https://files.pythonhosted.org/packages/a8/25/070c92013a1c029a602b03560d68772313d918268667fa993da7961759c9/lxml-6.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d", size = 4638308, upload-time = "2026-09-02T14:47:29.587Z" },
{ url = "https://files.pythonhosted.org/packages/1e/1c/722e88883173097a1a375153e3c2447eba3060d0231522cf6596e99f4195/lxml-6.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5", size = 4939696, upload-time = "2026-09-02T14:47:32.997Z" },
{ url = "https://files.pythonhosted.org/packages/db/36/aa413bc214dc4f785ad2b2ddd8cc99aae7062d49ab155e91e6011af00daf/lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11", size = 5105247, upload-time = "2026-09-02T14:47:36.734Z" },
{ url = "https://files.pythonhosted.org/packages/a3/a0/a1f7f1313795bfec67b77f01ef3b1128d49f2d7f66a8413fa55d47f4e25f/lxml-6.1.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b37772102d44bb6628186accca3a121b1fa3a6b3d97518a8c29a5229ca4c0d0a", size = 5011915, upload-time = "2026-09-02T14:47:39.846Z" },
{ url = "https://files.pythonhosted.org/packages/b9/78/840e7e3f1d0cc7a5cfac5d8505b97e25b6427fd774ac4bae672aaebfb4b5/lxml-6.1.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddcf547bea2aee967d6a77779376a45e77e610e8465147a1f3d7e20d539d6e32", size = 5638175, upload-time = "2026-09-02T14:47:43.644Z" },
{ url = "https://files.pythonhosted.org/packages/0a/20/e022dbc6b4753a9bc9fc5fb28a27163430c1731b9913997f6544c1b2518c/lxml-6.1.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:909f4e927bb051f7740d6367285fc60cdcfdaf0258c2dba4ff5ba7eadadc250c", size = 5244675, upload-time = "2026-09-02T14:47:47.635Z" },
{ url = "https://files.pythonhosted.org/packages/99/83/82cde81d2b5eb38d1539fdfdf318abdd014a7e604f4df01c9cd3deb18f2a/lxml-6.1.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a5c18810318303ce9afb3f95e2ddb54834f96fa699a8600433fd5a93dcf44c56", size = 5358205, upload-time = "2026-09-02T14:47:50.306Z" },
{ url = "https://files.pythonhosted.org/packages/d2/a1/f3b057371c8cb29f2a9c9c44ea320592446e40b74a4b0af68c3d8e65bc73/lxml-6.1.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3e42265103fb385d8642a78672edf376c6f7e1d3598a7a4f9cb1278f2f6b5f6f", size = 4704495, upload-time = "2026-09-02T14:47:53.251Z" },
{ url = "https://files.pythonhosted.org/packages/1a/a4/230eb28be5d412152ffc3c679b51fe1aeede5a53f3a8eb6e9748f2f4754f/lxml-6.1.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:21402998e4b78e7cce237d2788841aaa21ac9a4d1574d04dc2d12ee41ae807b5", size = 5255117, upload-time = "2026-09-02T14:47:55.963Z" },
{ url = "https://files.pythonhosted.org/packages/a3/18/1969f56763af24ce42ea156007b0b2d73fddea552e283b2010416394f0f4/lxml-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:38fc4e4e4e084e0bd491949482527d406788045c546d4f8789e93fc527b91385", size = 5054424, upload-time = "2026-09-02T14:47:58.131Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d4/2a90acc1f6fabaa3a8db9340437822bd8d041b205d626a4b3e8621aaa390/lxml-6.1.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5609efdb0d3c95499c00046bc53648b3482ec2175b5503d6e611b3f0555dc71d", size = 4785572, upload-time = "2026-09-02T14:48:01.029Z" },
{ url = "https://files.pythonhosted.org/packages/a5/1e/b90e845b1dcd0f2f3f26b98283d857f25909223aacd265eee032c34ab8b1/lxml-6.1.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97ce49699d87ebf8aad631b55d65b33219a4f1bfefbbf5bff19dc9af160aeaf9", size = 5656516, upload-time = "2026-09-02T14:48:03.419Z" },
{ url = "https://files.pythonhosted.org/packages/eb/ab/0a1b802c57f3fba5c4efd77d5c6b78adaa8f7b681f0c90456b140fe8bf6c/lxml-6.1.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:48542c9acba9ff9450bd18d871d2c2c8787fdb283572b623d206f1b927cd7d9e", size = 5245982, upload-time = "2026-09-02T14:48:06.109Z" },
{ url = "https://files.pythonhosted.org/packages/da/ee/2c016fbceb3778137459292538d9dfa7e3ad9070fe409c15254ddd90d2cc/lxml-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c55e71a9b1db1f107efb60da49c093689b74c5c31a708e5379e2fd9439d4fbb5", size = 5267340, upload-time = "2026-09-02T14:48:08.374Z" },
{ url = "https://files.pythonhosted.org/packages/9c/b1/736d18fd6f0835761923b7bac1f0c27d60c1200384e9093f05d8c5100525/lxml-6.1.3-cp312-cp312-win32.whl", hash = "sha256:b3ff39654f0ce6ebd4db154211136dbe7e8157bcc3bed2344c87f32c7c6ecb6c", size = 3602606, upload-time = "2026-09-02T14:48:10.384Z" },
{ url = "https://files.pythonhosted.org/packages/3a/5b/6ed903e4e6278a020c8a6f0dbbe78030d041840a6b4a64ea441a1e414077/lxml-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:3e9a00d1c2c30936f7add097c41afc5da6556c580909104aafd382cac92a855c", size = 4005999, upload-time = "2026-09-02T14:48:12.51Z" },
{ url = "https://files.pythonhosted.org/packages/e4/1b/7bcebb7b6332cb3ae85e9c13b139adb6f23f75c71d84041c56a5005d9a29/lxml-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:1aeca87830c4fe649dcf93fe2b059525b71c72587f21be4ae4af7103082a79fa", size = 3666631, upload-time = "2026-09-02T14:48:14.567Z" },
{ url = "https://files.pythonhosted.org/packages/52/05/3ef45db776baea068044c799bbba68f3ca00a440c0e930a17c572f3d9639/lxml-6.1.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3a48093cdb058a93af842ede9703520e810b05dcd0fc6d7190a06376c3bfb6bd", size = 8590357, upload-time = "2026-09-02T14:48:17.413Z" },
{ url = "https://files.pythonhosted.org/packages/8c/a5/eee2fc77eee5ea68e4a4334b1def1781a3beaeefd3d98e81b4a38dc447b7/lxml-6.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:887c021d9a977cff89cb273047c1352997b772a8908a25c21836861f69b92be1", size = 4632616, upload-time = "2026-09-02T14:48:20.745Z" },
{ url = "https://files.pythonhosted.org/packages/35/42/df27b56848acd29d8a720acc28977911aab36f2a09df4208d5502e887415/lxml-6.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:611a51e61c92f62345a50b0035df6fc0d678f9299f33728826d831598862f59d", size = 4936186, upload-time = "2026-09-02T14:48:22.94Z" },
{ url = "https://files.pythonhosted.org/packages/ab/8d/8a7b91df0b54d09d25f5f44885d6b3e0a6d6643a8c070191580318d20c42/lxml-6.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b477912f42c5c33405a10c759d22f80cf5af043ae02d95b9d8e5e5bc555739ed", size = 5093324, upload-time = "2026-09-02T14:48:25.132Z" },
{ url = "https://files.pythonhosted.org/packages/c6/7e/8f340ddcd43790332fb0de8a26628d571a492da3300cd191821698407c96/lxml-6.1.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cffe18571ccc51d742cd08cbb3f8b756de9311d18c7ea98f5d92f37b8fb60c2", size = 4998850, upload-time = "2026-09-02T14:48:27.394Z" },
{ url = "https://files.pythonhosted.org/packages/c5/c1/9c5bb572f1f09ec9e4322bd4a4e9f4ad48347fc56ef94cf4df58a5279dc8/lxml-6.1.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75cc6569e86be5785b6188ef1642670c6adbc984e81ec35e224842ecd9eefcc8", size = 5626813, upload-time = "2026-09-02T14:48:29.61Z" },
{ url = "https://files.pythonhosted.org/packages/ac/7d/8bf1fd8bae8247743968bb76d027a1ac5bd2c4b44495fba6a71b30d10706/lxml-6.1.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d85dfab42dd672f87a7f76e9de7172962aee69fa12044f0d6e1a23cbd53fb80e", size = 5232385, upload-time = "2026-09-02T14:48:31.969Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2e/6cef69ed81cb7df0d03b0dd09d08e6e2cf5061a743ff6f42f0b741548e9b/lxml-6.1.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:42632b4024ab24a6b488f559ac851312509888b6b80ae2aa11cf29a646a0d245", size = 5347088, upload-time = "2026-09-02T14:48:34.13Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e1/8e5fd8ddc8c7d685badb0f2db149e3c9da84eefc2827c01c658df2c4e3cb/lxml-6.1.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:febd35ef45f603c2d74b74655efdbf45e14f55fc0aef4ac82b663ca829b283e0", size = 4707227, upload-time = "2026-09-02T14:48:36.62Z" },
{ url = "https://files.pythonhosted.org/packages/7a/7e/00041382a11be40a88bf405ebff11c8efabd3de79f2691e1638b1c47a8a0/lxml-6.1.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a43b3bdf11e477dc7770609d3477316f974354dfc8425d596f64f471cc8daf6e", size = 5240208, upload-time = "2026-09-02T14:48:38.893Z" },
{ url = "https://files.pythonhosted.org/packages/fd/fe/316538b5cff0936fa63d45d421c655730fcbb5a28dcac728c175083002bc/lxml-6.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d582042c69857c364e8153de6e18e0da9b7b515a6a8113caf69a6ec8e0520f2", size = 5050271, upload-time = "2026-09-02T14:48:41.213Z" },
{ url = "https://files.pythonhosted.org/packages/c9/91/455bcccb3ac725373007344d351151810cd19762d1673b64b811f4359a42/lxml-6.1.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e49a646acfab83c68974f4aa1d0a2acca9e88d7d627ae0fc13201b14b76d310", size = 4780433, upload-time = "2026-09-02T14:48:43.779Z" },
{ url = "https://files.pythonhosted.org/packages/cb/f6/580440e2f52cf00bba5c5e1080bfa88cdfcde73be71a11d95170ddbb663f/lxml-6.1.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0dee106e9aa97fb00541b1ed7827070564d0549c3d3fba8920e6b20fd980f748", size = 5645928, upload-time = "2026-09-02T14:48:46.187Z" },
{ url = "https://files.pythonhosted.org/packages/f6/dc/d123c1f244306543d545f62443f794959e4f1ea709fe100f8740d514e74a/lxml-6.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd5e90f34cffcfed97f36cf066325773d2b6021c60c29942e53a18b028501b1d", size = 5231184, upload-time = "2026-09-02T14:48:48.691Z" },
{ url = "https://files.pythonhosted.org/packages/c3/3c/fe55b2bd5c6113c906511cd88f6a470195c5fbff1124f19970ab706c3477/lxml-6.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9b3e7d71bf6acff341233417abbdface29c647e3113892d9aaedc02eb4aa2bc", size = 5255814, upload-time = "2026-09-02T14:48:50.948Z" },
{ url = "https://files.pythonhosted.org/packages/e7/a7/485df55acf55dc35e4ca89d2f48f03889e5a3241826b18b85102b32ce9d8/lxml-6.1.3-cp313-cp313-win32.whl", hash = "sha256:160fcf381f76c3aeac28a756bec44f48942a8f7245a87aa28e3a523b4d90cd87", size = 3602214, upload-time = "2026-09-02T14:48:53.236Z" },
{ url = "https://files.pythonhosted.org/packages/c0/28/e46a7702bd95e9043291f7c3539b6184cba66f96cea9936f20939b284eeb/lxml-6.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:e477aca0bc0d19f3b4ae9e4f2a1cfd687c31bf772d78734910658186b40b2477", size = 4004091, upload-time = "2026-09-02T14:48:55.699Z" },
{ url = "https://files.pythonhosted.org/packages/8a/1d/154c78e20479a43916e63f19cb720d83f44f024b03228be44c92d9a97b24/lxml-6.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:b1cc980905221a5d8b3c476330730b3adb40ff80add71ffbdb6215ba055656f1", size = 3665468, upload-time = "2026-09-02T14:48:57.703Z" },
{ url = "https://files.pythonhosted.org/packages/0c/15/fc75a70b0af6021d0ea16811f1fc71cc42cd06ce90fe10f007a69b2eed84/lxml-6.1.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2bec13085dc8ef48a3fe62f7dfcacfeda2c785cdf19cc8eeda2bb9ed081da165", size = 8609725, upload-time = "2026-09-02T14:49:00.156Z" },
{ url = "https://files.pythonhosted.org/packages/84/ef/398fcf9018f881ec9aeaafae1ddd6586dfb13314a35d35e899de373dcae0/lxml-6.1.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f4db7c7e954d289d71878938348b3d91b904a3e8210a11939359fb758a58e7d", size = 4639629, upload-time = "2026-09-02T14:49:02.81Z" },
{ url = "https://files.pythonhosted.org/packages/a7/2d/49b6a6ad7ce8f64b07b9fe852ff0c6d3fcbb26db61bee4f63d4120180a1c/lxml-6.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2cae5d5c90a62d9139c512a0cb1aad1d182b022b5740daea2617eb5bf7fc658e", size = 4965074, upload-time = "2026-09-02T14:49:05.133Z" },
{ url = "https://files.pythonhosted.org/packages/66/bc/6230cf80e4331c33383b0b6b73dc31a393dd76edd4cb73d761de5123034d/lxml-6.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c6c0c13128a32eb04a51357e56a094e13aa8e6d3d1884de2e9ae923f6915e1a8", size = 5099355, upload-time = "2026-09-02T14:49:07.343Z" },
{ url = "https://files.pythonhosted.org/packages/ac/cf/d1143d9b7717e07a82f158a1fc9ce6e581fdad1226734950af869e3ffde4/lxml-6.1.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2221e88679d1351e9a40aaee54bc65679b9795bbd0160bc3d5e36b163344eb75", size = 5036795, upload-time = "2026-09-02T14:49:09.65Z" },
{ url = "https://files.pythonhosted.org/packages/31/6f/194bb00ffb89712c30f5a7e1b8e685590e140fad6c8261fec172c09a3dc0/lxml-6.1.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfb398886a7eb4c719161c3efcff2a1248febc53a4d8e5072d2d8a87fed84ac9", size = 5658740, upload-time = "2026-09-02T14:49:11.9Z" },
{ url = "https://files.pythonhosted.org/packages/e9/44/27e3cee3dcdb3b7bc09727b642bdbfcd098490ea77df04611db9060d7722/lxml-6.1.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7eb78ba28b187e1e9203a55c60fcf70df2d22cb205fe6d51b9383d6097419f0", size = 5245991, upload-time = "2026-09-02T14:49:14.154Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e9/8312560579fc980bbd2233a8a673cc46f7d613d3633f2bf08a21e8f4ad13/lxml-6.1.3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:ea6b1e9105b4b24a34c722432d9fb578f9ed83af21fa1abda639011e0f22bbb6", size = 5354136, upload-time = "2026-09-02T14:49:16.459Z" },
{ url = "https://files.pythonhosted.org/packages/74/d8/eda60f4f73a9c780b5d6e1175484f66e6c81a2c93346e2906a1fec9c7a02/lxml-6.1.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e8b17e23df3e827a69d25af70990ca2420e92668aaffaeeb3cd2351d7916a023", size = 4704379, upload-time = "2026-09-02T14:49:19.032Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c8/c9cc60057be78ac34bd2b842e45e6e88edbfe5e532e82c3b82381b7aab49/lxml-6.1.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b7c37339d7e75cab9a123a04248e243cefefb302ad6db566ea0c77cbcde421e", size = 5258676, upload-time = "2026-09-02T14:49:21.306Z" },
{ url = "https://files.pythonhosted.org/packages/41/7b/66894008fee8d1785b8db129747ae963fd427b68f456918df7f2f24a8b98/lxml-6.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:83e3a51e7933db700a0da0db31849db3a24022d9970da9bb73001e1d0326fd92", size = 5090069, upload-time = "2026-09-02T14:49:23.562Z" },
{ url = "https://files.pythonhosted.org/packages/8b/31/c1b60404859f4c3cd1f41f29c65a24e25cea78fde822d9574a21f66810be/lxml-6.1.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bde9ae026a55b9a192078dfa6e27dd0ca4a050171ab6272e92f97b757dfdf48", size = 4741958, upload-time = "2026-09-02T14:49:26.037Z" },
{ url = "https://files.pythonhosted.org/packages/23/b8/6285f0cf546f14da2554cabdeaf7c2c2ff3190c74807f0de2e8810a786f9/lxml-6.1.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a635e837b50a1819bebfedaac5916498ea024120969da8790500148fb0a894d", size = 5683245, upload-time = "2026-09-02T14:49:28.438Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f6/2168cab44336dcb15fed0f0b78577225b83297cdf0dee349c95420c3dcb0/lxml-6.1.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d0c5c362bc94f1929dc7e96e715bbe7bd17037f802e6d8f0d1545df9133c0559", size = 5246087, upload-time = "2026-09-02T14:49:30.955Z" },
{ url = "https://files.pythonhosted.org/packages/f5/89/32f5de69a0a31f30e6164981851f87b37ecb2c4ee838e504b88d49d4818e/lxml-6.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c59e4265608da6a041f54646ecc0c9ecdbb19aaf14c4c684bb6c2114998cc415", size = 5269352, upload-time = "2026-09-02T14:49:33.502Z" },
{ url = "https://files.pythonhosted.org/packages/a2/a1/741d952ed3a7ef7a50055c6415aec3f067015e97f72f4389ce77b09657ba/lxml-6.1.3-cp314-cp314-win32.whl", hash = "sha256:2e62c569ec7531b679b184cbfe335c501c1d13c4b363560013019962eb630e6d", size = 3662783, upload-time = "2026-09-02T14:50:23.751Z" },
{ url = "https://files.pythonhosted.org/packages/0f/bc/5811cc73cac05e324e05ba9b0924e1a163a317a167ede8a9c748b11db30a/lxml-6.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:66299564c046bc7e0cc5de5106601eae907e9fa5904cd68a323380a8502f7861", size = 4073951, upload-time = "2026-09-02T14:50:26.348Z" },
{ url = "https://files.pythonhosted.org/packages/92/18/3768c8b01ac3a9bed1914715e6011711b00e2a11628ffa6f7fa37f8e0269/lxml-6.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:ebd054ad1737a68fb7c5c073d405cef2b88bb824e294de3b4a4e995b47f0e376", size = 3749279, upload-time = "2026-09-02T14:50:28.749Z" },
{ url = "https://files.pythonhosted.org/packages/72/38/84684784738d9451db2b330de2483f496690c3a5c642071df24135739b37/lxml-6.1.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5a143e6207579de8baeded4eaac9134413200359f1969d636f0bfb98ee8c3c8f", size = 8860296, upload-time = "2026-09-02T14:49:36.346Z" },
{ url = "https://files.pythonhosted.org/packages/24/b7/fc4c50bb1b38e864010ea396046cabe85129bf9e65b11edcfbc37d356241/lxml-6.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1cec0f99b9b914d39176347a93b7610dc09324491aee1cbc57cd291a41a1d55", size = 4755190, upload-time = "2026-09-02T14:49:39.872Z" },
{ url = "https://files.pythonhosted.org/packages/94/e2/ee9aa6ed2b666b2db1f6f7fd48964ff9da39ebe827ef5eac0ab881f639d9/lxml-6.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6b9d2aad499c769ee8287609ab0e6de99d8bcea99c6e6c2e64945259fd52fb2", size = 4979517, upload-time = "2026-09-02T14:49:42.153Z" },
{ url = "https://files.pythonhosted.org/packages/29/e3/e7763d1661b283ddd4fa36f91b9a497db6b8d2aff55028b16c7f642e0755/lxml-6.1.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a23fefdb345b2d4d0ff2860571b5ff9a89a28b6a120f720e8fb0324d346626", size = 5115270, upload-time = "2026-09-02T14:49:44.493Z" },
{ url = "https://files.pythonhosted.org/packages/2d/cd/22205d5b4d177e3f4156f780412426ee7c7f8107809f119f0dcc40fa51e3/lxml-6.1.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:545ccc14fb05485f48b4439ec35beb16d5b5280eb6c81c658bd4707a2a119414", size = 5032449, upload-time = "2026-09-02T14:49:46.841Z" },
{ url = "https://files.pythonhosted.org/packages/da/43/06a4626c3bb79ef8c501b674afab8100d64e798665bb2a97d1c960636a49/lxml-6.1.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93476b6514b373fc6ca67d26c442784f7807c86f00635bfe79f935c3eab2af17", size = 5603325, upload-time = "2026-09-02T14:49:49.664Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9c/733682a0c2de9f5779ba207bbb3f3f6be8c6bda863fc01739b186b38783a/lxml-6.1.3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8db38ff3fb7aee7d6a82ae4da2eef1178656fe1216841fbd24870062a9d60473", size = 5229023, upload-time = "2026-09-02T14:49:52.447Z" },
{ url = "https://files.pythonhosted.org/packages/c6/8a/e69cdaca3fd33a647942925664f01b20908d41a6968c182305be9c38fb11/lxml-6.1.3-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:25f4118c438f96bb466e83108506d03d5c31b1bd2387e83e5b070bda6ded9c37", size = 5317811, upload-time = "2026-09-02T14:49:55.25Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b2/0c397588174403c2ab68fc464abf97e03e7324f9c6cb6a99023104707195/lxml-6.1.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:1beb0f9909b26cee938df9ba56b15252a84429b1fc30ce6fca161390b9789a70", size = 4646516, upload-time = "2026-09-02T14:49:57.761Z" },
{ url = "https://files.pythonhosted.org/packages/56/7e/cfea25afafbe49db8b225764f7f74bb37c2a7f5e717d917d3d4a5e098ed4/lxml-6.1.3-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a27ac6c780c8b8a1cd231b58407634cafc1c4cc28cd6c7141362df0f36351e7", size = 5240626, upload-time = "2026-09-02T14:50:00.279Z" },
{ url = "https://files.pythonhosted.org/packages/a1/75/7a587771bb52ebb0e2c57b6dbe9fd96a70fbb54d72ddd97d54c5f8ec18d5/lxml-6.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a1932d7ce78a561367512c594fe66eac2b2ec9b9264cfd9b5f950622f4a116e2", size = 5086619, upload-time = "2026-09-02T14:50:03.245Z" },
{ url = "https://files.pythonhosted.org/packages/1e/01/94c0ebe6d831861542d251e038052e52bf6d33f1d18f1cfffdc82851065a/lxml-6.1.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7d0f5976aa2701996f759b30172925829867547bb073af0ae67d1307a0f0262c", size = 4758828, upload-time = "2026-09-02T14:50:05.873Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f1/938d67bd0e5b1fdfa52be28aefdffbad57e1f6b8e921c2aab88542c75f40/lxml-6.1.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e7ce578aa8a80910a72a8ca0bbea3baae10100827249001999726a788456d8", size = 5627083, upload-time = "2026-09-02T14:50:08.555Z" },
{ url = "https://files.pythonhosted.org/packages/d8/65/4e51522f6c214650db0abb7b16ccd11b1238b8a05a8d59aa4ebed59c9f67/lxml-6.1.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d97c5227621af74b111882a290b10f371780a38eef9d9e730408fba2259b52fb", size = 5235170, upload-time = "2026-09-02T14:50:11.255Z" },
{ url = "https://files.pythonhosted.org/packages/92/c2/e73d19365665f6b16ef84df21199befc3b06e4c539046ad2d9595f6fb9ea/lxml-6.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da707f14ea3c35ee463d50acd596d6488e4b2b4ae7cf77a5bf93f55c023d63e8", size = 5252273, upload-time = "2026-09-02T14:50:13.782Z" },
{ url = "https://files.pythonhosted.org/packages/48/a9/7f386c84c9fe2854e1ca6e231c285e1c8f392971ac353c6865e6ec49faff/lxml-6.1.3-cp314-cp314t-win32.whl", hash = "sha256:9efe56a68179f3adc4de41861c9358931db03837c48dd5e1c78077b84dd07f3a", size = 3902712, upload-time = "2026-09-02T14:50:16.171Z" },
{ url = "https://files.pythonhosted.org/packages/82/a6/8a3eb793f7900ef01c7f99e6f5fcbcfbdff35251cfaef66b32a4c16352d6/lxml-6.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c9389b3784b56c58d933b5e0aecdf28f901b073ff385358d8a7d40907f6e14b2", size = 4400979, upload-time = "2026-09-02T14:50:18.621Z" },
{ url = "https://files.pythonhosted.org/packages/cc/c4/3807bea283b4fe9e9d9f5dde46a73df91178472b335d2778e10b2a37aa22/lxml-6.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32a409be3190b088f960ac92bfedfbef2f86c49ff940765e1548177592d20026", size = 3823401, upload-time = "2026-09-02T14:50:21.119Z" },
{ url = "https://files.pythonhosted.org/packages/e1/8e/4614fcd65496054cfb7172662f3576a59200278739506433b8c241ea422a/lxml-6.1.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6ea2f13dce778ca072ccee598bca46a092ce192e8fd907b6c1f0e52c800529a0", size = 8609378, upload-time = "2026-09-02T14:50:31.772Z" },
{ url = "https://files.pythonhosted.org/packages/f2/51/2cdce3c65fa99a6195dd8fbd512d33407c1000ad99f63e0a285b63d7a8eb/lxml-6.1.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c581b1d68b3845fb86c6b2983e755b29bf001461c59fa411d2c26a911b6559a9", size = 4640022, upload-time = "2026-09-02T14:50:34.41Z" },
{ url = "https://files.pythonhosted.org/packages/52/09/0b30084e9eb1c546a4be3d9c56df70058d116b1a320400a59b0f7da87bf0/lxml-6.1.3-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e01125896585139453cab8cb235893644d8815d7509520da95ae3ee8d1c1f79", size = 5037928, upload-time = "2026-09-02T14:50:37.007Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0e/5c37275a3e361f6138dc06db748ea565c1fe8a5f4ee5e2ddd80047c81a89/lxml-6.1.3-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:290f66b97ede0e552e1cb44a0fd8a74f9753ee635b50830a0b122fb72788d015", size = 5661932, upload-time = "2026-09-02T14:50:39.777Z" },
{ url = "https://files.pythonhosted.org/packages/70/c5/b71ffb289b15e2642e2a3cf6d468c44da39ea119061a99e5b05e3d10f217/lxml-6.1.3-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73fc05988ed20809450474ba760a87c8ad4e455fc09783c02195e56ec634b41a", size = 5249209, upload-time = "2026-09-02T14:50:42.141Z" },
{ url = "https://files.pythonhosted.org/packages/81/ea/9910da149a23932f9301652e57661cd9e42b0df18f12be21159b7255f92b/lxml-6.1.3-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:dc3a44689eea43eab836e5c98a8ab015dc2419987d1ea6eafc7c590cdff86bed", size = 4704543, upload-time = "2026-09-02T14:50:44.634Z" },
{ url = "https://files.pythonhosted.org/packages/76/07/9290329cd188c62e22021f79df04ee0cc33d9a93b0d38bd65ccd452ad9d0/lxml-6.1.3-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:209c3ccbfe35a04ac6d24f0611f9d1cbf8025d49991b14acd935236234d6c156", size = 5261298, upload-time = "2026-09-02T14:50:47.301Z" },
{ url = "https://files.pythonhosted.org/packages/c9/0c/aba78bd3401cd99b73a0aed8e2b9b43e14be94fab3603d4bbc8a62365f2a/lxml-6.1.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2f5b2a2b9811b853b39bfa41367c6d78747b8e3e80e07fc5a24aae295c1a4d7d", size = 5090453, upload-time = "2026-09-02T14:50:49.952Z" },
{ url = "https://files.pythonhosted.org/packages/8d/dc/fa4426c3355aa0216cbeb3911495b5f65a26e0df85859a89928fe28f0396/lxml-6.1.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6a406d0b3cb207b0fa460ed4dc93e866f44f105da0169361cb18ff998a44c7f0", size = 4744709, upload-time = "2026-09-02T14:50:52.394Z" },
{ url = "https://files.pythonhosted.org/packages/be/2b/224fe7918658ab7c532ac2412f3c1eb28f71e6364fb07566262d0cc6a7b6/lxml-6.1.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:53258656846f5c48996b882fb4b135885e088a3ad3d96b4bc0530f95124d1f69", size = 5685802, upload-time = "2026-09-02T14:50:55.043Z" },
{ url = "https://files.pythonhosted.org/packages/21/44/7d480819b9adcae5f84dd8ac529132c6b7a578544398225cd20321adcd91/lxml-6.1.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:aa633613ff907ea91b9b0489a1f0da1b8725d8c6ccec6b77e8a1c9c235044bb0", size = 5249019, upload-time = "2026-09-02T14:50:57.985Z" },
{ url = "https://files.pythonhosted.org/packages/72/83/385a267ea1b6b283f2249dd827ef360a295e9db14e13ef4665a120c60d64/lxml-6.1.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:90f709b9accab6b2e4d14f5c8718203877a0486bcb3afd74d8b539ecd1e961d4", size = 5271886, upload-time = "2026-09-02T14:51:01.667Z" },
{ url = "https://files.pythonhosted.org/packages/d8/0d/f967b0eb172ae876855a402d6d9b11fa86e3e0c89ca9bbfeadf7ffbfa719/lxml-6.1.3-cp315-cp315-win32.whl", hash = "sha256:b4fc6b03b9d9d90557274f571ab30e7fbbfc527955536935d96f98b6817a86e4", size = 3662894, upload-time = "2026-09-02T14:51:45.173Z" },
{ url = "https://files.pythonhosted.org/packages/f4/48/d8a8c4160a29e663109ad520bac2deb37fcd014756d024561e8bc3e611ec/lxml-6.1.3-cp315-cp315-win_amd64.whl", hash = "sha256:33cadd956b667997e4de1635fce9541f2e8ede2038fcde8cf55aa14d571d1bad", size = 4074626, upload-time = "2026-09-02T14:51:47.77Z" },
{ url = "https://files.pythonhosted.org/packages/25/20/3e1395d34d19f9254625d0b567b81cf70d37d3417be074f4d63b94a2be3c/lxml-6.1.3-cp315-cp315-win_arm64.whl", hash = "sha256:8a330c0ee5fa318c7b5cbbaad882baeca3f570357e7eb25ab34bf31008150758", size = 3749495, upload-time = "2026-09-02T14:51:50.663Z" },
{ url = "https://files.pythonhosted.org/packages/8f/c6/7465ffd9c43883526a382df6fa4846c9d8d419214f7effbf65270e795471/lxml-6.1.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0bf5a3e397df2ec4258eb5eea4c1ac6cf013ca1abd04a176903bff20a70021fe", size = 8857677, upload-time = "2026-09-02T14:51:05.109Z" },
{ url = "https://files.pythonhosted.org/packages/ed/eb/1f3a917e299df43c8162c3e6f64fc2cea3bcf277910f35bff5b8e5d39901/lxml-6.1.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:13d22c0d57355366b393936acf6b98a5e0edeadddd3fccbc6a846c50a76b8741", size = 4754522, upload-time = "2026-09-02T14:51:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/d7/f9/f81b4bdb6efb7a596be29603d8758154d00a5f545db9f3cef9d9041c8f64/lxml-6.1.3-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad7617727a96d189bd6f979d0fadf765198c7934e85f4edaba9bf3ad919a300", size = 5033744, upload-time = "2026-09-02T14:51:10.633Z" },
{ url = "https://files.pythonhosted.org/packages/c8/0f/26d9bfaacb319c86e0eca8a1a0bf1130d36a7afbd318883e23caea63763d/lxml-6.1.3-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cae82b5ca24b0c2beedb269f6e2a96f466acd926879ab00ae19f1a65cbf9ffb0", size = 5615269, upload-time = "2026-09-02T14:51:13.357Z" },
{ url = "https://files.pythonhosted.org/packages/5d/90/73675f3f4141350ed65d6fec533b107d4e802c5caa340cf111771edd86e0/lxml-6.1.3-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69cafd61aea04ebb3502c93c2aaa568b12931ca0802231e0b5de76bf8b6e74bd", size = 5236280, upload-time = "2026-09-02T14:51:16.051Z" },
{ url = "https://files.pythonhosted.org/packages/fd/be/ed260767e7977de463a0f91f3f4fffcab85c0a2a024a21ffe1fa442c2c79/lxml-6.1.3-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:dc205732d593118cf701d986f40e9de7801bb2e371cb189ddbda9b7348f4d97e", size = 4650718, upload-time = "2026-09-02T14:51:19.102Z" },
{ url = "https://files.pythonhosted.org/packages/d0/fd/e9839d03b1e767f2725cf7d7d81b80d5f3f9fdc10ad8827e2479311b046e/lxml-6.1.3-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88e719b9437f148f7e1465df845c758dd1598618cbea3a2fd1e61a715542f2b2", size = 5243376, upload-time = "2026-09-02T14:51:21.606Z" },
{ url = "https://files.pythonhosted.org/packages/34/a5/4606e347e2788c301f677004aa83e28d24da9fe663a24380122af57be6fc/lxml-6.1.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40983eabefd13da003e68170928c7acc011f0d095eefce5871a3c71c9385fb9a", size = 5092340, upload-time = "2026-09-02T14:51:24.21Z" },
{ url = "https://files.pythonhosted.org/packages/ea/99/3314a8661cdf30f493c55a87db283961dfaae08451976a2ca418958e1804/lxml-6.1.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:fad67b12ffe0f71e02b4932b04883cbc76a9072bbd30731409d3523cf058b011", size = 4758768, upload-time = "2026-09-02T14:51:26.813Z" },
{ url = "https://files.pythonhosted.org/packages/30/58/3bdc577f78ea8b7d72d39a84506f7001d5b28728f43e5b84891e3b7d9a4a/lxml-6.1.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd11e7550d89e551a87dcec30f04b1fca32e86b68708aa01a4daa455d8605e5", size = 5649546, upload-time = "2026-09-02T14:51:29.453Z" },
{ url = "https://files.pythonhosted.org/packages/6a/e4/652633de1a2395949ebb7a8fc7d089aba12a2b45f0fefbc9d29e3e3ab3cf/lxml-6.1.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ca0ec532ad2f5ba1e5ec120ac157769c57f01855b3d8bf37213f5d88abd9ba0a", size = 5234874, upload-time = "2026-09-02T14:51:32.262Z" },
{ url = "https://files.pythonhosted.org/packages/65/a6/c4581d171de30449304b4859bbd3607e9b40da13c0f88b68e6097c8d785e/lxml-6.1.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e99e09ab7741f1281e2677f4c0058c7f5267d182530b09c87e4f6aa26adf3887", size = 5260043, upload-time = "2026-09-02T14:51:34.841Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d7/ed6ee6186a89e69ca4ea9658b2a278f46a5efe8b5d4db56c7197f18653fe/lxml-6.1.3-cp315-cp315t-win32.whl", hash = "sha256:ace1d2c83b2bd24db5940600541140e87a325e119cb32d5fa9ad720d7e76648e", size = 3901093, upload-time = "2026-09-02T14:51:37.234Z" },
{ url = "https://files.pythonhosted.org/packages/67/9d/11d10257a4a048d04195d638bb61f0246ce2448eb05f682bcbab25a257a8/lxml-6.1.3-cp315-cp315t-win_amd64.whl", hash = "sha256:b49638355ea3bebba70da783ccbc630fd72afa16bc46c54474bfa1f9a915bbc6", size = 4395446, upload-time = "2026-09-02T14:51:39.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/b7/44edd7de434181c582892e68d1ffe6775ca403ce14aea07cb5a218a936cf/lxml-6.1.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5a721a98c649855963811b59b55755b30566e7f7fc40bdc9803d66dee9f811cf", size = 3822836, upload-time = "2026-09-02T14:51:42.471Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c1/2433176de263cc3f51fd2c303f993d5bb7f1da3139a0f7d168116c0bfa7a/lxml-6.1.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d2765c18ce303149ee804b1f3dad11232726dd0a702d73a15cf19179ac8cc962", size = 3942969, upload-time = "2026-09-02T14:46:36.55Z" },
{ url = "https://files.pythonhosted.org/packages/7c/71/de7759096f480180fd9e43ff7c017860e2d2a9a43741ab093cbdf1820f07/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d5a748d12dd9b535e0a130f60dae9ddf0adafbabe61e7864f55c7436c84547a", size = 4213008, upload-time = "2026-09-02T14:46:38.784Z" },
{ url = "https://files.pythonhosted.org/packages/b8/9b/c2d09af47a34fa6c0c27473083812b449a411680bd04bbe609cde291ddc8/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41096ec0740a58dad03d3ae0c7486d306d20becefb13ceb1649835ab3eb64167", size = 4322012, upload-time = "2026-09-02T14:46:41.031Z" },
{ url = "https://files.pythonhosted.org/packages/68/f3/bf56fee0403ebd995be8e78ec9aca566016487d1b3cbf755ebea8ccffbdb/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:415e3a115c0d510e329020012834d1c0aa1c581ee53a218603e38abbc1dea70a", size = 4257402, upload-time = "2026-09-02T14:46:43.134Z" },
{ url = "https://files.pythonhosted.org/packages/1c/1d/6da9cc086a20d9dd6bcbf7c5d9575f0331cca9a05e67dab02d15e828170b/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20428910dae17a1a93152a3ff2c0441d2f4932992c0797d65651dd0561f1792f", size = 4410889, upload-time = "2026-09-02T14:46:46.975Z" },
{ url = "https://files.pythonhosted.org/packages/03/5c/91fe48856f9f8089be3096fa4dbe4b3fb5526f3bf3e852ea9497f399cb9f/lxml-6.1.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bc8dd3d9c93e70c3df974a201ac2958b6d77b465d813c51d1f15fa8e645763ae", size = 3511258, upload-time = "2026-09-02T14:46:49.046Z" },
]
[[package]]
name = "networkx"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "tree-sitter"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f7/03/5600b84aff2e6c4fe80cfebb4063fe2f50299521befe5f6092ab8c082f4a/tree_sitter-0.26.0.tar.gz", hash = "sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245", size = 191423, upload-time = "2026-06-30T12:14:27.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/18/78aae7e4b5a36daaebb0276e4b07d084d45298758000787838e89329e11f/tree_sitter-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d6fe0e8fb4df77b5ee816228e2c4475a63d8cc1d4d3a7ffd7097b2b87fc3e95", size = 148679, upload-time = "2026-06-30T12:13:52.27Z" },
{ url = "https://files.pythonhosted.org/packages/24/e4/b371b9553b0e47d130fc2073e56cab94fecc868be04666bf5bbd1fcd1cc9/tree_sitter-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:514a9bf8993e5210e7970736aaf6020d1759b670e195ef17b1c48f586aa30736", size = 140759, upload-time = "2026-06-30T12:13:53.221Z" },
{ url = "https://files.pythonhosted.org/packages/22/7d/266fb0f2c41e6fb00b0f40e7a3338cdf99651e6a6511ca72bc78fc697636/tree_sitter-0.26.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10f0d4eb94aa7242dcb7f554bcd24dd7ba1c114f00d58759ba08c7a46c8ec51a", size = 637206, upload-time = "2026-06-30T12:13:54.334Z" },
{ url = "https://files.pythonhosted.org/packages/40/9f/47cf22febb47132d5b3a507a27bb99ef89fe5c8ec420a13c6daa9b64f782/tree_sitter-0.26.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8", size = 664758, upload-time = "2026-06-30T12:13:55.42Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4d/8d144ca3beb46a62a5102b6deac76bb0da55235c2c7840faf3b12f2e9d97/tree_sitter-0.26.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9997ba61368c48ed54e715676afadf703947a1542464e39d047764fb3624b01", size = 647438, upload-time = "2026-06-30T12:13:56.523Z" },
{ url = "https://files.pythonhosted.org/packages/4d/ed/ed1d6e78520c4fb64ed52fec3f2947bf8c1fbad7bc24e282c56193c9ba42/tree_sitter-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c56581ad256c4195a21bfe449fed5d44a02fe83a4a7d6e70e6ec302c881191c7", size = 661944, upload-time = "2026-06-30T12:13:57.82Z" },
{ url = "https://files.pythonhosted.org/packages/10/83/45f5bd43db1b8248d2fd08ef6cbe43e2725c539e09a2cfb8bc2818646788/tree_sitter-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754", size = 129496, upload-time = "2026-06-30T12:13:59.216Z" },
{ url = "https://files.pythonhosted.org/packages/f1/8d/be68e6c04563eb54145424cc83fe0aa8b0ba6c90d8989cf8a032671b5f16/tree_sitter-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:dea4b4e27d49e9ec5b785d4f994da000e6726882fcc6ad05ec98478500c71aef", size = 116484, upload-time = "2026-06-30T12:14:00.147Z" },
{ url = "https://files.pythonhosted.org/packages/87/ca/565702c44815393e3a973552ad546db4e5ca081ca8698640b4e93d809f51/tree_sitter-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c", size = 148934, upload-time = "2026-06-30T12:14:01.188Z" },
{ url = "https://files.pythonhosted.org/packages/54/6f/8bb61957f16ec1b1d92410a006cdc84a952b6352a7313b2ad299f2d21484/tree_sitter-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e", size = 140820, upload-time = "2026-06-30T12:14:02.087Z" },
{ url = "https://files.pythonhosted.org/packages/78/0a/8a6f08559182643a814a4ab559948ae817b2851890fd9b995a4fff6541ce/tree_sitter-0.26.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95", size = 638844, upload-time = "2026-06-30T12:14:03.428Z" },
{ url = "https://files.pythonhosted.org/packages/8a/2f/6e6781b31677231366cb3cf27bc8269157f6d4b03c9032865a4f5f2bbe7e/tree_sitter-0.26.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4", size = 667487, upload-time = "2026-06-30T12:14:04.669Z" },
{ url = "https://files.pythonhosted.org/packages/02/0b/0483078c8567445557a7015b0e5b187f6d7d4fda73464df9c4bdea7f7f3c/tree_sitter-0.26.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280", size = 647975, upload-time = "2026-06-30T12:14:05.797Z" },
{ url = "https://files.pythonhosted.org/packages/27/68/da83ca72c984e96ab4eb3bee0db1a6ffb5de1c8c455f92bd9f420cde7f0e/tree_sitter-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3", size = 665018, upload-time = "2026-06-30T12:14:07.278Z" },
{ url = "https://files.pythonhosted.org/packages/d1/36/4d67927fd47b89af4a00f65f55a7370e28778cd50e972c2430487e3ecc27/tree_sitter-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37", size = 129619, upload-time = "2026-06-30T12:14:08.373Z" },
{ url = "https://files.pythonhosted.org/packages/ed/72/cdefad523eb78710679c6da6a79e3d90f5afd32b1c6aa5a17bac7eef99f6/tree_sitter-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84", size = 116545, upload-time = "2026-06-30T12:14:09.273Z" },
{ url = "https://files.pythonhosted.org/packages/cb/b0/465257cf8f972ad9f9812ec1cbaa8ec210ebebb601ade9a15881aa2436b4/tree_sitter-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867", size = 148893, upload-time = "2026-06-30T12:14:10.541Z" },
{ url = "https://files.pythonhosted.org/packages/a1/ec/19d093e854b45e807fecfdd26105c266f43aeecc39c4dc97992a7074ad5a/tree_sitter-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab", size = 140829, upload-time = "2026-06-30T12:14:11.626Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ee/87e74671ed63a837e7a1f17ab94aa3913871e033b27523d8e7b83d6f7ad0/tree_sitter-0.26.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1", size = 639334, upload-time = "2026-06-30T12:14:12.836Z" },
{ url = "https://files.pythonhosted.org/packages/66/e7/f7e04cd9dff6b6ac0adf23922796fbc76accd4cf4bcda50542748d485679/tree_sitter-0.26.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7", size = 668102, upload-time = "2026-06-30T12:14:14.035Z" },
{ url = "https://files.pythonhosted.org/packages/d3/90/0bfb16b7894fea728c774a89d5af421a9368a2f913bbd4e8dcab7caaecfb/tree_sitter-0.26.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be", size = 648560, upload-time = "2026-06-30T12:14:15.302Z" },
{ url = "https://files.pythonhosted.org/packages/cd/e6/0fe05ba396e9623b0ae40ccf34171336b8701ec8d7bd0ee9f5224d638665/tree_sitter-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2", size = 665121, upload-time = "2026-06-30T12:14:16.403Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d2/a944b1ca35bed6068dc84a9967aaf3049d8cc0b7a36179eea8787270a6ab/tree_sitter-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f", size = 129615, upload-time = "2026-06-30T12:14:17.463Z" },
{ url = "https://files.pythonhosted.org/packages/09/ef/c7ca48293580d2249f36940c4eed5b4ddeb9ce75baf9a4ef30621987e0c7/tree_sitter-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564", size = 116525, upload-time = "2026-06-30T12:14:18.53Z" },
{ url = "https://files.pythonhosted.org/packages/c5/7a/4d84e6f6ae2c3e757490dd84de251712c31e293dfe31f28da1ec019cefa2/tree_sitter-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa", size = 148901, upload-time = "2026-06-30T12:14:19.452Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d9/efe62ec65dc9d096e834d27b8c058127e2146e42ff3380b822a233f016a6/tree_sitter-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3", size = 140805, upload-time = "2026-06-30T12:14:20.478Z" },
{ url = "https://files.pythonhosted.org/packages/c4/2c/c82326b7b97e3c485c18679883b16f89e5e913c639d3b219d3da70c9e67e/tree_sitter-0.26.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084", size = 640586, upload-time = "2026-06-30T12:14:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/e2/7a/f56e7d8282859452611024c7cbc623bfba5b24b8cb9b8f8bc88c5219fe9a/tree_sitter-0.26.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c", size = 668300, upload-time = "2026-06-30T12:14:22.728Z" },
{ url = "https://files.pythonhosted.org/packages/91/51/240ee81b9d5e9ca0a6cb1528e8605ffa70ab58c89ce126631be96d3e4bae/tree_sitter-0.26.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90", size = 649627, upload-time = "2026-06-30T12:14:23.819Z" },
{ url = "https://files.pythonhosted.org/packages/6a/54/760035cefedf9eb44f0f84c4ac22f1322e73155853e272576ee876336312/tree_sitter-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa", size = 664885, upload-time = "2026-06-30T12:14:25.064Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1b/0b36fe2a984ecedc4ce6aefd5d56447a6626a8e9b595c4e48658510ce8f8/tree_sitter-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c", size = 132688, upload-time = "2026-06-30T12:14:26.106Z" },
{ url = "https://files.pythonhosted.org/packages/4d/74/ebc041a13fbf40144afdb0d4b447e48e0b4012ca866c63de8b48f801f0c1/tree_sitter-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52", size = 120287, upload-time = "2026-06-30T12:14:26.991Z" },
]
[[package]]
name = "tree-sitter-c-sharp"
version = "0.23.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/7e2962bc1901daf264e7ce263b168e0139304a5f8f66c9b2baf20e550f87/tree_sitter_c_sharp-0.23.5.tar.gz", hash = "sha256:2635c7d5ec93e59f2e831b571bed99c4cc68a5d183a0994020aa769e1b990a71", size = 1147914, upload-time = "2026-04-14T16:11:22.441Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/c4/86d8d469400a856757a464a6ac01af97d8cdacbb595e62bdb98bf1e9db90/tree_sitter_c_sharp-0.23.5-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:61e1981cf21b09ee547b9c4c68e64fb4394325f8fc8d5f6d50d41471eba923ea", size = 333658, upload-time = "2026-04-14T16:11:11.288Z" },
{ url = "https://files.pythonhosted.org/packages/c8/13/593c8603f834eaf15082b81e079289fc9f062b4c0ab5b9489134084eec06/tree_sitter_c_sharp-0.23.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a75994a11f6fed3f5b8c36ad6a00e5dc43205bd912c43af3a2a54fdf649664eb", size = 376296, upload-time = "2026-04-14T16:11:12.972Z" },
{ url = "https://files.pythonhosted.org/packages/41/5a/a8855cbb5bbab28adb29c2c7f0e7be5a9f1d21450c13b3c3e613190d9b8c/tree_sitter_c_sharp-0.23.5-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aa88a780204cd153c4c1ae2d59c654cee1402212fa0d069823d6d34301587438", size = 358333, upload-time = "2026-04-14T16:11:14.214Z" },
{ url = "https://files.pythonhosted.org/packages/0a/c8/e0f391e343f5424d0627e3b6886c77baeb1249a3f10986be00b0b64ecdab/tree_sitter_c_sharp-0.23.5-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea38fb095d85d360dc5a0bec2fa605e496228876f798c9e089d5f0e72bcef46", size = 359448, upload-time = "2026-04-14T16:11:15.419Z" },
{ url = "https://files.pythonhosted.org/packages/6f/fc/10f807ac79f928241c5e0d827fdaf91e97dfba662fc7e07d7bd664140ec1/tree_sitter_c_sharp-0.23.5-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:05a9256415e7f24d4f133133794a9c224c60d19f677a04e2f6a94c25090b6d65", size = 358144, upload-time = "2026-04-14T16:11:17.087Z" },
{ url = "https://files.pythonhosted.org/packages/de/2a/6c3e12ef0cf09138717fcc02e1de8b76a3928d1bed65c7e3c2bd3172bcef/tree_sitter_c_sharp-0.23.5-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8636dc70b5a373c35c1036ed5de98e801f2e4d105ae41e2e20b6804c36e3bf33", size = 357525, upload-time = "2026-04-14T16:11:18.214Z" },
{ url = "https://files.pythonhosted.org/packages/2b/e0/bd287b092d611df95a9149117fd27b5947ce75527113d6898a4b4e2c8858/tree_sitter_c_sharp-0.23.5-cp310-abi3-win_amd64.whl", hash = "sha256:41a28cfa3d9ea50f5629e44550a03188c8fbd5079803dfc03554b6fd594b33fa", size = 338756, upload-time = "2026-04-14T16:11:19.661Z" },
{ url = "https://files.pythonhosted.org/packages/7f/fb/114ff43fdd256d0befed32f77c1dadee9517867181c70794571f718ed05c/tree_sitter_c_sharp-0.23.5-cp310-abi3-win_arm64.whl", hash = "sha256:2de4ebf95ddc2e92cd3105c8a8e0e7ec646bc82f52bfaf2f3acec0fa2401ec09", size = 337260, upload-time = "2026-04-14T16:11:20.849Z" },
]
[[package]]
name = "tree-sitter-python"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" },
{ url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" },
{ url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" },
{ url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" },
{ url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" },
{ url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" },
{ url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" },
]
[[package]]
name = "tree-sitter-typescript"
version = "0.23.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" },
{ url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" },
{ url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" },
{ url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" },
{ url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" },
{ url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" },
]