194 lines
5.7 KiB
Python
194 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate API documentation from docstrings.
|
|
|
|
Usage:
|
|
python scripts/generate_docs.py
|
|
|
|
Generates docs/api-reference.md from source code docstrings.
|
|
"""
|
|
|
|
import ast
|
|
import inspect
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
|
|
def extract_class_info(class_node: ast.ClassDef, source_lines: List[str]) -> dict:
|
|
"""Extract class information including docstring and methods."""
|
|
info = {
|
|
"name": class_node.name,
|
|
"docstring": ast.get_docstring(class_node) or "",
|
|
"methods": [],
|
|
}
|
|
|
|
for node in class_node.body:
|
|
if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
|
|
method_info = {
|
|
"name": node.name,
|
|
"docstring": ast.get_docstring(node) or "",
|
|
"args": [],
|
|
"returns": "",
|
|
}
|
|
|
|
# Extract arguments
|
|
for arg in node.args.args:
|
|
if arg.arg not in ("self", "cls"):
|
|
method_info["args"].append(arg.arg)
|
|
|
|
info["methods"].append(method_info)
|
|
|
|
return info
|
|
|
|
|
|
def parse_module(file_path: Path) -> List[dict]:
|
|
"""Parse Python module and extract classes with methods."""
|
|
source = file_path.read_text()
|
|
source_lines = source.splitlines()
|
|
tree = ast.parse(source)
|
|
|
|
classes = []
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ClassDef):
|
|
classes.append(extract_class_info(node, source_lines))
|
|
|
|
return classes
|
|
|
|
|
|
def generate_api_reference(src_dir: Path, output_path: Path):
|
|
"""Generate API reference markdown from source code."""
|
|
|
|
# Parse client.py
|
|
client_path = src_dir / "client.py"
|
|
classes = parse_module(client_path)
|
|
|
|
# Generate markdown
|
|
md_lines = [
|
|
"# API Reference",
|
|
"",
|
|
"Auto-generated API documentation.",
|
|
"",
|
|
"Last updated: " + Path().cwd().name, # Will be updated with date
|
|
"",
|
|
"---",
|
|
"",
|
|
]
|
|
|
|
for cls in classes:
|
|
if not cls["docstring"]:
|
|
continue
|
|
|
|
md_lines.append(f"## {cls['name']}")
|
|
md_lines.append("")
|
|
md_lines.append(cls["docstring"])
|
|
md_lines.append("")
|
|
|
|
if cls["methods"]:
|
|
md_lines.append("### Methods")
|
|
md_lines.append("")
|
|
|
|
for method in cls["methods"]:
|
|
args_str = ", ".join(method["args"])
|
|
md_lines.append(f"#### `{method['name']}({args_str})`")
|
|
md_lines.append("")
|
|
|
|
if method["docstring"]:
|
|
md_lines.append(method["docstring"])
|
|
md_lines.append("")
|
|
|
|
md_lines.append("")
|
|
|
|
md_lines.append("---")
|
|
md_lines.append("")
|
|
|
|
# Parse models.py
|
|
models_path = src_dir / "models.py"
|
|
if models_path.exists():
|
|
model_classes = parse_module(models_path)
|
|
|
|
md_lines.append("")
|
|
md_lines.append("# Models")
|
|
md_lines.append("")
|
|
md_lines.append("Pydantic models used in API responses.")
|
|
md_lines.append("")
|
|
|
|
for cls in model_classes:
|
|
if cls["name"].startswith("_"):
|
|
continue
|
|
|
|
md_lines.append(f"## {cls['name']}")
|
|
md_lines.append("")
|
|
|
|
if cls["docstring"]:
|
|
md_lines.append(cls["docstring"])
|
|
md_lines.append("")
|
|
|
|
# Show fields
|
|
md_lines.append("### Fields")
|
|
md_lines.append("")
|
|
md_lines.append("| Field | Type | Description |")
|
|
md_lines.append("|-------|------|-------------|")
|
|
|
|
# Extract fields from class body
|
|
for node in ast.walk(ast.parse(models_path.read_text())):
|
|
if isinstance(node, ast.ClassDef) and node.name == cls["name"]:
|
|
for item in node.body:
|
|
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
field_name = item.target.id
|
|
if not field_name.startswith("_"):
|
|
md_lines.append(f"| `{field_name}` | - | - |")
|
|
|
|
md_lines.append("")
|
|
md_lines.append("---")
|
|
md_lines.append("")
|
|
|
|
# Parse errors.py
|
|
errors_path = src_dir / "errors.py"
|
|
if errors_path.exists():
|
|
error_classes = parse_module(errors_path)
|
|
|
|
md_lines.append("")
|
|
md_lines.append("# Errors")
|
|
md_lines.append("")
|
|
md_lines.append("Exception classes for error handling.")
|
|
md_lines.append("")
|
|
|
|
for cls in error_classes:
|
|
if cls["name"].startswith("_"):
|
|
continue
|
|
|
|
md_lines.append(f"## {cls['name']}")
|
|
md_lines.append("")
|
|
|
|
if cls["docstring"]:
|
|
md_lines.append(cls["docstring"])
|
|
md_lines.append("")
|
|
|
|
md_lines.append("---")
|
|
md_lines.append("")
|
|
|
|
# Write output
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text("\n".join(md_lines))
|
|
print(f"✅ Generated {output_path}")
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
import sys
|
|
|
|
# Paths
|
|
root_dir = Path(__file__).parent.parent
|
|
src_dir = root_dir / "src" / "kwork_api"
|
|
output_path = root_dir / "docs" / "api-reference.md"
|
|
|
|
if not src_dir.exists():
|
|
print(f"❌ Source directory not found: {src_dir}")
|
|
sys.exit(1)
|
|
|
|
generate_api_reference(src_dir, output_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|