Complete Python API tutorial
This executable example builds the same handbook as the CLI tutorial. It shows where filesystem work remains the caller's responsibility and where Scribpy takes over, and walks through every stage of the end-user API in the order a real application would call it: scaffold, write content, validate, inspect findings programmatically, assemble, export twice, and add logging.
The complete runnable file is available as
examples/python-handbook.py. The sections
below explain it step by step, then a full self-contained script closes the
page. Run the complete example once from an empty directory:
python /path/to/scribpy/docs/examples/python-handbook.py
Overview
1. Initialize the project
from pathlib import Path
import scribpy
root = Path("handbook-api")
build = Path("build-api")
scribpy.init_skeleton(
root,
title="Team Handbook",
author="Documentation Team",
version="1.0.0",
)
init_skeleton creates root, scribpy.yml, and index.md. title is
mandatory and keyword-only; author and version are optional. It raises
ScaffoldCollisionError rather than replacing an existing root manifest —
wrap the call if the script might run twice against the same directory:
try:
scribpy.init_skeleton(root, title="Team Handbook")
except scribpy.ScaffoldCollisionError as error:
print(f"Skipping scaffold: project already exists at {error.path}")
Alternative: scaffold from an outline
Instead of a bare skeleton, you can derive the whole page tree from a headings-only Markdown outline. This is useful when a table of contents already exists (for example, sketched by a technical writer) and you want Scribpy to create the matching folders and stub pages:
outline = Path("outline.md")
outline.write_text(
"# Team Handbook\n"
"## Guide\n"
"### Installation\n"
"### Configuration\n"
"## Reference\n",
encoding="utf-8",
)
nodes = scribpy.parse_outline(outline, max_depth=4)
print(nodes[0].title, [child.title for child in nodes[0].children])
# Team Handbook ['Guide', 'Reference']
scribpy.init_from_outline(outline, root, max_depth=4)
Leaf headings become stub Markdown pages; headings with children become
directories with their own local scribpy.yml. parse_outline raises
OutlineValidationError (carrying a one-based line_number) for a
malformed outline, such as a heading level that skips a level. The rest of
this tutorial continues with init_skeleton, since it gives full control
over page content.
2. Write pages, manifests, and an image
Scribpy models can write Markdown, while ordinary pathlib handles YAML and
binary assets:
guide = root / "guide"
assets = root / "assets"
guide.mkdir()
assets.mkdir()
home = scribpy.MarkdownFile(
path=root / "index.md",
content=(
"# Welcome\n\n"
"\n\n"
"Read the [installation guide](guide/installation.md).\n"
),
)
home.write()
installation = scribpy.MarkdownFile(
path=guide / "installation.md",
content=(
"# Installation\n\n"
"Install the package in an isolated environment.\n\n"
"Steps:\n\n"
"- Create the environment.\n"
"- Install the package.\n"
),
)
installation.write()
# A tiny valid PNG used only to keep this example self-contained.
png = bytes.fromhex(
"89504e470d0a1a0a0000000d494844520000000100000001"
"08060000001f15c4890000000d4944415408d763f8cfc0f01f"
"00050001ff89993d1d0000000049454e44ae426082"
)
(assets / "logo.png").write_bytes(png)
(root / "scribpy.yml").write_text(
"""project:
title: Team Handbook
author: Documentation Team
version: 1.0.0
build:
toc: true
toc_depth: 3
heading_numbering:
enabled: true
order:
- index.md
- guide/
- assets/
""",
encoding="utf-8",
)
(guide / "scribpy.yml").write_text(
"""title: User guide
order:
- installation.md
""",
encoding="utf-8",
)
Image targets are still relative to the Markdown file containing them. The API does not change this rule.
MarkdownFile(path, content, encoding="utf-8") is a frozen dataclass: it does
not touch disk until .write() is called, and .write(path=None) creates
parent directories as needed, returning the path written. Read a page back
with MarkdownFile.from_path(path, *, encoding="utf-8").
Inspecting a page before writing it
Because MarkdownFile is a plain value, you can inspect and transform it
before committing anything to disk — useful for templated content:
draft = scribpy.MarkdownFile(
path=guide / "configuration.md",
content="# Configuration\n\nTODO: describe environment variables.\n",
)
published = draft.replace_text("TODO: describe", "Describes")
print(published.content)
published.write()
document = published.to_document()
print([ref.target for ref in document.image_references]) # []
replace_text and with_content return a new MarkdownFile rather than
mutating the original. to_document() produces a path-free MarkdownDocument
and eagerly extracts its image_references (as MarkdownImageReference
values with alt_text, target, optional title, and optional one-based
line/column).
3. Validate and inspect findings
report = scribpy.validate_project(root)
print(f"Markdown files: {report.markdown_count}")
print(f"Manifests: {report.manifest_count}")
if not report.is_valid:
scribpy.render_validation_report(report)
raise SystemExit(1)
warnings = report.by_severity(scribpy.DiagnosticSeverity.WARNING)
for warning in warnings:
print(warning.code, warning.path, warning.message)
validate_project turns expected manifest, decoding, filesystem, MkForge, and
collection problems into structured diagnostics. It does not hide unexpected
programming failures.
Reading a report programmatically
ProjectValidationReport is a plain dataclass, so an application can act on
it without printing anything — for example, gate a CI job, aggregate findings
across multiple projects, or convert diagnostics to another format:
report = scribpy.validate_project(root)
errors = report.by_severity(scribpy.DiagnosticSeverity.ERROR)
warnings = report.by_severity(scribpy.DiagnosticSeverity.WARNING)
summary = {
"root": str(report.root),
"is_valid": report.is_valid,
"markdown_count": report.markdown_count,
"manifest_count": report.manifest_count,
"error_count": len(errors),
"warning_count": len(warnings),
}
print(summary)
for finding in report.diagnostics:
location = f"{finding.path}:{finding.line}" if finding.path else "(project)"
print(f"[{finding.severity.value}] {finding.code} {location} — {finding.message}")
Each ProjectDiagnostic carries code, severity, message, and optional
path, line, column, category, and target. Project-wide problems
(for example, an unreadable root directory) have no path; source-level
problems (for example, a missing image) do.
Troubleshooting: turning a failed report into an exception
A common pattern at an application boundary is to fail fast with a precise error instead of continuing past a broken project:
report = scribpy.validate_project(root)
if not report.is_valid:
first_error = report.by_severity(scribpy.DiagnosticSeverity.ERROR)[0]
raise scribpy.InvalidMarkdownError(
f"{first_error.code} at {first_error.path}: {first_error.message}"
)
This is deliberately manual: validate_project never raises for expected
project-input problems, so your code decides whether "invalid" means "stop
the script," "log and continue," or "return an HTTP 422." by_severity
preserves finding order, so [0] is the first error encountered during
validation, not necessarily the most important one — inspect category and
code when a script needs to distinguish, say, manifest problems from
Markdown structure problems.
Collection rules alone
validate_project layers project-wide checks (manifests, encoding, MkForge)
on top of collection rules. If you already have a MarkdownCollection and
only want the collection-level checks (heading structure, links, images),
call diagnose() directly instead of revalidating the whole project:
collection = scribpy.MarkdownCollection.from_tree(root)
collection_report = collection.diagnose()
if collection_report.has_errors:
raise scribpy.InvalidMarkdownError(collection_report.summary())
4. Assemble Markdown
build.mkdir(exist_ok=True)
markdown_output = build / "handbook.md"
collection = scribpy.MarkdownCollection.from_tree(root)
scribpy.concatenate(collection, markdown_output)
print(markdown_output.read_text(encoding="utf-8")[:200])
MarkdownCollection.from_tree resolves traversal and manifest settings.
concatenate applies the build pipeline and writes collected images beside the
output under build-api/assets/.
Inspecting the collection before assembling
MarkdownCollection exposes its resolved manifest and file order, which is
useful to sanity-check before committing to a build:
collection = scribpy.MarkdownCollection.from_tree(root)
print(collection.manifest.project.get("title"))
print(collection.manifest.build.toc, collection.manifest.build.toc_depth)
for markdown_file in collection.files:
print(markdown_file.path.relative_to(root))
collection.concatenate() (the method, distinct from the top-level
scribpy.concatenate() function) performs only the in-memory structural
merge — one H1, folder headings, shifted source headings — and returns a
MarkdownDocument without touching disk or running the diagram/link/TOC
pipeline. The public scribpy.concatenate(collection, output) function
wraps this and additionally applies link rewriting, heading numbering, TOC
generation, diagram rendering, and image collection, then writes the result.
Prefer the module-level function unless you are specifically testing the
structural merge in isolation.
5. Export HTML
css = build / "handbook.css"
css.write_text("h1, h2 { color: #263c78; }\n", encoding="utf-8")
scribpy.html_export(
markdown_output,
build / "handbook.html",
toc_depth=3,
css=css,
)
The output parent must already exist. The HTML contains CSS and JavaScript but
still references copied image files, so retain build-api/assets/.
html_export raises FileNotFoundError if markdown_output or css does
not exist, and UnicodeDecodeError if either is not valid UTF-8.
6. Export MkDocs input
scribpy.mkdocs_export(root, build / "handbook-site")
This reads the source project again—it does not consume handbook.md. It keeps
the page tree and file links while rendering diagrams and collecting images.
ScaffoldCollisionError protects an existing destination mkdocs.yml.
7. Add logging around the build
logging_context is useful to trace exactly what Scribpy did during a build
without permanently configuring application-wide logging:
log_path = build / "scribpy.log"
with scribpy.logging_context(level="DEBUG", file=log_path, console=False):
collection = scribpy.MarkdownCollection.from_tree(root)
scribpy.concatenate(collection, markdown_output)
print(log_path.read_text(encoding="utf-8")[-500:])
console=False keeps the script's own stdout clean while every DEBUG-level
message still lands in scribpy.log. All handlers added by the context are
removed on exit, so calling logging_context again later (for example, to
capture only the MkDocs export) starts from a clean slate:
with scribpy.logging_context(level="INFO", file=build / "export.log"):
scribpy.mkdocs_export(root, build / "handbook-site")
8. Handle expected failures
At an application boundary, catch only failures you can explain or recover from:
try:
collection = scribpy.MarkdownCollection.from_tree(root)
scribpy.concatenate(collection, markdown_output)
except scribpy.InvalidScribpyManifestError as error:
print(f"Invalid manifest {error.path}: {error.detail}")
except scribpy.InvalidMarkdownError as error:
print(error.detail)
except (scribpy.PlantUmlRenderError, scribpy.MermaidRenderError) as error:
print(f"Diagram rendering failed: {error}")
except OSError as error:
print(f"Filesystem operation failed: {error}")
Avoid catching Exception: unexpected defects should remain visible.
Full script
The following mirrors examples/python-handbook.py
and can be copied into a .py file and run directly. It combines every stage
above — scaffold, write content, validate with a fail-fast check, assemble,
export HTML, export MkDocs, and a final logging pass — into one script:
"""Build the documentation tutorial project through Scribpy's Python API."""
from pathlib import Path
import scribpy
def main() -> None:
"""Create, validate, assemble, and export the tutorial handbook."""
root = Path("handbook-api")
build = Path("build-api")
scribpy.init_skeleton(
root,
title="Team Handbook",
author="Documentation Team",
version="1.0.0",
)
guide = root / "guide"
assets = root / "assets"
guide.mkdir()
assets.mkdir()
scribpy.MarkdownFile(
path=root / "index.md",
content=(
"# Welcome\n\n"
"\n\n"
"Read the [installation guide](guide/installation.md).\n"
),
).write()
scribpy.MarkdownFile(
path=guide / "installation.md",
content=(
"# Installation\n\n"
"Install the package in an isolated environment.\n\n"
"Steps:\n\n"
"- Create the environment.\n"
"- Install the package.\n"
),
).write()
png = bytes.fromhex(
"89504e470d0a1a0a0000000d494844520000000100000001"
"08060000001f15c4890000000d4944415408d763f8cfc0f01f"
"00050001ff89993d1d0000000049454e44ae426082"
)
(assets / "logo.png").write_bytes(png)
(root / "scribpy.yml").write_text(
"""project:
title: Team Handbook
author: Documentation Team
version: 1.0.0
build:
toc: true
toc_depth: 3
heading_numbering:
enabled: true
order:
- index.md
- guide/
- assets/
""",
encoding="utf-8",
)
(guide / "scribpy.yml").write_text(
"""title: User guide
order:
- installation.md
""",
encoding="utf-8",
)
report = scribpy.validate_project(root)
if not report.is_valid:
scribpy.render_validation_report(report)
raise SystemExit(1)
build.mkdir(exist_ok=True)
markdown_output = build / "handbook.md"
with scribpy.logging_context(level="INFO", file=build / "scribpy.log"):
collection = scribpy.MarkdownCollection.from_tree(root)
scribpy.concatenate(collection, markdown_output)
scribpy.html_export(markdown_output, build / "handbook.html")
scribpy.mkdocs_export(root, build / "handbook-site")
print("Created build-api/handbook.md, handbook.html, and handbook-site/")
if __name__ == "__main__":
main()
See the end-user API for the full function-by-function reference and the Python API reference for every exported name.