from __future__ import annotations import subprocess from pathlib import Path from typing import Callable from .scdm_backend import ScdmBackendInfo, resolve_scdm_backend, save_scdm_backend_cache, scdm_run_script_command from .scdm_schema import ScdmProbeJob, default_scdm_work_dir, file_fingerprint, read_json, utc_now, write_json def prepare_scdm_probe_job( step_path: str | Path, *, output_dir: str | Path | None = None, project_root: str | Path | None = None, backend: ScdmBackendInfo | None = None, unit: str = "model", scan_scope: str = "all", ) -> dict[str, object]: source = Path(step_path).expanduser() if not source.is_file(): return {"ok": False, "reason": "missing-step", "message": f"STEP file not found: {source}"} fingerprint = file_fingerprint(source) work_dir = Path(output_dir).expanduser() if output_dir else default_scdm_work_dir(source, project_root=project_root, fingerprint=fingerprint) work_dir = work_dir.resolve(strict=False) work_dir.mkdir(parents=True, exist_ok=True) job = ScdmProbeJob( step_path=source.resolve(strict=False), output_dir=work_dir, raw_features_path=work_dir / "scdm_raw_features.json", error_path=work_dir / "error.json", model_fingerprint=fingerprint, unit=unit, scan_scope=scan_scope, backend_path=str(backend.path) if backend else "", backend_version=backend.version if backend else "", ) job_path = work_dir / "scdm_probe_job.json" script_path = work_dir / "scdm_probe.py" write_json(job_path, job.to_payload()) script_path.write_text(generate_scdm_probe_script(job_path), encoding="utf-8") return { "ok": True, "reason": "ok", "work_dir": str(work_dir), "job_path": str(job_path), "script_path": str(script_path), "raw_features_path": str(job.raw_features_path), "error_path": str(job.error_path), "model_fingerprint": fingerprint, } def run_scdm_probe( step_path: str | Path, *, backend: ScdmBackendInfo | None = None, output_dir: str | Path | None = None, project_root: str | Path | None = None, timeout_seconds: float = 120.0, runner: Callable[..., subprocess.CompletedProcess[str]] | None = None, ) -> dict[str, object]: if backend is None: resolved = resolve_scdm_backend(project_root_override=project_root, validate=False) if not resolved.get("ok") or not isinstance(resolved.get("backend"), ScdmBackendInfo): return { "ok": False, "reason": str(resolved.get("reason") or "missing-scdm"), "message": str(resolved.get("message") or "SCDM backend is not available."), "backend_resolution": { "ok": bool(resolved.get("ok")), "reason": str(resolved.get("reason") or ""), "message": str(resolved.get("message") or ""), }, } backend = resolved["backend"] # type: ignore[assignment] prepared = prepare_scdm_probe_job(step_path, output_dir=output_dir, project_root=project_root, backend=backend) if not prepared.get("ok"): return {"backend": backend.to_cache(), **prepared} script_path = Path(str(prepared["script_path"])) raw_path = Path(str(prepared["raw_features_path"])) error_path = Path(str(prepared["error_path"])) command = scdm_run_script_command(backend.path, script_path) run = runner or subprocess.run try: completed = run( command, cwd=str(script_path.parent), capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=max(float(timeout_seconds), 0.1), creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), check=False, ) except subprocess.TimeoutExpired: write_json(error_path, {"ok": False, "reason": "timeout", "message": "SCDM probe timed out."}) return {"ok": False, "reason": "timeout", "message": "SCDM probe timed out.", "backend": backend.to_cache(), **prepared} except OSError as exc: write_json(error_path, {"ok": False, "reason": "launch-failed", "message": str(exc)}) return {"ok": False, "reason": "launch-failed", "message": str(exc), "backend": backend.to_cache(), **prepared} returncode = int(getattr(completed, "returncode", -1)) if returncode != 0: message = (str(getattr(completed, "stderr", "") or "") or str(getattr(completed, "stdout", "") or "")).strip() write_json( error_path, { "ok": False, "reason": "probe-failed", "returncode": returncode, "message": message, }, ) return {"ok": False, "reason": "probe-failed", "returncode": returncode, "message": message, "backend": backend.to_cache(), **prepared} if not raw_path.is_file(): write_json(error_path, {"ok": False, "reason": "missing-raw-output", "message": "SCDM probe did not write raw features."}) return { "ok": False, "reason": "missing-raw-output", "message": "SCDM probe did not write raw features.", "backend": backend.to_cache(), **prepared, } raw = read_json(raw_path) verified_backend = _probe_verified_backend(backend) try: save_scdm_backend_cache(verified_backend, project_root_override=project_root) except Exception: pass return {"ok": True, "reason": "ok", "raw": raw, "backend": verified_backend.to_cache(), **prepared} def _probe_verified_backend(backend: ScdmBackendInfo) -> ScdmBackendInfo: return ScdmBackendInfo( path=backend.path, source=backend.source, version=backend.version, verified_at=utc_now(), run_script_ok=True, license_ok=True, message="SCDM probe completed.", ) def generate_scdm_probe_script(job_path: str | Path) -> str: job_literal = repr(str(Path(job_path).expanduser())) return ( "from __future__ import print_function\n" "import json\n" "import traceback\n" f"JOB_PATH = {job_literal}\n" "\n" "def _write_json(path, payload):\n" " handle = open(path, 'w')\n" " try:\n" " handle.write(json.dumps(payload, indent=2))\n" " finally:\n" " handle.close()\n" "\n" "def _safe_name(value):\n" " try:\n" " return type(value).__name__\n" " except Exception:\n" " return ''\n" "\n" "def _float_attr(value, names):\n" " for name in names:\n" " try:\n" " result = getattr(value, name)\n" " return float(result)\n" " except Exception:\n" " pass\n" " return None\n" "\n" "def _xyz(value):\n" " if value is None:\n" " return []\n" " result = []\n" " for name in ('X', 'Y', 'Z'):\n" " try:\n" " result.append(float(getattr(value, name)))\n" " except Exception:\n" " return []\n" " return result\n" "\n" "def _items(collection):\n" " if collection is None:\n" " return []\n" " try:\n" " return list(collection)\n" " except Exception:\n" " items = []\n" " try:\n" " count = int(collection.Count)\n" " for index in range(count):\n" " items.append(collection[index])\n" " except Exception:\n" " pass\n" " return items\n" "\n" "def _geometry_from_face(face):\n" " geometry = {}\n" " surface = None\n" " for expr in ('Shape.Geometry', 'Geometry', 'Surface'):\n" " try:\n" " current = face\n" " for part in expr.split('.'):\n" " current = getattr(current, part)\n" " surface = current\n" " break\n" " except Exception:\n" " pass\n" " surface_name = _safe_name(surface)\n" " geometry['surfaceType'] = surface_name\n" " lowered = surface_name.lower()\n" " radius = _float_attr(surface, ('Radius', 'radius'))\n" " if radius is not None:\n" " geometry['radius'] = radius\n" " geometry['diameter'] = radius * 2.0\n" " try:\n" " geometry['center'] = _xyz(surface.Frame.Origin)\n" " except Exception:\n" " pass\n" " try:\n" " geometry['axis'] = _xyz(surface.Frame.DirZ)\n" " except Exception:\n" " pass\n" " try:\n" " center = geometry.get('center') or []\n" " axis = geometry.get('axis') or []\n" " if len(center) == 3 and len(axis) == 3:\n" " geometry['planeOffset'] = center[0] * axis[0] + center[1] * axis[1] + center[2] * axis[2]\n" " except Exception:\n" " pass\n" " if 'plane' in lowered:\n" " geometry['surfaceType'] = 'plane'\n" " elif 'cylinder' in lowered:\n" " geometry['surfaceType'] = 'cylinder'\n" " round_info = _round_info_from_face(face, geometry)\n" " if round_info:\n" " geometry['roundInfo'] = round_info\n" " return geometry\n" "\n" "def _round_info_from_face(face, geometry):\n" " if str(geometry.get('surfaceType', '')).lower() != 'cylinder':\n" " return {}\n" " round_info_type = globals().get('RoundInfo')\n" " if round_info_type is None:\n" " return {}\n" " try:\n" " info = round_info_type.Create(face)\n" " except Exception:\n" " return {}\n" " payload = {'available': True, 'type': _safe_name(info)}\n" " for attr in ('Radius', 'RoundRadius', 'ConstantRadius'):\n" " value = _float_attr(info, (attr, attr[0].lower() + attr[1:]))\n" " if value is not None:\n" " payload['radius'] = value\n" " payload['diameter'] = value * 2.0\n" " break\n" " for attr in ('IsConstant', 'IsRound'):\n" " try:\n" " payload[attr[0].lower() + attr[1:]] = bool(getattr(info, attr))\n" " except Exception:\n" " pass\n" " try:\n" " payload['attributes'] = [name for name in dir(info) if not name.startswith('_')][:40]\n" " except Exception:\n" " pass\n" " return payload\n" "\n" "def _path_value(value, expr):\n" " current = value\n" " for part in expr.split('.'):\n" " try:\n" " current = getattr(current, part)\n" " except Exception:\n" " return None\n" " return current\n" "\n" "def _first_path_value(value, exprs):\n" " for expr in exprs:\n" " result = _path_value(value, expr)\n" " if result is not None:\n" " return result\n" " return None\n" "\n" "def _geometry_from_edge(edge):\n" " geometry = {}\n" " shape = getattr(edge, 'Shape', edge)\n" " curve = _first_path_value(edge, ('Shape.Geometry', 'Geometry', 'Shape.Curve', 'Curve', 'Shape')) or shape\n" " geometry['curveShapeType'] = _safe_name(shape)\n" " geometry['curveType'] = _safe_name(curve)\n" " length = _float_attr(edge, ('Length', 'length'))\n" " if length is None:\n" " length = _float_attr(shape, ('Length', 'length'))\n" " if length is not None:\n" " geometry['length'] = length\n" " start = _xyz(_first_path_value(edge, ('StartPoint', 'Shape.StartPoint')))\n" " end = _xyz(_first_path_value(edge, ('EndPoint', 'Shape.EndPoint')))\n" " if start:\n" " geometry['startPoint'] = start\n" " if end:\n" " geometry['endPoint'] = end\n" " if len(start) == 3 and len(end) == 3:\n" " geometry['midPoint'] = [(start[i] + end[i]) * 0.5 for i in range(3)]\n" " radius = _float_attr(curve, ('Radius', 'radius'))\n" " if radius is not None:\n" " geometry['radius'] = radius\n" " geometry['diameter'] = radius * 2.0\n" " center = _xyz(_first_path_value(curve, ('Frame.Origin', 'Circle.Frame.Origin')))\n" " if center:\n" " geometry['center'] = center\n" " axis = _xyz(_first_path_value(curve, ('Frame.DirZ', 'Circle.Frame.DirZ')))\n" " if axis:\n" " geometry['axis'] = axis\n" " return geometry\n" "\n" "def _edge_adjacent_face_ordinals(edge, face_ordinals_by_marker):\n" " faces = []\n" " for expr in ('Faces', 'Shape.Faces', 'GetFaces'):\n" " value = _path_value(edge, expr)\n" " if value is None and expr == 'GetFaces':\n" " value = _maybe_call(edge, 'GetFaces')\n" " faces = _items(value)\n" " if faces:\n" " break\n" " ordinals = []\n" " for face in faces:\n" " marker = str(id(face))\n" " if marker in face_ordinals_by_marker:\n" " ordinals.append(face_ordinals_by_marker[marker])\n" " return {'adjacentFaceCount': len(faces), 'adjacentFaceOrdinals': ordinals}\n" "\n" "def _int_or_none(value):\n" " try:\n" " return int(value)\n" " except Exception:\n" " return None\n" "\n" "def _edge_kind(geometry):\n" " curve_type = str(geometry.get('curveType', '') or geometry.get('curveShapeType', '')).lower()\n" " if geometry.get('radius') is not None or 'circle' in curve_type or 'arc' in curve_type:\n" " return 'circular'\n" " if 'line' in curve_type or 'segment' in curve_type:\n" " return 'linear'\n" " return 'other'\n" "\n" "def _add_edge_geometry_summary(summary, geometry):\n" " summary['totalEdgeCount'] = int(summary.get('totalEdgeCount', 0)) + 1\n" " kind = _edge_kind(geometry)\n" " kind_counts = summary.setdefault('edgeKindCounts', {})\n" " kind_counts[kind] = int(kind_counts.get(kind, 0)) + 1\n" " radius = geometry.get('radius')\n" " if radius is not None:\n" " try:\n" " radius = float(radius)\n" " summary['circularEdgeCount'] = int(summary.get('circularEdgeCount', 0)) + 1\n" " values = summary.setdefault('circularRadii', [])\n" " if len(values) < 80:\n" " values.append(radius)\n" " except Exception:\n" " pass\n" " length = geometry.get('length')\n" " if length is not None:\n" " try:\n" " length = float(length)\n" " summary['minEdgeLength'] = min(float(summary.get('minEdgeLength', length)), length)\n" " summary['maxEdgeLength'] = max(float(summary.get('maxEdgeLength', length)), length)\n" " except Exception:\n" " pass\n" "\n" "def _final_edge_geometry_summary(summary):\n" " result = dict(summary)\n" " radii = result.get('circularRadii')\n" " if isinstance(radii, list) and radii:\n" " buckets = {}\n" " for value in radii:\n" " try:\n" " key = '%.6g' % float(value)\n" " buckets[key] = int(buckets.get(key, 0)) + 1\n" " except Exception:\n" " pass\n" " result['circularRadiusBuckets'] = [\n" " {'radius': key, 'count': buckets[key]} for key in sorted(buckets.keys())[:40]\n" " ]\n" " result.pop('circularRadii', None)\n" " return result\n" "\n" "def _record_face_adjacency(adjacency_map, body_index, edge_topology, geometry):\n" " ordinals = []\n" " for value in edge_topology.get('adjacentFaceOrdinals', []) or []:\n" " number = _int_or_none(value)\n" " if number is not None and number not in ordinals:\n" " ordinals.append(number)\n" " if len(ordinals) < 2:\n" " return\n" " ordinals.sort()\n" " kind = _edge_kind(geometry)\n" " for left_index in range(len(ordinals)):\n" " for right_index in range(left_index + 1, len(ordinals)):\n" " left = ordinals[left_index]\n" " right = ordinals[right_index]\n" " key = (body_index, left, right)\n" " item = adjacency_map.setdefault(\n" " key,\n" " {'bodyIndex': body_index, 'faceOrdinals': [left, right], 'edgeCount': 0, 'edgeKinds': {}, 'edges': []},\n" " )\n" " item['edgeCount'] = int(item.get('edgeCount', 0)) + 1\n" " edge_kinds = item.setdefault('edgeKinds', {})\n" " edge_kinds[kind] = int(edge_kinds.get(kind, 0)) + 1\n" " edges = item.setdefault('edges', [])\n" " if len(edges) < 6:\n" " edges.append({\n" " 'edgeOrdinal': edge_topology.get('edgeOrdinal'),\n" " 'globalEdgeOrdinal': edge_topology.get('globalEdgeOrdinal'),\n" " 'curveType': geometry.get('curveType'),\n" " 'kind': kind,\n" " 'length': geometry.get('length'),\n" " 'radius': geometry.get('radius'),\n" " })\n" "\n" "def _face_adjacency_rows(adjacency_map):\n" " rows = list(adjacency_map.values())\n" " rows.sort(key=lambda item: (int(item.get('bodyIndex') or 0), item.get('faceOrdinals') or []))\n" " return rows\n" "\n" "def _count_key(counts, key):\n" " key = str(key or '').strip() or 'unknown'\n" " counts[key] = int(counts.get(key, 0)) + 1\n" "\n" "def _feature_inventory(objects):\n" " result = {'objectTypeCounts': {}, 'surfaceTypeCounts': {}, 'curveTypeCounts': {}, 'operationCounts': {}}\n" " for item in objects:\n" " if not isinstance(item, dict):\n" " continue\n" " _count_key(result['objectTypeCounts'], item.get('objectType'))\n" " geometry = item.get('geometry')\n" " if not isinstance(geometry, dict):\n" " geometry = {}\n" " if geometry.get('surfaceType') is not None:\n" " _count_key(result['surfaceTypeCounts'], geometry.get('surfaceType'))\n" " if geometry.get('curveType') is not None:\n" " _count_key(result['curveTypeCounts'], geometry.get('curveType'))\n" " for command in item.get('backendCommandCandidates', []) or []:\n" " if isinstance(command, dict):\n" " _count_key(result['operationCounts'], command.get('operation'))\n" " return result\n" "\n" "def _command_candidates(object_type, geometry):\n" " surface_type = str(geometry.get('surfaceType', '')).lower()\n" " result = []\n" " if object_type == 'face' and surface_type == 'plane':\n" " result.append({'operation': 'pull_face_offset', 'enabled': True, 'parameterFields': {'distance': 0}})\n" " if object_type in ('face', 'hole') and surface_type == 'cylinder':\n" " result.append({'operation': 'change_hole_diameter', 'enabled': True, 'parameterFields': {'diameter': geometry.get('diameter')}})\n" " result.append({'operation': 'move_hole_axis', 'enabled': True, 'parameterFields': {'center': geometry.get('center')}})\n" " if object_type == 'hole' and surface_type == 'cylinder':\n" " result.append({'operation': 'fill_feature', 'enabled': True, 'parameterFields': {}})\n" " round_info = geometry.get('roundInfo')\n" " if isinstance(round_info, dict) and round_info.get('radius') is not None:\n" " result.append({'operation': 'change_round_radius', 'enabled': True, 'parameterFields': {'radius': round_info.get('radius')}})\n" " result.append({'operation': 'delete_round_or_chamfer', 'enabled': True, 'parameterFields': {}})\n" " return result\n" "\n" "def _open_step(path):\n" " errors = []\n" " for opener in ('DocumentOpen.Execute', 'Application.OpenDocument'):\n" " try:\n" " current = globals()\n" " target = None\n" " for part in opener.split('.'):\n" " target = current.get(part) if isinstance(current, dict) else getattr(current, part)\n" " current = target\n" " target(path)\n" " return\n" " except Exception as exc:\n" " errors.append(str(exc))\n" " raise Exception('Could not open STEP: ' + '; '.join(errors))\n" "\n" "def _root_part():\n" " try:\n" " return GetRootPart()\n" " except Exception:\n" " pass\n" " try:\n" " return Application.ActiveWindow.Document.MainPart\n" " except Exception:\n" " return None\n" "\n" "def _maybe_call(target, name):\n" " try:\n" " value = getattr(target, name)\n" " except Exception:\n" " return None\n" " try:\n" " return value()\n" " except Exception:\n" " return value\n" "\n" "def _body_faces(body):\n" " for name in ('Faces', 'GetFaces'):\n" " items = _items(_maybe_call(body, name))\n" " if items:\n" " return items\n" " return []\n" "\n" "def _body_edges(body):\n" " for name in ('Edges', 'GetEdges'):\n" " items = _items(_maybe_call(body, name))\n" " if items:\n" " return items\n" " return []\n" "\n" "def _child_parts(part):\n" " children = []\n" " for name in ('Components', 'GetAllComponents'):\n" " for component in _items(_maybe_call(part, name)):\n" " for attr in ('Content', 'ContentMaster', 'Template', 'Part'):\n" " try:\n" " value = getattr(component, attr)\n" " if value is not None:\n" " children.append(value)\n" " break\n" " except Exception:\n" " pass\n" " return children\n" "\n" "def _all_bodies(root):\n" " if root is None:\n" " return []\n" " for name in ('GetAllBodies', 'Bodies'):\n" " items = _items(_maybe_call(root, name))\n" " if items:\n" " return items\n" " bodies = []\n" " queue = [root]\n" " seen = set()\n" " while queue:\n" " part = queue.pop(0)\n" " marker = str(id(part))\n" " if marker in seen:\n" " continue\n" " seen.add(marker)\n" " bodies.extend(_items(_maybe_call(part, 'Bodies')))\n" " queue.extend(_child_parts(part))\n" " return bodies\n" "\n" "def _hole_face_markers(bodies):\n" " standard_holes = globals().get('StandardHoles')\n" " if standard_holes is None:\n" " return set()\n" " faces = []\n" " options = None\n" " options_cls = globals().get('FindStandardHoleOptions')\n" " if options_cls is not None:\n" " try:\n" " options = options_cls()\n" " except Exception:\n" " options = None\n" " identified = []\n" " find = getattr(standard_holes, 'Find', None)\n" " if find is not None:\n" " for args in ((bodies, options, None), (bodies, options), (options, None), (options,), (None,)):\n" " try:\n" " identified = _items(find(*args))\n" " if identified:\n" " break\n" " except Exception:\n" " pass\n" " if identified:\n" " try:\n" " faces = _items(standard_holes.GetHoleFaces(identified))\n" " except Exception:\n" " faces = []\n" " if not faces:\n" " for hole in identified:\n" " try:\n" " faces.extend(_items(getattr(hole, 'Faces')))\n" " except Exception:\n" " pass\n" " if faces:\n" " return set(str(id(face)) for face in faces)\n" " for args in ((bodies,), ()):\n" " try:\n" " faces = _items(standard_holes.GetHoleFaces(*args))\n" " if faces:\n" " break\n" " except Exception:\n" " pass\n" " return set(str(id(face)) for face in faces)\n" "\n" "def _available_commands():\n" " names = ('StandardHoles', 'OffsetFaces', 'Move', 'Fill', 'Delete', 'Chamfer', 'ConstantRound', 'RoundInfo')\n" " result = []\n" " for name in names:\n" " result.append({'name': name, 'available': globals().get(name) is not None})\n" " return result\n" "\n" "def main():\n" " job = json.load(open(JOB_PATH, 'r'))\n" " model = job.get('model', {})\n" " outputs = job.get('outputs', {})\n" " raw_path = outputs.get('rawFeatures')\n" " error_path = outputs.get('error')\n" " try:\n" " _open_step(model.get('path'))\n" " root = _root_part()\n" " bodies = _all_bodies(root)\n" " hole_face_markers = _hole_face_markers(bodies)\n" " objects = []\n" " face_adjacency = {}\n" " edge_geometry_summary = {}\n" " face_counter = 0\n" " edge_counter = 0\n" " for body_index, body in enumerate(bodies):\n" " body_faces = _body_faces(body)\n" " face_ordinals_by_marker = dict((str(id(face)), index) for index, face in enumerate(body_faces))\n" " for face_index, face in enumerate(body_faces):\n" " geometry = _geometry_from_face(face)\n" " object_type = 'hole' if str(id(face)) in hole_face_markers else 'face'\n" " if object_type == 'face' and isinstance(geometry.get('roundInfo'), dict) and geometry.get('roundInfo', {}).get('radius') is not None:\n" " object_type = 'round'\n" " objects.append({\n" " 'backendId': 'body:%d/face:%d' % (body_index, face_index),\n" " 'objectType': object_type,\n" " 'geometry': geometry,\n" " 'topologyHint': {'bodyIndex': body_index, 'faceOrdinal': face_index, 'globalFaceOrdinal': face_counter},\n" " 'backendCommandCandidates': _command_candidates(object_type, geometry),\n" " 'rawLimitations': [],\n" " })\n" " face_counter += 1\n" " for edge_index, edge in enumerate(_body_edges(body)):\n" " geometry = _geometry_from_edge(edge)\n" " edge_topology = {'bodyIndex': body_index, 'edgeOrdinal': edge_index, 'globalEdgeOrdinal': edge_counter}\n" " edge_topology.update(_edge_adjacent_face_ordinals(edge, face_ordinals_by_marker))\n" " _add_edge_geometry_summary(edge_geometry_summary, geometry)\n" " _record_face_adjacency(face_adjacency, body_index, edge_topology, geometry)\n" " objects.append({\n" " 'backendId': 'body:%d/edge:%d' % (body_index, edge_index),\n" " 'objectType': 'edge',\n" " 'geometry': geometry,\n" " 'topologyHint': edge_topology,\n" " 'backendCommandCandidates': [],\n" " 'rawLimitations': [],\n" " })\n" " edge_counter += 1\n" " payload = {\n" " 'schemaVersion': 1,\n" " 'backend': job.get('backend', {}),\n" " 'model': model,\n" " 'scan': job.get('scan', {}),\n" " 'objects': objects,\n" " 'diagnostics': {\n" " 'availableCommands': _available_commands(),\n" " 'faceAdjacency': _face_adjacency_rows(face_adjacency),\n" " 'edgeGeometrySummary': _final_edge_geometry_summary(edge_geometry_summary),\n" " 'featureInventory': _feature_inventory(objects),\n" " },\n" " 'summary': {'bodyCount': len(bodies), 'objectCount': len(objects), 'faceCount': face_counter, 'edgeCount': edge_counter, 'holeFaceCount': len(hole_face_markers)},\n" " }\n" " _write_json(raw_path, payload)\n" " except Exception as exc:\n" " _write_json(error_path, {'ok': False, 'reason': 'probe-exception', 'message': str(exc), 'traceback': traceback.format_exc()})\n" " raise\n" "\n" "main()\n" ) __all__ = [ "generate_scdm_probe_script", "prepare_scdm_probe_job", "run_scdm_probe", ]