80 lines
2.4 KiB
Python
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()
|