Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import re | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| import cadquery as cq | |
| from cadquery import exporters | |
| BLOCKED_TOKENS = [ | |
| "__", | |
| "open(", | |
| "exec(", | |
| "eval(", | |
| "compile(", | |
| "input(", | |
| "globals(", | |
| "getattr(", | |
| "setattr(", | |
| "delattr(", | |
| "subprocess", | |
| "socket", | |
| "requests", | |
| "urllib", | |
| "shutil", | |
| "pickle", | |
| "pathlib", | |
| "os.", | |
| "sys.", | |
| ] | |
| ALLOWED_IMPORT_LINES = { | |
| "import cadquery as cq", | |
| "from cadquery import exporters", | |
| "import math", | |
| } | |
| def clean_code(code: str) -> str: | |
| value = code.strip() | |
| value = re.sub(r"^```(?:python|py)?", "", value, flags=re.IGNORECASE).strip() | |
| value = re.sub(r"```$", "", value).strip() | |
| lines = [] | |
| for line in value.splitlines(): | |
| stripped = line.strip() | |
| if stripped in ALLOWED_IMPORT_LINES: | |
| continue | |
| if stripped.startswith("import ") or stripped.startswith("from "): | |
| raise ValueError(f"Unsupported import line: {stripped}") | |
| lines.append(line) | |
| return "\n".join(lines).strip() | |
| def validate_code(code: str) -> None: | |
| lowered = code.lower() | |
| for token in BLOCKED_TOKENS: | |
| if token in lowered: | |
| raise ValueError(f"Blocked Python token in CadQuery code: {token}") | |
| def exportable_object(namespace: dict[str, Any], captured: list[Any]) -> Any: | |
| if captured: | |
| return captured[-1] | |
| for name in ["fixture", "result", "model", "solid", "body", "part"]: | |
| if name in namespace: | |
| return namespace[name] | |
| raise ValueError("CadQuery code must assign the final object to fixture/result/model/solid/body/part or call show_object(obj).") | |
| def normalize_export_object(obj: Any) -> Any: | |
| if hasattr(obj, "toCompound"): | |
| return obj.toCompound() | |
| return obj | |
| def object_bbox(obj: Any): | |
| obj = normalize_export_object(obj) | |
| shape = obj.val() if hasattr(obj, "val") else obj | |
| return shape.BoundingBox() | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Run constrained CadQuery code and export STL.") | |
| parser.add_argument("--out-dir", required=True) | |
| parser.add_argument("--name", default="generated_cadquery") | |
| args = parser.parse_args() | |
| payload = json.loads(sys.stdin.read() or "{}") | |
| raw_code = str(payload.get("code", "")) | |
| code = clean_code(raw_code) | |
| validate_code(code) | |
| out_dir = Path(args.out_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| safe_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", args.name).strip("_") or "generated_cadquery" | |
| stl_path = out_dir / f"{safe_name}.stl" | |
| captured: list[Any] = [] | |
| def show_object(obj: Any, *args: Any, **kwargs: Any) -> None: | |
| captured.append(obj) | |
| safe_builtins = { | |
| "abs": abs, | |
| "bool": bool, | |
| "dict": dict, | |
| "enumerate": enumerate, | |
| "float": float, | |
| "int": int, | |
| "len": len, | |
| "list": list, | |
| "max": max, | |
| "min": min, | |
| "pow": pow, | |
| "range": range, | |
| "round": round, | |
| "set": set, | |
| "str": str, | |
| "sum": sum, | |
| "tuple": tuple, | |
| "zip": zip, | |
| } | |
| namespace: dict[str, Any] = { | |
| "__builtins__": safe_builtins, | |
| "cq": cq, | |
| "Assembly": cq.Assembly, | |
| "Color": cq.Color, | |
| "exporters": exporters, | |
| "math": math, | |
| "show_object": show_object, | |
| } | |
| safe_builtins["locals"] = lambda: namespace | |
| exec(code, namespace, namespace) | |
| obj = exportable_object(namespace, captured) | |
| export_obj = normalize_export_object(obj) | |
| exporters.export(export_obj, str(stl_path)) | |
| bbox = object_bbox(obj) | |
| print( | |
| json.dumps( | |
| { | |
| "name": safe_name, | |
| "stl_path": str(stl_path), | |
| "bounding_box": { | |
| "xlen": bbox.xlen, | |
| "ylen": bbox.ylen, | |
| "zlen": bbox.zlen, | |
| "xmin": bbox.xmin, | |
| "xmax": bbox.xmax, | |
| "ymin": bbox.ymin, | |
| "ymax": bbox.ymax, | |
| "zmin": bbox.zmin, | |
| "zmax": bbox.zmax, | |
| }, | |
| "cadquery_features": payload.get("features", []), | |
| "code": code, | |
| } | |
| ) | |
| ) | |
| if __name__ == "__main__": | |
| main() | |