Files
mail-intake/manage_docs.py

187 lines
4.9 KiB
Python

"""
MkDocs documentation management CLI.
This script provides a proper CLI interface to:
- Generate MkDocs Markdown files with mkdocstrings directives
- Build the documentation site
- Serve the documentation site locally
All operations are performed by calling MkDocs as a Python library
(no shell command invocation).
Requirements:
- mkdocs
- mkdocs-material
- mkdocstrings[python]
Usage:
python manage_docs.py generate
python manage_docs.py build
python manage_docs.py serve
python manage_docs.py build_mcp
Optional flags:
--docs-dir PATH Path to docs directory (default: ./docs)
--package-root NAME Root Python package name (default: mail_intake)
"""
import re
import argparse
from pathlib import Path
from typing import Iterable
from mkdocs.commands import build as mkdocs_build
from mkdocs.commands import serve as mkdocs_serve
from mkdocs.config import load_config
PROJECT_ROOT = Path(__file__).resolve().parent
DEFAULT_DOCS_DIR = PROJECT_ROOT / "docs"
DEFAULT_PACKAGE_ROOT = "mail_intake"
MKDOCS_YML = PROJECT_ROOT / "mkdocs.yml"
DEFAULT_MCP_DIR = PROJECT_ROOT / "mcp"
MKDOCSTRINGS_DIRECTIVE = re.compile(r"^:::\s+([a-zA-Z0-9_.]+)", re.MULTILINE)
# -------------------------
# Existing functionality
# -------------------------
def generate_docs_from_nav(
project_root: Path,
docs_root: Path,
package_root: str,
) -> None:
"""
Create and populate MkDocs Markdown files with mkdocstrings directives.
This function:
- Walks the Python package structure
- Mirrors it under the docs directory
- Creates missing .md files
- Creates index.md for packages (__init__.py)
- Overwrites content with ::: package.module
Examples:
mail_intake/__init__.py -> docs/mail_intake/index.md
mail_intake/config.py -> docs/mail_intake/config.md
mail_intake/adapters/__init__.py -> docs/mail_intake/adapters/index.md
mail_intake/adapters/base.py -> docs/mail_intake/adapters/base.md
"""
package_dir = project_root / package_root
if not package_dir.exists():
raise FileNotFoundError(f"Package not found: {package_dir}")
docs_root.mkdir(parents=True, exist_ok=True)
for py_file in package_dir.rglob("*.py"):
rel = py_file.relative_to(project_root)
if py_file.name == "__init__.py":
# Package → index.md
module_path = ".".join(rel.parent.parts)
md_path = docs_root / rel.parent / "index.md"
title = rel.parent.name.replace("_", " ").title()
else:
# Regular module → <module>.md
module_path = ".".join(rel.with_suffix("").parts)
md_path = docs_root / rel.with_suffix(".md")
title = md_path.stem.replace("_", " ").title()
md_path.parent.mkdir(parents=True, exist_ok=True)
content = f"""# {title}
::: {module_path}
"""
md_path.write_text(content, encoding="utf-8")
def load_mkdocs_config():
if not MKDOCS_YML.exists():
raise FileNotFoundError("mkdocs.yml not found at project root")
return load_config(str(MKDOCS_YML))
def cmd_generate(args: argparse.Namespace) -> None:
generate_docs_from_nav(
project_root=PROJECT_ROOT,
docs_root=args.docs_dir,
package_root=args.package_root,
)
def cmd_build(_: argparse.Namespace) -> None:
config = load_mkdocs_config()
mkdocs_build.build(config)
def cmd_serve(_: argparse.Namespace) -> None:
mkdocs_serve.serve(
config_file=str(MKDOCS_YML)
)
# -------------------------
# MCP generation
# -------------------------
def iter_markdown_files(docs_root: Path) -> Iterable[Path]:
yield from docs_root.rglob("*.md")
def extract_modules(md_file: Path) -> list[str]:
content = md_file.read_text(encoding="utf-8")
return MKDOCSTRINGS_DIRECTIVE.findall(content)
# -------------------------
# CLI
# -------------------------
def main() -> None:
parser = argparse.ArgumentParser(
prog="manage_docs.py",
description="Manage MkDocs documentation and MCP exports for the project",
)
parser.add_argument(
"--docs-dir",
type=Path,
default=DEFAULT_DOCS_DIR,
help="Path to the docs directory",
)
parser.add_argument(
"--package-root",
default=DEFAULT_PACKAGE_ROOT,
help="Root Python package name",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser(
"generate",
help="Generate Markdown files with mkdocstrings directives",
).set_defaults(func=cmd_generate)
subparsers.add_parser(
"build",
help="Build the MkDocs site",
).set_defaults(func=cmd_build)
subparsers.add_parser(
"serve",
help="Serve the MkDocs site locally",
).set_defaults(func=cmd_serve)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()