solidworks-mcp-python 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2052 @@
1
+ """Circuit breaker adapter for SolidWorks operations.
2
+
3
+ Implements the circuit breaker pattern to prevent cascading failures when SolidWorks
4
+ operations fail repeatedly.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import time
10
+ from collections.abc import Awaitable, Callable
11
+ from enum import Enum
12
+ from typing import Any, TypeVar, cast
13
+
14
+ from loguru import logger
15
+
16
+ from .base import (
17
+ AdapterHealth,
18
+ AdapterResult,
19
+ AdapterResultStatus,
20
+ ExtrusionParameters,
21
+ LoftParameters,
22
+ MassProperties,
23
+ RevolveParameters,
24
+ SolidWorksAdapter,
25
+ SolidWorksFeature,
26
+ SolidWorksModel,
27
+ SweepParameters,
28
+ )
29
+
30
+ T = TypeVar("T")
31
+
32
+
33
+ def _to_input_dict(params: Any) -> dict[str, Any]:
34
+ """Convert a Pydantic model or plain dict to a flat dict for SoC logging."""
35
+ if hasattr(params, "model_dump"):
36
+ return cast(dict[str, Any], params.model_dump())
37
+ return cast(dict[str, Any], params) if isinstance(params, dict) else {}
38
+
39
+
40
+ class CircuitState(Enum):
41
+ """Circuit breaker states.
42
+
43
+ Attributes:
44
+ CLOSED (Any): The closed value.
45
+ HALF_OPEN (Any): The half open value.
46
+ OPEN (Any): The open value.
47
+ """
48
+
49
+ CLOSED = "closed" # Normal operation
50
+ OPEN = "open" # Circuit is open, blocking requests
51
+ HALF_OPEN = "half_open" # Testing if service is back
52
+
53
+
54
+ class _BreakerBucket:
55
+ """Per-operation circuit breaker state.
56
+
57
+ Attributes:
58
+ state (CircuitState): The current state of this bucket.
59
+ failure_count (int): Consecutive failures recorded in ``CLOSED``/``OPEN``.
60
+ last_failure_time (float): ``time.time()`` of the most recent failure.
61
+ half_open_calls (int): Calls let through since entering ``HALF_OPEN``.
62
+ """
63
+
64
+ __slots__ = ("state", "failure_count", "last_failure_time", "half_open_calls")
65
+
66
+ def __init__(self) -> None:
67
+ self.state = CircuitState.CLOSED
68
+ self.failure_count = 0
69
+ self.last_failure_time: float = 0.0
70
+ self.half_open_calls = 0
71
+
72
+
73
+ class CircuitBreakerAdapter(SolidWorksAdapter):
74
+ """Circuit breaker wrapper for SolidWorks adapters.
75
+
76
+ Each named operation (``create_cut_extrude``, ``list_features``, ...) gets
77
+ its own independent breaker bucket, keyed by the ``operation_name`` passed
78
+ to ``_execute_with_circuit_breaker``. A run of failures on one tool (e.g.
79
+ every ``create_cut_extrude`` attempt failing while a wrong argument count
80
+ is being worked out) trips only that tool's breaker — unrelated read-only
81
+ tools like ``list_features`` or ``get_model_info`` keep working. Before
82
+ this, a single shared ``CircuitState`` meant one failing tool blocked
83
+ every other tool on the adapter, including reads, until the shared
84
+ recovery timeout elapsed.
85
+
86
+ Args:
87
+ adapter (SolidWorksAdapter | None): Adapter instance used for the operation.
88
+ Defaults to None.
89
+ failure_threshold (int): The failure threshold value. Defaults to 5.
90
+ recovery_timeout (int): The recovery timeout value. Defaults to 60.
91
+ half_open_max_calls (int): The half open max calls value. Defaults to 3.
92
+ config (dict[str, object] | None): Configuration values for the operation. Defaults
93
+ to None.
94
+
95
+ Attributes:
96
+ adapter (Any): The adapter value.
97
+ failure_threshold (Any): The failure threshold value.
98
+ half_open_max_calls (Any): The half open max calls value.
99
+ recovery_timeout (Any): The recovery timeout value.
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ adapter: SolidWorksAdapter | None = None,
105
+ failure_threshold: int = 5,
106
+ recovery_timeout: int = 60,
107
+ half_open_max_calls: int = 3,
108
+ config: dict[str, object] | None = None,
109
+ ) -> None:
110
+ """Initialize the circuit breaker adapter.
111
+
112
+ Args:
113
+ adapter (SolidWorksAdapter | None): Adapter instance used for the operation.
114
+ Defaults to None.
115
+ failure_threshold (int): The failure threshold value. Defaults to 5.
116
+ recovery_timeout (int): The recovery timeout value. Defaults to 60.
117
+ half_open_max_calls (int): The half open max calls value. Defaults to 3.
118
+ config (dict[str, object] | None): Configuration values for the operation. Defaults
119
+ to None.
120
+
121
+ Returns:
122
+ None: None.
123
+ """
124
+ if adapter is None:
125
+ from .mock_adapter import MockSolidWorksAdapter
126
+
127
+ adapter = MockSolidWorksAdapter(config or {})
128
+ super().__init__(config)
129
+ self.adapter = adapter
130
+ self.failure_threshold = failure_threshold
131
+ self.recovery_timeout = recovery_timeout
132
+ self.half_open_max_calls = half_open_max_calls
133
+
134
+ # Legacy/default bucket, used by connect() and the generic call() API.
135
+ self.state = CircuitState.CLOSED
136
+ self.failure_count = 0
137
+ self.last_failure_time: float = 0.0
138
+ self.half_open_calls = 0
139
+
140
+ # Per-operation buckets, keyed by operation_name, used by every named
141
+ # method that goes through _execute_with_circuit_breaker.
142
+ self._buckets: dict[str, _BreakerBucket] = {}
143
+
144
+ def _get_bucket(self, operation_name: str) -> _BreakerBucket:
145
+ """Get (creating if needed) the breaker bucket for one operation name.
146
+
147
+ Args:
148
+ operation_name (str): The operation name value.
149
+
150
+ Returns:
151
+ _BreakerBucket: That operation's independent breaker state.
152
+ """
153
+ bucket = self._buckets.get(operation_name)
154
+ if bucket is None:
155
+ bucket = _BreakerBucket()
156
+ self._buckets[operation_name] = bucket
157
+ return bucket
158
+
159
+ async def _invoke_with_optional_args(
160
+ self,
161
+ method: Callable[..., Awaitable[T]],
162
+ *args: object,
163
+ ) -> T:
164
+ """Invoke adapter method with args, retrying without args on signature mismatch.
165
+
166
+ Args:
167
+ method (Callable[..., Awaitable[T]]): The method value.
168
+ *args (object): Additional positional arguments forwarded to the call.
169
+
170
+ Returns:
171
+ T: The result produced by the operation.
172
+ """
173
+ try:
174
+ return await method(*args)
175
+ except TypeError:
176
+ return await method()
177
+
178
+ def _should_allow_request(self, bucket: Any = None) -> bool:
179
+ """Check if request should be allowed through circuit breaker.
180
+
181
+ Args:
182
+ bucket (Any): The breaker bucket to check. Defaults to ``self``
183
+ (the legacy/default bucket used by ``connect()``/``call()``).
184
+
185
+ Returns:
186
+ bool: True if should allow request, otherwise False.
187
+ """
188
+ if bucket is None:
189
+ bucket = self
190
+ if bucket.state == CircuitState.CLOSED:
191
+ return True
192
+ elif bucket.state == CircuitState.OPEN:
193
+ # Check if enough time has passed to try again
194
+ if time.time() - bucket.last_failure_time >= self.recovery_timeout:
195
+ bucket.state = CircuitState.HALF_OPEN
196
+ bucket.half_open_calls = 0
197
+ return True
198
+ return False
199
+ elif bucket.state == CircuitState.HALF_OPEN:
200
+ return bucket.half_open_calls < self.half_open_max_calls
201
+
202
+ def _record_success(self, bucket: Any = None) -> None:
203
+ """Record successful operation.
204
+
205
+ Args:
206
+ bucket (Any): The breaker bucket to update. Defaults to ``self``.
207
+
208
+ Returns:
209
+ None: None.
210
+ """
211
+ if bucket is None:
212
+ bucket = self
213
+ if bucket.state == CircuitState.HALF_OPEN:
214
+ # Reset circuit breaker on success in half-open state
215
+ bucket.state = CircuitState.CLOSED
216
+ bucket.failure_count = 0
217
+ bucket.half_open_calls = 0
218
+ elif bucket.state == CircuitState.CLOSED:
219
+ # Reset failure count on success
220
+ bucket.failure_count = 0
221
+
222
+ def _record_failure(self, bucket: Any = None) -> None:
223
+ """Record failed operation.
224
+
225
+ Args:
226
+ bucket (Any): The breaker bucket to update. Defaults to ``self``.
227
+
228
+ Returns:
229
+ None: None.
230
+ """
231
+ if bucket is None:
232
+ bucket = self
233
+ bucket.failure_count += 1
234
+ bucket.last_failure_time = time.time()
235
+
236
+ if bucket.state == CircuitState.HALF_OPEN:
237
+ # Go back to open state
238
+ bucket.state = CircuitState.OPEN
239
+ elif (
240
+ bucket.state == CircuitState.CLOSED
241
+ and bucket.failure_count >= self.failure_threshold
242
+ ):
243
+ # Open circuit breaker
244
+ bucket.state = CircuitState.OPEN
245
+ logger.warning(
246
+ f"Circuit breaker opened after {bucket.failure_count} failures"
247
+ )
248
+
249
+ async def _execute_with_circuit_breaker(
250
+ self,
251
+ operation_name: str,
252
+ operation: Callable[[], Awaitable[AdapterResult[T]]],
253
+ input_dict: dict[str, Any] | None = None,
254
+ error_results_are_failures: bool = True,
255
+ ) -> AdapterResult[T]:
256
+ """Build internal execute with circuit breaker.
257
+
258
+ Args:
259
+ operation_name (str): The operation name value.
260
+ operation (Callable[[], Awaitable[AdapterResult[T]]]): Callable object executed by
261
+ the helper.
262
+ input_dict (dict | None): Input parameters for SoC logging. Defaults to None.
263
+ error_results_are_failures (bool): When False, an ERROR result
264
+ returned by the operation counts as a healthy round trip, not a
265
+ breaker failure - only exceptions trip the breaker. For
266
+ diagnostic/lookup operations whose ERROR is an expected answer
267
+ ("the model has rebuild errors", "no such feature"); otherwise
268
+ they would lock themselves out exactly when needed.
269
+
270
+ Returns:
271
+ AdapterResult[T]: The result produced by the operation.
272
+ """
273
+ bucket = self._get_bucket(operation_name)
274
+ if not self._should_allow_request(bucket):
275
+ return AdapterResult(
276
+ status=AdapterResultStatus.ERROR,
277
+ error=f"Circuit breaker is {bucket.state.value} for {operation_name}",
278
+ metadata={"circuit_state": bucket.state.value},
279
+ )
280
+
281
+ if bucket.state == CircuitState.HALF_OPEN:
282
+ bucket.half_open_calls += 1
283
+
284
+ t0 = time.time()
285
+ try:
286
+ result = await operation()
287
+ latency_ms = (time.time() - t0) * 1000.0
288
+ if result.is_success or not error_results_are_failures:
289
+ self._record_success(bucket)
290
+ else:
291
+ self._record_failure(bucket)
292
+ self._soc_log(operation_name, input_dict, result, latency_ms)
293
+ return result
294
+ except Exception as e:
295
+ latency_ms = (time.time() - t0) * 1000.0
296
+ self._record_failure(bucket)
297
+ err_result: AdapterResult[T] = AdapterResult(
298
+ status=AdapterResultStatus.ERROR,
299
+ error=f"Circuit breaker caught exception in {operation_name}: {e}",
300
+ metadata={"circuit_state": bucket.state.value},
301
+ )
302
+ self._soc_log(operation_name, input_dict, err_result, latency_ms)
303
+ return err_result
304
+
305
+ def _soc_log(
306
+ self,
307
+ tool_name: str,
308
+ input_dict: dict[str, Any] | None,
309
+ result: AdapterResult[Any],
310
+ latency_ms: float,
311
+ ) -> None:
312
+ """Write a ToolCallRecord if soc_session_id is set."""
313
+ if not self.soc_session_id or input_dict is None:
314
+ return
315
+ try:
316
+ from solidworks_mcp.agents.history_db import insert_tool_call_record
317
+
318
+ output_data: dict[str, Any] = {}
319
+ if result.data is not None:
320
+ output_data = {"data": result.data}
321
+
322
+ insert_tool_call_record(
323
+ session_id=self.soc_session_id,
324
+ tool_name=tool_name,
325
+ input_json=json.dumps(input_dict, default=str),
326
+ output_json=json.dumps(output_data, default=str),
327
+ success=result.is_success,
328
+ latency_ms=latency_ms,
329
+ db_path=self.soc_db_path,
330
+ )
331
+ except Exception as exc:
332
+ logger.debug(
333
+ f"[soc_log] failed to write ToolCallRecord for {tool_name}: {exc}"
334
+ )
335
+
336
+ # Adapter interface implementation
337
+
338
+ async def connect(self) -> None:
339
+ """Connect through circuit breaker.
340
+
341
+ Returns:
342
+ None: None.
343
+
344
+ Raises:
345
+ Exception: If the operation cannot be completed.
346
+ """
347
+ if not self._should_allow_request():
348
+ raise Exception(f"Circuit breaker is {self.state.value}")
349
+
350
+ try:
351
+ await self.adapter.connect()
352
+ self._record_success()
353
+ except Exception:
354
+ self._record_failure()
355
+ raise
356
+
357
+ async def disconnect(self) -> None:
358
+ """Disconnect - always allowed.
359
+
360
+ Returns:
361
+ None: None.
362
+ """
363
+ await self.adapter.disconnect()
364
+
365
+ def is_connected(self) -> bool:
366
+ """Check connection status.
367
+
368
+ Returns:
369
+ bool: True if connected, otherwise False.
370
+ """
371
+ return self.adapter.is_connected()
372
+
373
+ async def health_check(self) -> AdapterHealth:
374
+ """Get health check with circuit breaker status.
375
+
376
+ ``circuit_breaker`` reports the legacy/default bucket (used by
377
+ ``connect()``), which is what "connection health" means here.
378
+ ``circuit_breaker_operations`` reports every per-tool bucket, so a
379
+ tripped tool-specific breaker is visible even while the connection
380
+ itself is healthy.
381
+
382
+ Returns:
383
+ AdapterHealth: The result produced by the operation.
384
+ """
385
+ base_health = await self.adapter.health_check()
386
+ if base_health.metrics is None:
387
+ base_health.metrics = {}
388
+ base_health.metrics["circuit_breaker"] = {
389
+ "state": self.state.value,
390
+ "failure_count": self.failure_count,
391
+ "last_failure_time": self.last_failure_time,
392
+ "half_open_calls": self.half_open_calls,
393
+ }
394
+ base_health.metrics["circuit_breaker_operations"] = {
395
+ name: {
396
+ "state": bucket.state.value,
397
+ "failure_count": bucket.failure_count,
398
+ "last_failure_time": bucket.last_failure_time,
399
+ "half_open_calls": bucket.half_open_calls,
400
+ }
401
+ for name, bucket in self._buckets.items()
402
+ }
403
+
404
+ # Consider circuit as unhealthy if the connection breaker or any
405
+ # per-operation breaker is open.
406
+ open_operations = [
407
+ name
408
+ for name, bucket in self._buckets.items()
409
+ if bucket.state == CircuitState.OPEN
410
+ ]
411
+ if self.state == CircuitState.OPEN:
412
+ base_health.healthy = False
413
+ base_health.connection_status = "circuit_breaker_open"
414
+ elif open_operations:
415
+ base_health.healthy = False
416
+ base_health.connection_status = (
417
+ f"circuit_breaker_open_for: {', '.join(sorted(open_operations))}"
418
+ )
419
+
420
+ return base_health
421
+
422
+ # Model operations
423
+
424
+ async def open_model(self, file_path: str) -> AdapterResult[SolidWorksModel]:
425
+ """Open model through circuit breaker.
426
+
427
+ Args:
428
+ file_path (str): Path to the target file.
429
+
430
+ Returns:
431
+ AdapterResult[SolidWorksModel]: The result produced by the operation.
432
+ """
433
+ return await self._execute_with_circuit_breaker(
434
+ "open_model",
435
+ lambda: self.adapter.open_model(file_path),
436
+ input_dict={"file_path": file_path},
437
+ )
438
+
439
+ async def close_model(self, save: bool = False) -> AdapterResult[None]:
440
+ """Close model through circuit breaker.
441
+
442
+ Args:
443
+ save (bool): The save value. Defaults to False.
444
+
445
+ Returns:
446
+ AdapterResult[None]: The result produced by the operation.
447
+ """
448
+ return await self._execute_with_circuit_breaker(
449
+ "close_model",
450
+ lambda: self.adapter.close_model(save),
451
+ input_dict={"save": save},
452
+ )
453
+
454
+ async def save_file(self, file_path: str | None = None) -> AdapterResult[None]:
455
+ """Save model through circuit breaker.
456
+
457
+ Args:
458
+ file_path (str | None): Path to the target file. Defaults to None.
459
+
460
+ Returns:
461
+ AdapterResult[None]: The result produced by the operation.
462
+ """
463
+ return await self._execute_with_circuit_breaker(
464
+ "save_file",
465
+ lambda: self.adapter.save_file(file_path),
466
+ input_dict={"file_path": file_path},
467
+ )
468
+
469
+ async def execute_macro(
470
+ self, params: dict[str, Any]
471
+ ) -> AdapterResult[dict[str, Any]]:
472
+ """Provide execute macro support for the circuit breaker adapter.
473
+
474
+ Args:
475
+ params (dict[str, Any]): The params value.
476
+
477
+ Returns:
478
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
479
+ """
480
+ return await self._execute_with_circuit_breaker(
481
+ "execute_macro",
482
+ lambda: self.adapter.execute_macro(params), # type: ignore[attr-defined]
483
+ input_dict=params,
484
+ )
485
+
486
+ async def create_part(
487
+ self,
488
+ name: str | None = None,
489
+ units: str | None = None,
490
+ template: str | None = None,
491
+ ) -> AdapterResult[SolidWorksModel]:
492
+ """Create part through circuit breaker.
493
+
494
+ Args:
495
+ name (str | None): The name value. Defaults to None.
496
+ units (str | None): The units value. Defaults to None.
497
+ template (str | None): Template file path. Defaults to None.
498
+
499
+ Returns:
500
+ AdapterResult[SolidWorksModel]: The result produced by the operation.
501
+ """
502
+
503
+ async def _op() -> AdapterResult[SolidWorksModel]:
504
+ """Build internal op.
505
+
506
+ Returns:
507
+ AdapterResult[SolidWorksModel]: The result produced by the operation.
508
+ """
509
+ if template is not None:
510
+ # No zero-arg retry: dropping the template would silently
511
+ # build the document from a different one.
512
+ return await self.adapter.create_part(name, units, template)
513
+ if name is None and units is None:
514
+ return await self.adapter.create_part()
515
+ return await self._invoke_with_optional_args(
516
+ self.adapter.create_part,
517
+ name,
518
+ units,
519
+ )
520
+
521
+ return await self._execute_with_circuit_breaker(
522
+ "create_part",
523
+ _op,
524
+ input_dict={"name": name, "units": units, "template": template},
525
+ )
526
+
527
+ async def call(self, operation: Callable[[], object | Awaitable[object]]) -> object:
528
+ """Legacy call API used by tests.
529
+
530
+ Args:
531
+ operation (Callable[[], object | Awaitable[object]]): Callable object executed by
532
+ the helper.
533
+
534
+ Returns:
535
+ object: The result produced by the operation.
536
+
537
+ Raises:
538
+ RuntimeError: If the operation cannot be completed.
539
+ Exception: Circuit breaker is open.
540
+ """
541
+ if not self._should_allow_request():
542
+ raise Exception("Circuit breaker is open")
543
+ try:
544
+ result = operation()
545
+ if asyncio.iscoroutine(result):
546
+ result = await result
547
+ self._record_success()
548
+ return result
549
+ except Exception as exc:
550
+ self._record_failure()
551
+ raise RuntimeError(str(exc)) from exc
552
+
553
+ async def create_assembly(
554
+ self, name: str | None = None, template: str | None = None
555
+ ) -> AdapterResult[SolidWorksModel]:
556
+ """Create assembly through circuit breaker.
557
+
558
+ Args:
559
+ name (str | None): The name value. Defaults to None.
560
+ template (str | None): Template file path. Defaults to None.
561
+
562
+ Returns:
563
+ AdapterResult[SolidWorksModel]: The result produced by the operation.
564
+ """
565
+
566
+ async def _op() -> AdapterResult[SolidWorksModel]:
567
+ """Build internal op.
568
+
569
+ Returns:
570
+ AdapterResult[SolidWorksModel]: The result produced by the operation.
571
+ """
572
+ if template is not None:
573
+ return await self.adapter.create_assembly(name, template)
574
+ if name is None:
575
+ return await self.adapter.create_assembly()
576
+ return await self._invoke_with_optional_args(
577
+ self.adapter.create_assembly,
578
+ name,
579
+ )
580
+
581
+ return await self._execute_with_circuit_breaker(
582
+ "create_assembly", _op, input_dict={"name": name, "template": template}
583
+ )
584
+
585
+ async def create_drawing(
586
+ self, name: str | None = None, template: str | None = None
587
+ ) -> AdapterResult[SolidWorksModel]:
588
+ """Create drawing through circuit breaker.
589
+
590
+ Args:
591
+ name (str | None): The name value. Defaults to None.
592
+ template (str | None): Template file path. Defaults to None.
593
+
594
+ Returns:
595
+ AdapterResult[SolidWorksModel]: The result produced by the operation.
596
+ """
597
+ return await self._execute_with_circuit_breaker(
598
+ "create_drawing",
599
+ lambda: self.adapter.create_drawing(name, template),
600
+ input_dict={"name": name, "template": template},
601
+ )
602
+
603
+ # Feature operations
604
+
605
+ async def create_extrusion(
606
+ self, params: ExtrusionParameters
607
+ ) -> AdapterResult[SolidWorksFeature]:
608
+ """Create extrusion through circuit breaker.
609
+
610
+ Args:
611
+ params (ExtrusionParameters): The params value.
612
+
613
+ Returns:
614
+ AdapterResult[SolidWorksFeature]: The result produced by the operation.
615
+ """
616
+ return await self._execute_with_circuit_breaker(
617
+ "create_extrusion",
618
+ lambda: self.adapter.create_extrusion(params),
619
+ input_dict=_to_input_dict(params),
620
+ )
621
+
622
+ async def create_cut_extrude(
623
+ self, params: ExtrusionParameters
624
+ ) -> AdapterResult[SolidWorksFeature]:
625
+ """Create cut-extrude through circuit breaker."""
626
+ return await self._execute_with_circuit_breaker(
627
+ "create_cut_extrude",
628
+ lambda: self.adapter.create_cut_extrude(params),
629
+ input_dict=_to_input_dict(params),
630
+ )
631
+
632
+ async def add_fillet(
633
+ self, radius: float, edge_names: list[str]
634
+ ) -> AdapterResult[SolidWorksFeature]:
635
+ """Add fillet through circuit breaker."""
636
+ return await self._execute_with_circuit_breaker(
637
+ "add_fillet",
638
+ lambda: self.adapter.add_fillet(radius, edge_names),
639
+ input_dict={"radius": radius, "edge_names": edge_names},
640
+ )
641
+
642
+ async def add_chamfer(
643
+ self, distance: float, edge_names: list[str]
644
+ ) -> AdapterResult[SolidWorksFeature]:
645
+ """Add chamfer through circuit breaker."""
646
+ return await self._execute_with_circuit_breaker(
647
+ "add_chamfer",
648
+ lambda: self.adapter.add_chamfer(distance, edge_names),
649
+ input_dict={"distance": distance, "edge_names": edge_names},
650
+ )
651
+
652
+ async def get_selected_entities(self) -> AdapterResult[list[dict[str, Any]]]:
653
+ """Report the current UI selection through circuit breaker."""
654
+ return await self._execute_with_circuit_breaker(
655
+ "get_selected_entities",
656
+ lambda: self.adapter.get_selected_entities(),
657
+ input_dict={},
658
+ )
659
+
660
+ async def select_feature(
661
+ self, feature_name: str, append: bool = False
662
+ ) -> AdapterResult[dict[str, Any]]:
663
+ """Select a named feature through circuit breaker."""
664
+ return await self._execute_with_circuit_breaker(
665
+ "select_feature",
666
+ lambda: self.adapter.select_feature(feature_name, append),
667
+ input_dict={"feature_name": feature_name, "append": append},
668
+ error_results_are_failures=False,
669
+ )
670
+
671
+ async def select_face_edges(
672
+ self,
673
+ point: list[float] | None = None,
674
+ side: str | None = None,
675
+ plane: str | None = None,
676
+ edge_type: str = "all",
677
+ append: bool = False,
678
+ ) -> AdapterResult[dict[str, Any]]:
679
+ """Multi-select a face's edges through circuit breaker."""
680
+ return await self._execute_with_circuit_breaker(
681
+ "select_face_edges",
682
+ lambda: self.adapter.select_face_edges(
683
+ point, side, plane, edge_type, append
684
+ ),
685
+ input_dict={
686
+ "point": point,
687
+ "side": side,
688
+ "plane": plane,
689
+ "edge_type": edge_type,
690
+ "append": append,
691
+ },
692
+ error_results_are_failures=False,
693
+ )
694
+
695
+ async def rebuild_model(
696
+ self, top_level_only: bool = False
697
+ ) -> AdapterResult[dict[str, Any]]:
698
+ """Force a rebuild through circuit breaker."""
699
+ return await self._execute_with_circuit_breaker(
700
+ "rebuild_model",
701
+ lambda: self.adapter.rebuild_model(top_level_only),
702
+ input_dict={"top_level_only": top_level_only},
703
+ error_results_are_failures=False,
704
+ )
705
+
706
+ async def create_reference_plane(
707
+ self,
708
+ reference: str,
709
+ offset: float = 0.0,
710
+ angle: float = 0.0,
711
+ flip: bool = False,
712
+ ) -> AdapterResult[dict[str, Any]]:
713
+ """Create reference plane through circuit breaker."""
714
+ return await self._execute_with_circuit_breaker(
715
+ "create_reference_plane",
716
+ lambda: self.adapter.create_reference_plane(reference, offset, angle, flip),
717
+ input_dict={
718
+ "reference": reference,
719
+ "offset": offset,
720
+ "angle": angle,
721
+ "flip": flip,
722
+ },
723
+ )
724
+
725
+ async def create_axis(self, reference: str) -> AdapterResult[dict[str, Any]]:
726
+ """Create reference axis through circuit breaker."""
727
+ return await self._execute_with_circuit_breaker(
728
+ "create_axis",
729
+ lambda: self.adapter.create_axis(reference),
730
+ input_dict={"reference": reference},
731
+ )
732
+
733
+ async def create_reference_point(
734
+ self,
735
+ mode: str,
736
+ x: float,
737
+ y: float,
738
+ z: float,
739
+ distance: float | None = None,
740
+ percent: float | None = None,
741
+ ) -> AdapterResult[dict[str, Any]]:
742
+ """Create a reference point through circuit breaker."""
743
+ return await self._execute_with_circuit_breaker(
744
+ "create_reference_point",
745
+ lambda: self.adapter.create_reference_point(
746
+ mode, x, y, z, distance, percent
747
+ ),
748
+ input_dict={
749
+ "mode": mode,
750
+ "x": x,
751
+ "y": y,
752
+ "z": z,
753
+ "distance": distance,
754
+ "percent": percent,
755
+ },
756
+ )
757
+
758
+ async def mirror_feature(
759
+ self,
760
+ features: list[str],
761
+ mirror_plane: str,
762
+ merge: bool = True,
763
+ mirror_bodies: bool = True,
764
+ ) -> AdapterResult[dict[str, Any]]:
765
+ """Mirror solid bodies or features through circuit breaker."""
766
+ return await self._execute_with_circuit_breaker(
767
+ "mirror_feature",
768
+ lambda: self.adapter.mirror_feature(
769
+ features, mirror_plane, merge, mirror_bodies
770
+ ),
771
+ input_dict={
772
+ "features": features,
773
+ "mirror_plane": mirror_plane,
774
+ "merge": merge,
775
+ "mirror_bodies": mirror_bodies,
776
+ },
777
+ )
778
+
779
+ async def pattern_circular(
780
+ self,
781
+ features: list[str],
782
+ axis: str,
783
+ count: int,
784
+ angle: float = 360.0,
785
+ equal_spacing: bool = True,
786
+ ) -> AdapterResult[dict[str, Any]]:
787
+ """Pattern features around an axis through circuit breaker."""
788
+ return await self._execute_with_circuit_breaker(
789
+ "pattern_circular",
790
+ lambda: self.adapter.pattern_circular(
791
+ features, axis, count, angle, equal_spacing
792
+ ),
793
+ input_dict={
794
+ "features": features,
795
+ "axis": axis,
796
+ "count": count,
797
+ "angle": angle,
798
+ "equal_spacing": equal_spacing,
799
+ },
800
+ )
801
+
802
+ async def create_revolve(
803
+ self, params: RevolveParameters
804
+ ) -> AdapterResult[SolidWorksFeature]:
805
+ """Create revolve through circuit breaker.
806
+
807
+ Args:
808
+ params (RevolveParameters): The params value.
809
+
810
+ Returns:
811
+ AdapterResult[SolidWorksFeature]: The result produced by the operation.
812
+ """
813
+ return await self._execute_with_circuit_breaker(
814
+ "create_revolve",
815
+ lambda: self.adapter.create_revolve(params),
816
+ input_dict=_to_input_dict(params),
817
+ )
818
+
819
+ async def create_sweep(
820
+ self, params: SweepParameters
821
+ ) -> AdapterResult[SolidWorksFeature]:
822
+ """Create sweep through circuit breaker.
823
+
824
+ Args:
825
+ params (SweepParameters): The params value.
826
+
827
+ Returns:
828
+ AdapterResult[SolidWorksFeature]: The result produced by the operation.
829
+ """
830
+ return await self._execute_with_circuit_breaker(
831
+ "create_sweep",
832
+ lambda: self.adapter.create_sweep(params),
833
+ input_dict=_to_input_dict(params),
834
+ )
835
+
836
+ async def create_loft(
837
+ self, params: LoftParameters
838
+ ) -> AdapterResult[SolidWorksFeature]:
839
+ """Create loft through circuit breaker.
840
+
841
+ Args:
842
+ params (LoftParameters): The params value.
843
+
844
+ Returns:
845
+ AdapterResult[SolidWorksFeature]: The result produced by the operation.
846
+ """
847
+ return await self._execute_with_circuit_breaker(
848
+ "create_loft",
849
+ lambda: self.adapter.create_loft(params),
850
+ input_dict=_to_input_dict(params),
851
+ )
852
+
853
+ # Sketch operations
854
+
855
+ async def create_sketch(self, plane: str) -> AdapterResult[str]:
856
+ """Create sketch through circuit breaker.
857
+
858
+ Args:
859
+ plane (str): The plane value.
860
+
861
+ Returns:
862
+ AdapterResult[str]: The result produced by the operation.
863
+ """
864
+ return await self._execute_with_circuit_breaker(
865
+ "create_sketch",
866
+ lambda: self.adapter.create_sketch(plane),
867
+ input_dict={"plane": plane},
868
+ )
869
+
870
+ async def add_line(
871
+ self, x1: float, y1: float, x2: float, y2: float
872
+ ) -> AdapterResult[str]:
873
+ """Add line through circuit breaker.
874
+
875
+ Args:
876
+ x1 (float): The x1 value.
877
+ y1 (float): The y1 value.
878
+ x2 (float): The x2 value.
879
+ y2 (float): The y2 value.
880
+
881
+ Returns:
882
+ AdapterResult[str]: The result produced by the operation.
883
+ """
884
+ return await self._execute_with_circuit_breaker(
885
+ "add_line",
886
+ lambda: self.adapter.add_line(x1, y1, x2, y2),
887
+ input_dict={"x1": x1, "y1": y1, "x2": x2, "y2": y2},
888
+ )
889
+
890
+ async def add_centerline(
891
+ self, x1: float, y1: float, x2: float, y2: float
892
+ ) -> AdapterResult[str]:
893
+ """Add centerline through circuit breaker.
894
+
895
+ Args:
896
+ x1 (float): The x1 value.
897
+ y1 (float): The y1 value.
898
+ x2 (float): The x2 value.
899
+ y2 (float): The y2 value.
900
+
901
+ Returns:
902
+ AdapterResult[str]: The result produced by the operation.
903
+ """
904
+ return await self._execute_with_circuit_breaker(
905
+ "add_centerline",
906
+ lambda: self.adapter.add_centerline(x1, y1, x2, y2),
907
+ input_dict={"x1": x1, "y1": y1, "x2": x2, "y2": y2},
908
+ )
909
+
910
+ async def add_circle(
911
+ self, center_x: float, center_y: float, radius: float
912
+ ) -> AdapterResult[str]:
913
+ """Add circle through circuit breaker.
914
+
915
+ Args:
916
+ center_x (float): The center x value.
917
+ center_y (float): The center y value.
918
+ radius (float): The radius value.
919
+
920
+ Returns:
921
+ AdapterResult[str]: The result produced by the operation.
922
+ """
923
+ return await self._execute_with_circuit_breaker(
924
+ "add_circle",
925
+ lambda: self.adapter.add_circle(center_x, center_y, radius),
926
+ input_dict={"center_x": center_x, "center_y": center_y, "radius": radius},
927
+ )
928
+
929
+ async def add_rectangle(
930
+ self, x1: float, y1: float, x2: float, y2: float
931
+ ) -> AdapterResult[str]:
932
+ """Add rectangle through circuit breaker.
933
+
934
+ Args:
935
+ x1 (float): The x1 value.
936
+ y1 (float): The y1 value.
937
+ x2 (float): The x2 value.
938
+ y2 (float): The y2 value.
939
+
940
+ Returns:
941
+ AdapterResult[str]: The result produced by the operation.
942
+ """
943
+ return await self._execute_with_circuit_breaker(
944
+ "add_rectangle",
945
+ lambda: self.adapter.add_rectangle(x1, y1, x2, y2),
946
+ input_dict={"x1": x1, "y1": y1, "x2": x2, "y2": y2},
947
+ )
948
+
949
+ async def add_arc(
950
+ self,
951
+ center_x: float,
952
+ center_y: float,
953
+ start_x: float,
954
+ start_y: float,
955
+ end_x: float,
956
+ end_y: float,
957
+ ) -> AdapterResult[str]:
958
+ """Add arc through circuit breaker.
959
+
960
+ Args:
961
+ center_x (float): Arc center X coordinate.
962
+ center_y (float): Arc center Y coordinate.
963
+ start_x (float): Arc start X coordinate.
964
+ start_y (float): Arc start Y coordinate.
965
+ end_x (float): Arc end X coordinate.
966
+ end_y (float): Arc end Y coordinate.
967
+
968
+ Returns:
969
+ AdapterResult[str]: The result produced by the operation.
970
+ """
971
+ return await self._execute_with_circuit_breaker(
972
+ "add_arc",
973
+ lambda: self.adapter.add_arc(
974
+ center_x,
975
+ center_y,
976
+ start_x,
977
+ start_y,
978
+ end_x,
979
+ end_y,
980
+ ),
981
+ input_dict={
982
+ "center_x": center_x,
983
+ "center_y": center_y,
984
+ "start_x": start_x,
985
+ "start_y": start_y,
986
+ "end_x": end_x,
987
+ "end_y": end_y,
988
+ },
989
+ )
990
+
991
+ async def add_spline(self, points: list[dict[str, float]]) -> AdapterResult[str]:
992
+ """Add spline through circuit breaker."""
993
+ return await self._execute_with_circuit_breaker(
994
+ "add_spline",
995
+ lambda: self.adapter.add_spline(points),
996
+ input_dict={"points": points},
997
+ )
998
+
999
+ async def add_polygon(
1000
+ self, center_x: float, center_y: float, radius: float, sides: int
1001
+ ) -> AdapterResult[str]:
1002
+ """Add polygon through circuit breaker."""
1003
+ return await self._execute_with_circuit_breaker(
1004
+ "add_polygon",
1005
+ lambda: self.adapter.add_polygon(center_x, center_y, radius, sides),
1006
+ input_dict={
1007
+ "center_x": center_x,
1008
+ "center_y": center_y,
1009
+ "radius": radius,
1010
+ "sides": sides,
1011
+ },
1012
+ )
1013
+
1014
+ async def add_ellipse(
1015
+ self,
1016
+ center_x: float,
1017
+ center_y: float,
1018
+ major_axis: float,
1019
+ minor_axis: float,
1020
+ ) -> AdapterResult[str]:
1021
+ """Add ellipse through circuit breaker."""
1022
+ return await self._execute_with_circuit_breaker(
1023
+ "add_ellipse",
1024
+ lambda: self.adapter.add_ellipse(
1025
+ center_x, center_y, major_axis, minor_axis
1026
+ ),
1027
+ input_dict={
1028
+ "center_x": center_x,
1029
+ "center_y": center_y,
1030
+ "major_axis": major_axis,
1031
+ "minor_axis": minor_axis,
1032
+ },
1033
+ )
1034
+
1035
+ async def sketch_linear_pattern(
1036
+ self,
1037
+ entities: list[str],
1038
+ direction_x: float,
1039
+ direction_y: float,
1040
+ spacing: float,
1041
+ count: int,
1042
+ ) -> AdapterResult[str]:
1043
+ """Sketch linear pattern through circuit breaker."""
1044
+ return await self._execute_with_circuit_breaker(
1045
+ "sketch_linear_pattern",
1046
+ lambda: self.adapter.sketch_linear_pattern(
1047
+ entities, direction_x, direction_y, spacing, count
1048
+ ),
1049
+ input_dict={
1050
+ "entities": entities,
1051
+ "direction_x": direction_x,
1052
+ "direction_y": direction_y,
1053
+ "spacing": spacing,
1054
+ "count": count,
1055
+ },
1056
+ )
1057
+
1058
+ async def sketch_circular_pattern(
1059
+ self,
1060
+ entities: list[str],
1061
+ angle: float,
1062
+ count: int,
1063
+ ) -> AdapterResult[str]:
1064
+ """Sketch circular pattern through circuit breaker."""
1065
+ return await self._execute_with_circuit_breaker(
1066
+ "sketch_circular_pattern",
1067
+ lambda: self.adapter.sketch_circular_pattern(entities, angle, count),
1068
+ input_dict={"entities": entities, "angle": angle, "count": count},
1069
+ )
1070
+
1071
+ async def sketch_mirror(
1072
+ self, entities: list[str], mirror_line: str
1073
+ ) -> AdapterResult[str]:
1074
+ """Sketch mirror through circuit breaker."""
1075
+ return await self._execute_with_circuit_breaker(
1076
+ "sketch_mirror",
1077
+ lambda: self.adapter.sketch_mirror(entities, mirror_line),
1078
+ input_dict={"entities": entities, "mirror_line": mirror_line},
1079
+ )
1080
+
1081
+ async def sketch_offset(
1082
+ self,
1083
+ entities: list[str],
1084
+ offset_distance: float,
1085
+ reverse_direction: bool,
1086
+ ) -> AdapterResult[str]:
1087
+ """Sketch offset through circuit breaker."""
1088
+ return await self._execute_with_circuit_breaker(
1089
+ "sketch_offset",
1090
+ lambda: self.adapter.sketch_offset(
1091
+ entities, offset_distance, reverse_direction
1092
+ ),
1093
+ input_dict={
1094
+ "entities": entities,
1095
+ "offset_distance": offset_distance,
1096
+ "reverse_direction": reverse_direction,
1097
+ },
1098
+ )
1099
+
1100
+ async def add_sketch_constraint(
1101
+ self,
1102
+ entity1: str,
1103
+ entity2: str | None,
1104
+ relation_type: str,
1105
+ entity3: str | None = None,
1106
+ ) -> AdapterResult[str]:
1107
+ """Add sketch constraint through circuit breaker."""
1108
+ return await self._execute_with_circuit_breaker(
1109
+ "add_sketch_constraint",
1110
+ lambda: self.adapter.add_sketch_constraint(
1111
+ entity1,
1112
+ entity2,
1113
+ relation_type,
1114
+ entity3,
1115
+ ),
1116
+ input_dict={
1117
+ "entity1": entity1,
1118
+ "entity2": entity2,
1119
+ "relation_type": relation_type,
1120
+ "entity3": entity3,
1121
+ },
1122
+ )
1123
+
1124
+ async def add_sketch_dimension(
1125
+ self,
1126
+ entity1: str,
1127
+ entity2: str | None,
1128
+ dimension_type: str,
1129
+ value: float,
1130
+ ) -> AdapterResult[str]:
1131
+ """Add sketch dimension through circuit breaker."""
1132
+ return await self._execute_with_circuit_breaker(
1133
+ "add_sketch_dimension",
1134
+ lambda: self.adapter.add_sketch_dimension(
1135
+ entity1,
1136
+ entity2,
1137
+ dimension_type,
1138
+ value,
1139
+ ),
1140
+ input_dict={
1141
+ "entity1": entity1,
1142
+ "entity2": entity2,
1143
+ "dimension_type": dimension_type,
1144
+ "value": value,
1145
+ },
1146
+ )
1147
+
1148
+ async def check_sketch_fully_defined(
1149
+ self, sketch_name: str | None = None
1150
+ ) -> AdapterResult[dict[str, Any]]:
1151
+ """Check sketch definition status through circuit breaker."""
1152
+ return await self._execute_with_circuit_breaker(
1153
+ "check_sketch_fully_defined",
1154
+ lambda: self.adapter.check_sketch_fully_defined(sketch_name),
1155
+ input_dict={"sketch_name": sketch_name},
1156
+ )
1157
+
1158
+ async def exit_sketch(self) -> AdapterResult[None]:
1159
+ """Exit sketch through circuit breaker.
1160
+
1161
+ Returns:
1162
+ AdapterResult[None]: The result produced by the operation.
1163
+ """
1164
+ return await self._execute_with_circuit_breaker(
1165
+ "exit_sketch",
1166
+ lambda: self.adapter.exit_sketch(),
1167
+ input_dict={},
1168
+ )
1169
+
1170
+ # Analysis operations
1171
+
1172
+ async def get_mass_properties(self) -> AdapterResult[MassProperties]:
1173
+ """Get mass properties through circuit breaker.
1174
+
1175
+ Returns:
1176
+ AdapterResult[MassProperties]: The result produced by the operation.
1177
+ """
1178
+ return await self._execute_with_circuit_breaker(
1179
+ "get_mass_properties",
1180
+ lambda: self.adapter.get_mass_properties(),
1181
+ input_dict={},
1182
+ )
1183
+
1184
+ async def get_model_info(self) -> AdapterResult[dict[str, object]]:
1185
+ """Get active model metadata through circuit breaker.
1186
+
1187
+ Returns:
1188
+ AdapterResult[dict[str, object]]: The result produced by the operation.
1189
+ """
1190
+ return await self._execute_with_circuit_breaker(
1191
+ "get_model_info",
1192
+ lambda: self.adapter.get_model_info(),
1193
+ input_dict={},
1194
+ )
1195
+
1196
+ async def list_features(
1197
+ self, include_suppressed: bool = False, max_assembly_depth: int = 2
1198
+ ) -> AdapterResult[list[dict[str, object]]]:
1199
+ """List model features through circuit breaker.
1200
+
1201
+ Args:
1202
+ include_suppressed (bool): The include suppressed value. Defaults to False.
1203
+ max_assembly_depth (int): Sub-assembly recursion depth. Defaults to 2.
1204
+
1205
+ Returns:
1206
+ AdapterResult[list[dict[str, object]]]: The result produced by the operation.
1207
+ """
1208
+ return await self._execute_with_circuit_breaker(
1209
+ "list_features",
1210
+ lambda: self.adapter.list_features(include_suppressed, max_assembly_depth),
1211
+ input_dict={
1212
+ "include_suppressed": include_suppressed,
1213
+ "max_assembly_depth": max_assembly_depth,
1214
+ },
1215
+ )
1216
+
1217
+ async def list_configurations(self) -> AdapterResult[list[str]]:
1218
+ """List model configurations through circuit breaker.
1219
+
1220
+ Returns:
1221
+ AdapterResult[list[str]]: The result produced by the operation.
1222
+ """
1223
+ return await self._execute_with_circuit_breaker(
1224
+ "list_configurations",
1225
+ lambda: self.adapter.list_configurations(),
1226
+ input_dict={},
1227
+ )
1228
+
1229
+ # Export operations
1230
+
1231
+ async def export_image(self, payload: dict) -> AdapterResult[dict]:
1232
+ """Export viewport image through circuit breaker.
1233
+
1234
+ Args:
1235
+ payload (dict): The payload value.
1236
+
1237
+ Returns:
1238
+ AdapterResult[dict]: The result produced by the operation.
1239
+ """
1240
+ return await self._execute_with_circuit_breaker(
1241
+ "export_image",
1242
+ lambda: self.adapter.export_image(payload),
1243
+ input_dict=payload,
1244
+ )
1245
+
1246
+ async def export_file(
1247
+ self, file_path: str, format_type: str
1248
+ ) -> AdapterResult[None]:
1249
+ """Export file through circuit breaker.
1250
+
1251
+ Args:
1252
+ file_path (str): Path to the target file.
1253
+ format_type (str): The format type value.
1254
+
1255
+ Returns:
1256
+ AdapterResult[None]: The result produced by the operation.
1257
+ """
1258
+ return await self._execute_with_circuit_breaker(
1259
+ "export_file",
1260
+ lambda: self.adapter.export_file(file_path, format_type),
1261
+ input_dict={"file_path": file_path, "format_type": format_type},
1262
+ )
1263
+
1264
+ # Dimension operations
1265
+
1266
+ async def get_dimension(self, name: str) -> AdapterResult[float]:
1267
+ """Get dimension through circuit breaker.
1268
+
1269
+ Args:
1270
+ name (str): The name value.
1271
+
1272
+ Returns:
1273
+ AdapterResult[float]: The result produced by the operation.
1274
+ """
1275
+ return await self._execute_with_circuit_breaker(
1276
+ "get_dimension",
1277
+ lambda: self.adapter.get_dimension(name),
1278
+ input_dict={"name": name},
1279
+ )
1280
+
1281
+ async def set_dimension(self, name: str, value: float) -> AdapterResult[None]:
1282
+ """Set dimension through circuit breaker.
1283
+
1284
+ Args:
1285
+ name (str): The name value.
1286
+ value (float): The value value.
1287
+
1288
+ Returns:
1289
+ AdapterResult[None]: The result produced by the operation.
1290
+ """
1291
+ return await self._execute_with_circuit_breaker(
1292
+ "set_dimension",
1293
+ lambda: self.adapter.set_dimension(name, value),
1294
+ input_dict={"name": name, "value": value},
1295
+ )
1296
+
1297
+ async def set_units(self, unit_system: str) -> AdapterResult[dict[str, Any]]:
1298
+ """Set document units through circuit breaker.
1299
+
1300
+ Args:
1301
+ unit_system (str): One of ``mm``, ``cm``, ``m``, ``in``, ``ft``.
1302
+
1303
+ Returns:
1304
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1305
+ """
1306
+ return await self._execute_with_circuit_breaker(
1307
+ "set_units",
1308
+ lambda: self.adapter.set_units(unit_system),
1309
+ input_dict={"unit_system": unit_system},
1310
+ )
1311
+
1312
+ async def list_open_documents(self) -> AdapterResult[list[dict[str, Any]]]:
1313
+ """List open documents through circuit breaker.
1314
+
1315
+ Returns:
1316
+ AdapterResult[list[dict[str, Any]]]: The result produced by the
1317
+ operation.
1318
+ """
1319
+ return await self._execute_with_circuit_breaker(
1320
+ "list_open_documents",
1321
+ lambda: self.adapter.list_open_documents(),
1322
+ input_dict={},
1323
+ )
1324
+
1325
+ async def activate_document(
1326
+ self, title_or_path: str
1327
+ ) -> AdapterResult[dict[str, Any]]:
1328
+ """Activate an open document through circuit breaker.
1329
+
1330
+ Args:
1331
+ title_or_path (str): Title, full path, or file name of an open
1332
+ document.
1333
+
1334
+ Returns:
1335
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1336
+ """
1337
+ return await self._execute_with_circuit_breaker(
1338
+ "activate_document",
1339
+ lambda: self.adapter.activate_document(title_or_path),
1340
+ input_dict={"title_or_path": title_or_path},
1341
+ )
1342
+
1343
+ # Assembly operations
1344
+
1345
+ async def insert_component(
1346
+ self, file_path: str, x: float = 0.0, y: float = 0.0, z: float = 0.0
1347
+ ) -> AdapterResult[dict[str, Any]]:
1348
+ """Insert component through circuit breaker.
1349
+
1350
+ Args:
1351
+ file_path (str): Path to the target file.
1352
+ x (float): X position in millimetres. Defaults to 0.0.
1353
+ y (float): Y position in millimetres. Defaults to 0.0.
1354
+ z (float): Z position in millimetres. Defaults to 0.0.
1355
+
1356
+ Returns:
1357
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1358
+ """
1359
+ return await self._execute_with_circuit_breaker(
1360
+ "insert_component",
1361
+ lambda: self.adapter.insert_component(file_path, x, y, z),
1362
+ input_dict={"file_path": file_path, "x": x, "y": y, "z": z},
1363
+ )
1364
+
1365
+ async def list_components(self) -> AdapterResult[list[str]]:
1366
+ """List components through circuit breaker.
1367
+
1368
+ Returns:
1369
+ AdapterResult[list[str]]: The result produced by the operation.
1370
+ """
1371
+ return await self._execute_with_circuit_breaker(
1372
+ "list_components",
1373
+ lambda: self.adapter.list_components(),
1374
+ input_dict={},
1375
+ )
1376
+
1377
+ async def get_model_geometry_summary(self) -> AdapterResult[dict[str, Any]]:
1378
+ """Summarise the active document through circuit breaker.
1379
+
1380
+ Returns:
1381
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1382
+ """
1383
+ return await self._execute_with_circuit_breaker(
1384
+ "get_model_geometry_summary",
1385
+ lambda: self.adapter.get_model_geometry_summary(),
1386
+ input_dict={},
1387
+ )
1388
+
1389
+ async def create_drawing_view(
1390
+ self, payload: Any = None
1391
+ ) -> AdapterResult[dict[str, Any]]:
1392
+ """Create a drawing view through circuit breaker.
1393
+
1394
+ Args:
1395
+ payload (Any): Tool payload for the drawing view.
1396
+
1397
+ Returns:
1398
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1399
+ """
1400
+ return await self._execute_with_circuit_breaker(
1401
+ "create_drawing_view",
1402
+ lambda: self.adapter.create_drawing_view(payload),
1403
+ input_dict=payload if isinstance(payload, dict) else {},
1404
+ )
1405
+
1406
+ async def add_drawing_view(
1407
+ self, payload: Any = None
1408
+ ) -> AdapterResult[dict[str, Any]]:
1409
+ """Add a drawing view through circuit breaker.
1410
+
1411
+ Args:
1412
+ payload (Any): Tool payload for the drawing view.
1413
+
1414
+ Returns:
1415
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1416
+ """
1417
+ return await self._execute_with_circuit_breaker(
1418
+ "add_drawing_view",
1419
+ lambda: self.adapter.add_drawing_view(payload),
1420
+ input_dict=payload if isinstance(payload, dict) else {},
1421
+ )
1422
+
1423
+ async def create_technical_drawing(
1424
+ self, payload: Any = None
1425
+ ) -> AdapterResult[dict[str, Any]]:
1426
+ """Create standard views through circuit breaker.
1427
+
1428
+ Args:
1429
+ payload (Any): Tool payload for the technical drawing.
1430
+
1431
+ Returns:
1432
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1433
+ """
1434
+ return await self._execute_with_circuit_breaker(
1435
+ "create_technical_drawing",
1436
+ lambda: self.adapter.create_technical_drawing(payload),
1437
+ input_dict=payload if isinstance(payload, dict) else {},
1438
+ )
1439
+
1440
+ async def add_note(self, payload: Any = None) -> AdapterResult[dict[str, Any]]:
1441
+ """Add a drawing note through circuit breaker.
1442
+
1443
+ Args:
1444
+ payload (Any): Tool payload for the note.
1445
+
1446
+ Returns:
1447
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1448
+ """
1449
+ return await self._execute_with_circuit_breaker(
1450
+ "add_note",
1451
+ lambda: self.adapter.add_note(payload),
1452
+ input_dict=payload if isinstance(payload, dict) else {},
1453
+ )
1454
+
1455
+ async def list_drawing_views(self) -> AdapterResult[list[str]]:
1456
+ """List drawing views through circuit breaker.
1457
+
1458
+ Returns:
1459
+ AdapterResult[list[str]]: The result produced by the operation.
1460
+ """
1461
+ return await self._execute_with_circuit_breaker(
1462
+ "list_drawing_views",
1463
+ lambda: self.adapter.list_drawing_views(),
1464
+ input_dict={},
1465
+ )
1466
+
1467
+ async def auto_center_marks(
1468
+ self,
1469
+ view_name: str,
1470
+ mark_holes: bool = True,
1471
+ mark_fillets: bool = False,
1472
+ mark_slots: bool = True,
1473
+ ) -> AdapterResult[dict[str, Any]]:
1474
+ """Auto-insert drawing centre marks through circuit breaker."""
1475
+ return await self._execute_with_circuit_breaker(
1476
+ "auto_center_marks",
1477
+ lambda: self.adapter.auto_center_marks(
1478
+ view_name, mark_holes, mark_fillets, mark_slots
1479
+ ),
1480
+ input_dict={
1481
+ "view_name": view_name,
1482
+ "mark_holes": mark_holes,
1483
+ "mark_fillets": mark_fillets,
1484
+ "mark_slots": mark_slots,
1485
+ },
1486
+ )
1487
+
1488
+ async def save_body_as_part(
1489
+ self, body_name: str, file_path: str
1490
+ ) -> AdapterResult[dict[str, Any]]:
1491
+ """Save a body to a new part through circuit breaker."""
1492
+ return await self._execute_with_circuit_breaker(
1493
+ "save_body_as_part",
1494
+ lambda: self.adapter.save_body_as_part(body_name, file_path),
1495
+ input_dict={"body_name": body_name, "file_path": file_path},
1496
+ )
1497
+
1498
+ async def check_interference(
1499
+ self, params: Any = None
1500
+ ) -> AdapterResult[dict[str, Any]]:
1501
+ """Check interference through circuit breaker.
1502
+
1503
+ Args:
1504
+ params (Any): Optional interference-detection settings.
1505
+
1506
+ Returns:
1507
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1508
+ """
1509
+ return await self._execute_with_circuit_breaker(
1510
+ "check_interference",
1511
+ lambda: self.adapter.check_interference(params),
1512
+ input_dict=params if isinstance(params, dict) else {},
1513
+ )
1514
+
1515
+ async def delete_feature(self, name: str) -> AdapterResult[dict[str, Any]]:
1516
+ """Delete a feature through circuit breaker.
1517
+
1518
+ Args:
1519
+ name (str): Feature or sketch name.
1520
+
1521
+ Returns:
1522
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1523
+ """
1524
+ return await self._execute_with_circuit_breaker(
1525
+ "delete_feature",
1526
+ lambda: self.adapter.delete_feature(name),
1527
+ input_dict={"name": name},
1528
+ )
1529
+
1530
+ async def suppress_feature(
1531
+ self, name: str, suppress: bool = True
1532
+ ) -> AdapterResult[dict[str, Any]]:
1533
+ """Suppress or unsuppress a feature through circuit breaker.
1534
+
1535
+ Args:
1536
+ name (str): Feature name.
1537
+ suppress (bool): True to suppress, False to unsuppress.
1538
+
1539
+ Returns:
1540
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1541
+ """
1542
+ return await self._execute_with_circuit_breaker(
1543
+ "suppress_feature",
1544
+ lambda: self.adapter.suppress_feature(name, suppress),
1545
+ input_dict={"name": name, "suppress": suppress},
1546
+ )
1547
+
1548
+ async def rename_feature(
1549
+ self, old_name: str, new_name: str
1550
+ ) -> AdapterResult[dict[str, Any]]:
1551
+ """Rename a feature through circuit breaker.
1552
+
1553
+ Args:
1554
+ old_name (str): Current feature name.
1555
+ new_name (str): New feature name.
1556
+
1557
+ Returns:
1558
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1559
+ """
1560
+ return await self._execute_with_circuit_breaker(
1561
+ "rename_feature",
1562
+ lambda: self.adapter.rename_feature(old_name, new_name),
1563
+ input_dict={"old_name": old_name, "new_name": new_name},
1564
+ )
1565
+
1566
+ async def undo(self, count: int = 1) -> AdapterResult[dict[str, Any]]:
1567
+ """Undo through circuit breaker.
1568
+
1569
+ Args:
1570
+ count (int): Number of steps to undo.
1571
+
1572
+ Returns:
1573
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1574
+ """
1575
+ return await self._execute_with_circuit_breaker(
1576
+ "undo",
1577
+ lambda: self.adapter.undo(count),
1578
+ input_dict={"count": count},
1579
+ )
1580
+
1581
+ async def add_mate(
1582
+ self,
1583
+ component_a: str,
1584
+ component_b: str,
1585
+ entity_a: str = "Front Plane",
1586
+ entity_b: str = "Front Plane",
1587
+ mate_type: str = "coincident",
1588
+ alignment: str = "aligned",
1589
+ distance: float = 0.0,
1590
+ angle: float = 0.0,
1591
+ ) -> AdapterResult[dict[str, Any]]:
1592
+ """Add mate through circuit breaker.
1593
+
1594
+ Args:
1595
+ component_a (str): First component instance name.
1596
+ component_b (str): Second component instance name.
1597
+ entity_a (str): Named feature on the first component. Defaults to
1598
+ "Front Plane".
1599
+ entity_b (str): Named feature on the second component. Defaults to
1600
+ "Front Plane".
1601
+ mate_type (str): Mate type. Defaults to "coincident".
1602
+ alignment (str): Mate alignment. Defaults to "aligned".
1603
+ distance (float): Distance in millimetres. Defaults to 0.0.
1604
+ angle (float): Angle in degrees. Defaults to 0.0.
1605
+
1606
+ Returns:
1607
+ AdapterResult[dict[str, Any]]: The result produced by the operation.
1608
+ """
1609
+ return await self._execute_with_circuit_breaker(
1610
+ "add_mate",
1611
+ lambda: self.adapter.add_mate(
1612
+ component_a,
1613
+ component_b,
1614
+ entity_a,
1615
+ entity_b,
1616
+ mate_type,
1617
+ alignment,
1618
+ distance,
1619
+ angle,
1620
+ ),
1621
+ input_dict={
1622
+ "component_a": component_a,
1623
+ "component_b": component_b,
1624
+ "entity_a": entity_a,
1625
+ "entity_b": entity_b,
1626
+ "mate_type": mate_type,
1627
+ "alignment": alignment,
1628
+ "distance": distance,
1629
+ "angle": angle,
1630
+ },
1631
+ )
1632
+
1633
+ # SolidWorks-as-Code checkpoint
1634
+
1635
+ async def soc_create_checkpoint(
1636
+ self,
1637
+ label: str,
1638
+ file_path: str,
1639
+ *,
1640
+ feature_tree: list[dict[str, Any]] | None = None,
1641
+ ) -> int | None:
1642
+ """Create a named SoC checkpoint after saving the model.
1643
+
1644
+ Records a SoCCheckpoint row and a ModelStateSnapshot. Call this
1645
+ immediately after ``save_file`` to mark a stable rollback point.
1646
+
1647
+ Args:
1648
+ label (str): Short human name (e.g. "base-extrude").
1649
+ file_path (str): Path of the .sldprt that was just saved.
1650
+ feature_tree (list | None): Optional feature list from list_features().
1651
+
1652
+ Returns:
1653
+ int | None: The new SoCCheckpoint.id, or None if soc_session_id is unset.
1654
+ """
1655
+ if not self.soc_session_id:
1656
+ return None
1657
+ try:
1658
+ from solidworks_mcp.agents.history_db import (
1659
+ create_soc_checkpoint,
1660
+ insert_model_state_snapshot,
1661
+ list_tool_call_records,
1662
+ )
1663
+
1664
+ records = list_tool_call_records(
1665
+ self.soc_session_id, db_path=self.soc_db_path
1666
+ )
1667
+ last_id = records[-1]["id"] if records else None
1668
+
1669
+ snapshot_id: int | None = None
1670
+ if feature_tree is not None:
1671
+ import json as _json
1672
+
1673
+ insert_model_state_snapshot(
1674
+ session_id=self.soc_session_id,
1675
+ model_path=file_path,
1676
+ feature_tree_json=_json.dumps(feature_tree, default=str),
1677
+ db_path=self.soc_db_path,
1678
+ )
1679
+ from solidworks_mcp.agents.history_db import list_model_state_snapshots
1680
+
1681
+ snaps = list_model_state_snapshots(
1682
+ self.soc_session_id, db_path=self.soc_db_path
1683
+ )
1684
+ if snaps:
1685
+ snapshot_id = snaps[0]["id"]
1686
+
1687
+ return create_soc_checkpoint(
1688
+ session_id=self.soc_session_id,
1689
+ label=label,
1690
+ file_path=file_path,
1691
+ last_record_id=last_id,
1692
+ snapshot_id=snapshot_id,
1693
+ db_path=self.soc_db_path,
1694
+ )
1695
+ except Exception as exc:
1696
+ logger.debug(
1697
+ f"[soc_checkpoint] failed to create checkpoint {label!r}: {exc}"
1698
+ )
1699
+ return None
1700
+
1701
+ # --- Engineering-data and drawing-documentation methods ---
1702
+
1703
+ async def list_model_dimensions(
1704
+ self,
1705
+ feature_name: str | None = None,
1706
+ ) -> AdapterResult[list[dict[str, Any]]]:
1707
+ """List every dimension in the active part or assembly through circuit breaker."""
1708
+ return await self._execute_with_circuit_breaker(
1709
+ "list_model_dimensions",
1710
+ lambda: self.adapter.list_model_dimensions(feature_name),
1711
+ input_dict={"feature_name": feature_name},
1712
+ error_results_are_failures=False,
1713
+ )
1714
+
1715
+ async def measure_entities(
1716
+ self,
1717
+ entities: list[str],
1718
+ ) -> AdapterResult[dict[str, Any]]:
1719
+ """Measure one or two selected entities through circuit breaker."""
1720
+ return await self._execute_with_circuit_breaker(
1721
+ "measure_entities",
1722
+ lambda: self.adapter.measure_entities(entities),
1723
+ input_dict={"entities": entities},
1724
+ error_results_are_failures=False,
1725
+ )
1726
+
1727
+ async def get_material(
1728
+ self,
1729
+ configuration: str | None = None,
1730
+ ) -> AdapterResult[dict[str, Any]]:
1731
+ """Read the material assigned to the active part through circuit breaker."""
1732
+ return await self._execute_with_circuit_breaker(
1733
+ "get_material",
1734
+ lambda: self.adapter.get_material(configuration),
1735
+ input_dict={"configuration": configuration},
1736
+ error_results_are_failures=False,
1737
+ )
1738
+
1739
+ async def set_material(
1740
+ self,
1741
+ material_name: str,
1742
+ database: str | None = None,
1743
+ configuration: str | None = None,
1744
+ ) -> AdapterResult[dict[str, Any]]:
1745
+ """Assign a material to the active part through circuit breaker."""
1746
+ return await self._execute_with_circuit_breaker(
1747
+ "set_material",
1748
+ lambda: self.adapter.set_material(material_name, database, configuration),
1749
+ input_dict={
1750
+ "material_name": material_name,
1751
+ "database": database,
1752
+ "configuration": configuration,
1753
+ },
1754
+ )
1755
+
1756
+ async def list_available_materials(
1757
+ self,
1758
+ search: str | None = None,
1759
+ ) -> AdapterResult[list[dict[str, str]]]:
1760
+ """List materials from the configured material libraries through circuit breaker."""
1761
+ return await self._execute_with_circuit_breaker(
1762
+ "list_available_materials",
1763
+ lambda: self.adapter.list_available_materials(search),
1764
+ input_dict={"search": search},
1765
+ error_results_are_failures=False,
1766
+ )
1767
+
1768
+ async def get_custom_properties(
1769
+ self,
1770
+ configuration: str | None = None,
1771
+ ) -> AdapterResult[dict[str, Any]]:
1772
+ """Read the custom properties of the active document through circuit breaker."""
1773
+ return await self._execute_with_circuit_breaker(
1774
+ "get_custom_properties",
1775
+ lambda: self.adapter.get_custom_properties(configuration),
1776
+ input_dict={"configuration": configuration},
1777
+ error_results_are_failures=False,
1778
+ )
1779
+
1780
+ async def set_custom_property(
1781
+ self,
1782
+ name: str,
1783
+ value: str,
1784
+ configuration: str | None = None,
1785
+ ) -> AdapterResult[dict[str, Any]]:
1786
+ """Create or overwrite one custom property through circuit breaker."""
1787
+ return await self._execute_with_circuit_breaker(
1788
+ "set_custom_property",
1789
+ lambda: self.adapter.set_custom_property(name, value, configuration),
1790
+ input_dict={"name": name, "value": value, "configuration": configuration},
1791
+ )
1792
+
1793
+ async def export_flat_pattern(
1794
+ self,
1795
+ file_path: str,
1796
+ ) -> AdapterResult[dict[str, Any]]:
1797
+ """Export the active sheet-metal part's flat pattern through circuit breaker."""
1798
+ return await self._execute_with_circuit_breaker(
1799
+ "export_flat_pattern",
1800
+ lambda: self.adapter.export_flat_pattern(file_path),
1801
+ input_dict={"file_path": file_path},
1802
+ )
1803
+
1804
+ async def insert_model_dimensions(
1805
+ self,
1806
+ view_name: str | None = None,
1807
+ include_hole_wizard: bool = True,
1808
+ ) -> AdapterResult[dict[str, Any]]:
1809
+ """Bring model dimensions into drawing views through circuit breaker."""
1810
+ return await self._execute_with_circuit_breaker(
1811
+ "insert_model_dimensions",
1812
+ lambda: self.adapter.insert_model_dimensions(
1813
+ view_name, include_hole_wizard
1814
+ ),
1815
+ input_dict={
1816
+ "view_name": view_name,
1817
+ "include_hole_wizard": include_hole_wizard,
1818
+ },
1819
+ )
1820
+
1821
+ async def arrange_dimensions(
1822
+ self,
1823
+ view_name: str | None = None,
1824
+ ) -> AdapterResult[dict[str, Any]]:
1825
+ """Move drawing dimensions so none overlaps, then verify through circuit breaker."""
1826
+ return await self._execute_with_circuit_breaker(
1827
+ "arrange_dimensions",
1828
+ lambda: self.adapter.arrange_dimensions(view_name),
1829
+ input_dict={"view_name": view_name},
1830
+ )
1831
+
1832
+ async def insert_bom_table(
1833
+ self,
1834
+ view_name: str,
1835
+ template_path: str | None = None,
1836
+ bom_type: str = "top_level_only",
1837
+ x_mm: float | None = None,
1838
+ y_mm: float | None = None,
1839
+ ) -> AdapterResult[dict[str, Any]]:
1840
+ """Insert a BOM table anchored to an assembly drawing view through circuit breaker."""
1841
+ return await self._execute_with_circuit_breaker(
1842
+ "insert_bom_table",
1843
+ lambda: self.adapter.insert_bom_table(
1844
+ view_name, template_path, bom_type, x_mm, y_mm
1845
+ ),
1846
+ input_dict={
1847
+ "view_name": view_name,
1848
+ "template_path": template_path,
1849
+ "bom_type": bom_type,
1850
+ "x_mm": x_mm,
1851
+ "y_mm": y_mm,
1852
+ },
1853
+ )
1854
+
1855
+ async def set_sheet_scale(
1856
+ self,
1857
+ numerator: float,
1858
+ denominator: float,
1859
+ ) -> AdapterResult[dict[str, Any]]:
1860
+ """Set the active drawing sheet's scale through circuit breaker."""
1861
+ return await self._execute_with_circuit_breaker(
1862
+ "set_sheet_scale",
1863
+ lambda: self.adapter.set_sheet_scale(numerator, denominator),
1864
+ input_dict={"numerator": numerator, "denominator": denominator},
1865
+ )
1866
+
1867
+ async def add_gtol(
1868
+ self,
1869
+ characteristic: str,
1870
+ tolerance: float,
1871
+ x_mm: float,
1872
+ y_mm: float,
1873
+ diameter_zone: bool = False,
1874
+ material_condition: str = "none",
1875
+ datums: list[str] | None = None,
1876
+ attach_at_mm: tuple[float, float] | None = None,
1877
+ dimension: tuple[str, str] | None = None,
1878
+ ) -> AdapterResult[dict[str, Any]]:
1879
+ """Add a GD&T feature control frame through circuit breaker."""
1880
+ return await self._execute_with_circuit_breaker(
1881
+ "add_gtol",
1882
+ lambda: self.adapter.add_gtol(
1883
+ characteristic,
1884
+ tolerance,
1885
+ x_mm,
1886
+ y_mm,
1887
+ diameter_zone,
1888
+ material_condition,
1889
+ datums,
1890
+ attach_at_mm,
1891
+ dimension,
1892
+ ),
1893
+ input_dict={
1894
+ "characteristic": characteristic,
1895
+ "tolerance": tolerance,
1896
+ "x_mm": x_mm,
1897
+ "y_mm": y_mm,
1898
+ "diameter_zone": diameter_zone,
1899
+ "material_condition": material_condition,
1900
+ "datums": datums,
1901
+ "attach_at_mm": attach_at_mm,
1902
+ "dimension": dimension,
1903
+ },
1904
+ )
1905
+
1906
+ async def add_datum_tag(
1907
+ self,
1908
+ label: str,
1909
+ attach_at_mm: tuple[float, float],
1910
+ x_mm: float,
1911
+ y_mm: float,
1912
+ ) -> AdapterResult[dict[str, Any]]:
1913
+ """Add a datum feature symbol through circuit breaker."""
1914
+ return await self._execute_with_circuit_breaker(
1915
+ "add_datum_tag",
1916
+ lambda: self.adapter.add_datum_tag(label, attach_at_mm, x_mm, y_mm),
1917
+ input_dict={
1918
+ "label": label,
1919
+ "attach_at_mm": attach_at_mm,
1920
+ "x_mm": x_mm,
1921
+ "y_mm": y_mm,
1922
+ },
1923
+ )
1924
+
1925
+ async def create_standard_drawing(
1926
+ self,
1927
+ model_path: str,
1928
+ output_dir: str | None = None,
1929
+ title: str | None = None,
1930
+ subtitle: str | None = None,
1931
+ drawing_number: str | None = None,
1932
+ author: str = "",
1933
+ revision: str = "A",
1934
+ company: str = "",
1935
+ drawn_date: str | None = None,
1936
+ notes: list[str] | None = None,
1937
+ include_isometric: bool = True,
1938
+ write_model_properties: bool = True,
1939
+ title_block_text_height_mm: float | None = None,
1940
+ include_bom: bool = False,
1941
+ ) -> AdapterResult[dict[str, Any]]:
1942
+ """Create a complete standard A3 drawing through circuit breaker."""
1943
+ return await self._execute_with_circuit_breaker(
1944
+ "create_standard_drawing",
1945
+ lambda: self.adapter.create_standard_drawing(
1946
+ model_path,
1947
+ output_dir,
1948
+ title,
1949
+ subtitle,
1950
+ drawing_number,
1951
+ author,
1952
+ revision,
1953
+ company,
1954
+ drawn_date,
1955
+ notes,
1956
+ include_isometric,
1957
+ write_model_properties,
1958
+ title_block_text_height_mm,
1959
+ include_bom,
1960
+ ),
1961
+ input_dict={
1962
+ "model_path": model_path,
1963
+ "output_dir": output_dir,
1964
+ "title": title,
1965
+ "subtitle": subtitle,
1966
+ "drawing_number": drawing_number,
1967
+ "author": author,
1968
+ "revision": revision,
1969
+ "company": company,
1970
+ "drawn_date": drawn_date,
1971
+ "notes": notes,
1972
+ "include_isometric": include_isometric,
1973
+ "write_model_properties": write_model_properties,
1974
+ "title_block_text_height_mm": title_block_text_height_mm,
1975
+ "include_bom": include_bom,
1976
+ },
1977
+ )
1978
+
1979
+
1980
+ class CircuitBreaker:
1981
+ """Legacy standalone circuit breaker class expected by tests.
1982
+
1983
+ Args:
1984
+ failure_threshold (int): The failure threshold value. Defaults to 5.
1985
+ recovery_timeout (float): The recovery timeout value. Defaults to 60.0.
1986
+ expected_exception (type[Exception]): The expected exception value. Defaults to
1987
+ Exception.
1988
+
1989
+ Attributes:
1990
+ expected_exception (Any): The expected exception value.
1991
+ failure_count (Any): The failure count value.
1992
+ failure_threshold (Any): The failure threshold value.
1993
+ last_failure_time (Any): The last failure time value.
1994
+ recovery_timeout (Any): The recovery timeout value.
1995
+ state (Any): The state value.
1996
+ """
1997
+
1998
+ def __init__(
1999
+ self,
2000
+ failure_threshold: int = 5,
2001
+ recovery_timeout: float = 60.0,
2002
+ expected_exception: type[Exception] = Exception,
2003
+ ) -> None:
2004
+ """Initialize the circuit breaker.
2005
+
2006
+ Args:
2007
+ failure_threshold (int): The failure threshold value. Defaults to 5.
2008
+ recovery_timeout (float): The recovery timeout value. Defaults to 60.0.
2009
+ expected_exception (type[Exception]): The expected exception value. Defaults to
2010
+ Exception.
2011
+
2012
+ Returns:
2013
+ None: None.
2014
+ """
2015
+ self.failure_threshold = failure_threshold
2016
+ self.recovery_timeout = recovery_timeout
2017
+ self.expected_exception = expected_exception
2018
+ self.state = CircuitState.CLOSED
2019
+ self.failure_count = 0
2020
+ self.last_failure_time = 0.0
2021
+
2022
+ async def call(self, operation: Callable[[], object | Awaitable[object]]) -> object:
2023
+ """Provide call support for the circuit breaker.
2024
+
2025
+ Args:
2026
+ operation (Callable[[], object | Awaitable[object]]): Callable object executed by
2027
+ the helper.
2028
+
2029
+ Returns:
2030
+ object: The result produced by the operation.
2031
+
2032
+ Raises:
2033
+ Exception: Circuit breaker is open.
2034
+ """
2035
+ if self.state == CircuitState.OPEN:
2036
+ if time.time() - self.last_failure_time < self.recovery_timeout:
2037
+ raise Exception("Circuit breaker is open")
2038
+ self.state = CircuitState.HALF_OPEN
2039
+
2040
+ try:
2041
+ result = operation()
2042
+ if asyncio.iscoroutine(result):
2043
+ result = await result
2044
+ self.state = CircuitState.CLOSED
2045
+ self.failure_count = 0
2046
+ return result
2047
+ except self.expected_exception:
2048
+ self.failure_count += 1
2049
+ self.last_failure_time = time.time()
2050
+ if self.failure_count >= self.failure_threshold:
2051
+ self.state = CircuitState.OPEN
2052
+ raise