core/scripts/dump-api-schemas.py

80 lines
2.4 KiB
Python

"""
Dump workbench/schemas.py dataclass field shapes to a JSON snapshot.
Usage (from repo root):
uv run python scripts/dump-api-schemas.py
Output:
uv run python scripts/dump-api-schemas.py > workbench-ui/api-schema-snapshot.json
The snapshot is used by workbench-ui/src/types/api.test.ts to detect
TypeScript ↔ Python schema drift.
"""
from __future__ import annotations
import ast
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).parent.parent
SCHEMAS_PATH = REPO_ROOT / "workbench" / "schemas.py"
def annotation_to_str(node: ast.expr) -> str:
"""Convert an AST annotation node to a canonical string."""
return ast.unparse(node)
def extract_dataclasses(source: str) -> dict[str, dict[str, str]]:
"""Parse source and extract fields from @dataclass-decorated classes."""
tree = ast.parse(source)
result: dict[str, dict[str, str]] = {}
for node in ast.walk(tree):
if not isinstance(node, ast.ClassDef):
continue
# Check for @dataclass decorator (any form)
is_dataclass = any(
(isinstance(d, ast.Name) and d.id == "dataclass")
or (isinstance(d, ast.Attribute) and d.attr == "dataclass")
or (
isinstance(d, ast.Call)
and (
(isinstance(d.func, ast.Name) and d.func.id == "dataclass")
or (isinstance(d.func, ast.Attribute) and d.func.attr == "dataclass")
)
)
for d in node.decorator_list
)
if not is_dataclass:
continue
fields: dict[str, str] = {}
for stmt in node.body:
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
field_name = stmt.target.id
annotation = annotation_to_str(stmt.annotation)
fields[field_name] = annotation
result[node.name] = fields
return result
def main() -> None:
if not SCHEMAS_PATH.exists():
print(f"ERROR: {SCHEMAS_PATH} not found", file=sys.stderr)
sys.exit(1)
source = SCHEMAS_PATH.read_text(encoding="utf-8")
dataclasses = extract_dataclasses(source)
snapshot = {"dataclasses": {name: {"fields": fields} for name, fields in dataclasses.items()}}
print(json.dumps(snapshot, indent=2))
print(f"Dataclasses found: {', '.join(dataclasses.keys())}", file=sys.stderr)
if __name__ == "__main__":
main()