@hybridlabor-api/bdb-hardware-pcb 0.1.0

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 (192) hide show
  1. package/README.md +116 -0
  2. package/config/mcp/antigravity.json +22 -0
  3. package/config/mcp/claude.json +22 -0
  4. package/config/mcp/codex.toml +25 -0
  5. package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
  6. package/docs/review_documentation.md +121 -0
  7. package/installer.js +358 -0
  8. package/mcp_servers/kicad-mcp-server/.env.example +22 -0
  9. package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
  10. package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
  11. package/mcp_servers/kicad-mcp-server/README.md +316 -0
  12. package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
  13. package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
  14. package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
  15. package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
  16. package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
  17. package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
  18. package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
  19. package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
  20. package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
  21. package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
  22. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
  23. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
  24. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
  25. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
  26. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
  27. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
  28. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
  29. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
  30. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
  31. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
  32. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
  33. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
  34. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
  35. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
  36. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
  37. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
  38. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
  39. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
  40. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
  41. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
  42. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
  43. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
  44. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
  45. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
  46. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
  47. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
  48. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
  49. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
  50. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
  51. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
  52. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
  53. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
  54. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
  55. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
  56. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
  57. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
  58. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
  59. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
  60. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
  61. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
  62. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
  63. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
  64. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
  65. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
  66. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
  67. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
  68. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
  69. package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
  70. package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
  71. package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
  72. package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
  73. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
  74. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
  75. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
  76. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
  77. package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
  78. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
  79. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
  80. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
  81. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
  82. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
  83. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
  84. package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
  85. package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
  86. package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
  87. package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
  88. package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
  89. package/mcp_servers/openscad-mcp-server/README.md +154 -0
  90. package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
  91. package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
  92. package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
  93. package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
  94. package/mcp_servers/openscad-mcp-server/glama.json +4 -0
  95. package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
  96. package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
  97. package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
  98. package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
  99. package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
  100. package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
  101. package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
  102. package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
  103. package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
  104. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
  105. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
  106. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
  107. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
  108. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
  109. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
  110. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
  111. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
  112. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
  113. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
  114. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
  115. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
  116. package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
  117. package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
  118. package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
  119. package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
  120. package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
  121. package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
  122. package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
  123. package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
  124. package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
  125. package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
  126. package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
  127. package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
  128. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
  129. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
  130. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
  131. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
  132. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
  133. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
  134. package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
  135. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
  136. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
  137. package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
  138. package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
  139. package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
  140. package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
  141. package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
  142. package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
  143. package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
  144. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
  145. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
  146. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
  147. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
  148. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
  149. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
  150. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
  151. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
  152. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
  153. package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
  154. package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
  155. package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
  156. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
  157. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
  158. package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
  159. package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
  160. package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
  161. package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
  162. package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
  163. package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
  164. package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
  165. package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
  166. package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
  167. package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
  168. package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
  169. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
  170. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
  171. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
  172. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
  173. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
  174. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
  175. package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
  176. package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
  177. package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
  178. package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
  179. package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
  180. package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
  181. package/package.json +44 -0
  182. package/scripts/install_mcps.sh +86 -0
  183. package/scripts/openscad_wrapper.sh +62 -0
  184. package/scripts/run_kicad_mcp.sh +25 -0
  185. package/scripts/run_openscad_mcp.sh +41 -0
  186. package/scripts/test_mcp_connection.py +626 -0
  187. package/scripts/test_mcp_connection.sh +168 -0
  188. package/skills/code-first-hardware-design/SKILL.md +260 -0
  189. package/skills/pcb-constraint-definition/SKILL.md +236 -0
  190. package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
  191. package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
  192. package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
@@ -0,0 +1,415 @@
1
+ """
2
+ Error handling and retry mechanisms for remote CUDA MVS processing.
3
+
4
+ This module provides utilities for handling network errors, implementing
5
+ retry mechanisms, and ensuring robust communication with remote CUDA MVS servers.
6
+ """
7
+
8
+ import time
9
+ import random
10
+ import logging
11
+ import functools
12
+ from typing import Callable, Any, Type, Union, List, Dict, Optional, Tuple
13
+ import requests
14
+
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Define common exception types for network operations
20
+ NETWORK_EXCEPTIONS = (
21
+ requests.exceptions.ConnectionError,
22
+ requests.exceptions.Timeout,
23
+ requests.exceptions.HTTPError,
24
+ requests.exceptions.RequestException,
25
+ ConnectionRefusedError,
26
+ TimeoutError,
27
+ )
28
+
29
+ def retry_with_backoff(
30
+ max_retries: int = 3,
31
+ base_delay: float = 1.0,
32
+ max_delay: float = 60.0,
33
+ exception_types: Tuple[Type[Exception], ...] = NETWORK_EXCEPTIONS,
34
+ jitter_factor: float = 0.1,
35
+ logger_instance: Optional[logging.Logger] = None
36
+ ) -> Callable:
37
+ """
38
+ Decorator for retrying a function with exponential backoff.
39
+
40
+ Args:
41
+ max_retries: Maximum number of retries
42
+ base_delay: Base delay in seconds
43
+ max_delay: Maximum delay in seconds
44
+ exception_types: Tuple of exception types to catch and retry
45
+ jitter_factor: Factor for random jitter (0.0 to 1.0)
46
+ logger_instance: Logger instance to use (uses module logger if None)
47
+
48
+ Returns:
49
+ Decorator function
50
+ """
51
+ log = logger_instance or logger
52
+
53
+ def decorator(func: Callable) -> Callable:
54
+ @functools.wraps(func)
55
+ def wrapper(*args, **kwargs) -> Any:
56
+ retries = 0
57
+ last_exception = None
58
+
59
+ while True:
60
+ try:
61
+ return func(*args, **kwargs)
62
+ except exception_types as e:
63
+ retries += 1
64
+ last_exception = e
65
+
66
+ if retries > max_retries:
67
+ log.error(f"Max retries ({max_retries}) exceeded: {str(e)}")
68
+ raise
69
+
70
+ # Calculate delay with exponential backoff
71
+ delay = min(max_delay, base_delay * (2 ** (retries - 1)))
72
+
73
+ # Add jitter to avoid thundering herd problem
74
+ jitter = random.uniform(0, jitter_factor * delay)
75
+ sleep_time = delay + jitter
76
+
77
+ log.warning(f"Retry {retries}/{max_retries} after {sleep_time:.2f}s: {str(e)}")
78
+ time.sleep(sleep_time)
79
+ except Exception as e:
80
+ # Don't retry other exceptions
81
+ log.error(f"Non-retryable exception: {str(e)}")
82
+ raise
83
+
84
+ return wrapper
85
+
86
+ return decorator
87
+
88
+ def timeout_handler(
89
+ timeout: float,
90
+ default_value: Any = None,
91
+ exception_types: Tuple[Type[Exception], ...] = (TimeoutError, requests.exceptions.Timeout),
92
+ logger_instance: Optional[logging.Logger] = None
93
+ ) -> Callable:
94
+ """
95
+ Decorator for handling timeouts in function calls.
96
+
97
+ Args:
98
+ timeout: Timeout in seconds
99
+ default_value: Value to return if timeout occurs
100
+ exception_types: Tuple of exception types to catch as timeouts
101
+ logger_instance: Logger instance to use (uses module logger if None)
102
+
103
+ Returns:
104
+ Decorator function
105
+ """
106
+ log = logger_instance or logger
107
+
108
+ def decorator(func: Callable) -> Callable:
109
+ @functools.wraps(func)
110
+ def wrapper(*args, **kwargs) -> Any:
111
+ import signal
112
+
113
+ def timeout_handler(signum, frame):
114
+ raise TimeoutError(f"Function {func.__name__} timed out after {timeout} seconds")
115
+
116
+ # Set timeout using signal
117
+ original_handler = signal.signal(signal.SIGALRM, timeout_handler)
118
+ signal.alarm(int(timeout))
119
+
120
+ try:
121
+ return func(*args, **kwargs)
122
+ except exception_types as e:
123
+ log.warning(f"Timeout in {func.__name__}: {str(e)}")
124
+ return default_value
125
+ finally:
126
+ # Reset signal handler and alarm
127
+ signal.signal(signal.SIGALRM, original_handler)
128
+ signal.alarm(0)
129
+
130
+ return wrapper
131
+
132
+ return decorator
133
+
134
+ class NetworkErrorTracker:
135
+ """
136
+ Tracks network errors and provides information about error patterns.
137
+
138
+ This class helps identify persistent network issues and can be used
139
+ to make decisions about server availability.
140
+ """
141
+
142
+ def __init__(
143
+ self,
144
+ error_window: int = 10,
145
+ error_threshold: float = 0.5,
146
+ reset_after: int = 100
147
+ ):
148
+ """
149
+ Initialize the error tracker.
150
+
151
+ Args:
152
+ error_window: Number of recent requests to consider
153
+ error_threshold: Error rate threshold to consider a server problematic
154
+ reset_after: Number of successful requests after which to reset error count
155
+ """
156
+ self.error_window = error_window
157
+ self.error_threshold = error_threshold
158
+ self.reset_after = reset_after
159
+
160
+ self.requests = [] # List of (timestamp, success) tuples
161
+ self.consecutive_successes = 0
162
+ self.consecutive_failures = 0
163
+
164
+ def record_request(self, success: bool) -> None:
165
+ """
166
+ Record the result of a request.
167
+
168
+ Args:
169
+ success: Whether the request was successful
170
+ """
171
+ timestamp = time.time()
172
+ self.requests.append((timestamp, success))
173
+
174
+ # Trim old requests outside the window
175
+ self._trim_old_requests()
176
+
177
+ # Update consecutive counters
178
+ if success:
179
+ self.consecutive_successes += 1
180
+ self.consecutive_failures = 0
181
+
182
+ # Reset error count after enough consecutive successes
183
+ if self.consecutive_successes >= self.reset_after:
184
+ self.requests = [(timestamp, True)]
185
+ self.consecutive_successes = 1
186
+ else:
187
+ self.consecutive_failures += 1
188
+ self.consecutive_successes = 0
189
+
190
+ def _trim_old_requests(self) -> None:
191
+ """
192
+ Remove requests that are outside the current window.
193
+ """
194
+ if len(self.requests) > self.error_window:
195
+ self.requests = self.requests[-self.error_window:]
196
+
197
+ def get_error_rate(self) -> float:
198
+ """
199
+ Get the current error rate.
200
+
201
+ Returns:
202
+ Error rate as a float between 0.0 and 1.0
203
+ """
204
+ if not self.requests:
205
+ return 0.0
206
+
207
+ failures = sum(1 for _, success in self.requests if not success)
208
+ return failures / len(self.requests)
209
+
210
+ def is_server_problematic(self) -> bool:
211
+ """
212
+ Check if the server is experiencing persistent issues.
213
+
214
+ Returns:
215
+ True if the server is problematic, False otherwise
216
+ """
217
+ return self.get_error_rate() >= self.error_threshold
218
+
219
+ def get_status(self) -> Dict[str, Any]:
220
+ """
221
+ Get the current status of the error tracker.
222
+
223
+ Returns:
224
+ Dictionary with status information
225
+ """
226
+ return {
227
+ "error_rate": self.get_error_rate(),
228
+ "is_problematic": self.is_server_problematic(),
229
+ "consecutive_successes": self.consecutive_successes,
230
+ "consecutive_failures": self.consecutive_failures,
231
+ "total_requests": len(self.requests),
232
+ "recent_failures": sum(1 for _, success in self.requests if not success)
233
+ }
234
+
235
+ class CircuitBreaker:
236
+ """
237
+ Circuit breaker pattern implementation for network requests.
238
+
239
+ This class helps prevent cascading failures by stopping requests
240
+ to a problematic server until it recovers.
241
+ """
242
+
243
+ # Circuit states
244
+ CLOSED = "closed" # Normal operation
245
+ OPEN = "open" # No requests allowed
246
+ HALF_OPEN = "half_open" # Testing if service is back
247
+
248
+ def __init__(
249
+ self,
250
+ failure_threshold: int = 5,
251
+ recovery_timeout: float = 30.0,
252
+ reset_timeout: float = 60.0,
253
+ logger_instance: Optional[logging.Logger] = None
254
+ ):
255
+ """
256
+ Initialize the circuit breaker.
257
+
258
+ Args:
259
+ failure_threshold: Number of consecutive failures before opening
260
+ recovery_timeout: Time in seconds before testing recovery
261
+ reset_timeout: Time in seconds before fully resetting
262
+ logger_instance: Logger instance to use (uses module logger if None)
263
+ """
264
+ self.failure_threshold = failure_threshold
265
+ self.recovery_timeout = recovery_timeout
266
+ self.reset_timeout = reset_timeout
267
+ self.log = logger_instance or logger
268
+
269
+ self.state = self.CLOSED
270
+ self.failure_count = 0
271
+ self.last_failure_time = 0
272
+ self.last_success_time = 0
273
+
274
+ def record_success(self) -> None:
275
+ """
276
+ Record a successful request.
277
+ """
278
+ self.last_success_time = time.time()
279
+
280
+ if self.state == self.HALF_OPEN:
281
+ self.log.info("Circuit breaker reset to closed state after successful test request")
282
+ self.state = self.CLOSED
283
+ self.failure_count = 0
284
+ elif self.state == self.CLOSED:
285
+ # Reset failure count after a successful request
286
+ self.failure_count = 0
287
+
288
+ def record_failure(self) -> None:
289
+ """
290
+ Record a failed request.
291
+ """
292
+ self.last_failure_time = time.time()
293
+
294
+ if self.state == self.CLOSED:
295
+ self.failure_count += 1
296
+
297
+ if self.failure_count >= self.failure_threshold:
298
+ self.log.warning(f"Circuit breaker opened after {self.failure_count} consecutive failures")
299
+ self.state = self.OPEN
300
+ elif self.state == self.HALF_OPEN:
301
+ self.log.warning("Circuit breaker opened again after failed test request")
302
+ self.state = self.OPEN
303
+
304
+ def allow_request(self) -> bool:
305
+ """
306
+ Check if a request should be allowed.
307
+
308
+ Returns:
309
+ True if the request should be allowed, False otherwise
310
+ """
311
+ if self.state == self.CLOSED:
312
+ return True
313
+
314
+ if self.state == self.OPEN:
315
+ # Check if recovery timeout has elapsed
316
+ if time.time() - self.last_failure_time > self.recovery_timeout:
317
+ self.log.info("Circuit breaker entering half-open state to test service")
318
+ self.state = self.HALF_OPEN
319
+ return True
320
+ return False
321
+
322
+ # In HALF_OPEN state, allow only one request
323
+ return True
324
+
325
+ def reset(self) -> None:
326
+ """
327
+ Reset the circuit breaker to closed state.
328
+ """
329
+ self.state = self.CLOSED
330
+ self.failure_count = 0
331
+ self.log.info("Circuit breaker manually reset to closed state")
332
+
333
+ def get_status(self) -> Dict[str, Any]:
334
+ """
335
+ Get the current status of the circuit breaker.
336
+
337
+ Returns:
338
+ Dictionary with status information
339
+ """
340
+ now = time.time()
341
+ return {
342
+ "state": self.state,
343
+ "failure_count": self.failure_count,
344
+ "time_since_last_failure": now - self.last_failure_time if self.last_failure_time > 0 else None,
345
+ "time_since_last_success": now - self.last_success_time if self.last_success_time > 0 else None,
346
+ "recovery_timeout": self.recovery_timeout,
347
+ "reset_timeout": self.reset_timeout
348
+ }
349
+
350
+ def safe_request(
351
+ url: str,
352
+ method: str = "GET",
353
+ circuit_breaker: Optional[CircuitBreaker] = None,
354
+ error_tracker: Optional[NetworkErrorTracker] = None,
355
+ retry_count: int = 3,
356
+ timeout: float = 30.0,
357
+ **kwargs
358
+ ) -> Optional[requests.Response]:
359
+ """
360
+ Make a safe HTTP request with circuit breaker and retry logic.
361
+
362
+ Args:
363
+ url: URL to request
364
+ method: HTTP method (GET, POST, etc.)
365
+ circuit_breaker: Circuit breaker instance
366
+ error_tracker: Error tracker instance
367
+ retry_count: Number of retries
368
+ timeout: Request timeout in seconds
369
+ **kwargs: Additional arguments for requests
370
+
371
+ Returns:
372
+ Response object or None if request failed
373
+ """
374
+ # Check circuit breaker
375
+ if circuit_breaker and not circuit_breaker.allow_request():
376
+ logger.warning(f"Circuit breaker prevented request to {url}")
377
+ return None
378
+
379
+ # Set default timeout
380
+ kwargs.setdefault("timeout", timeout)
381
+
382
+ # Make request with retry
383
+ response = None
384
+ success = False
385
+
386
+ try:
387
+ for attempt in range(retry_count + 1):
388
+ try:
389
+ response = requests.request(method, url, **kwargs)
390
+ response.raise_for_status()
391
+ success = True
392
+ break
393
+ except NETWORK_EXCEPTIONS as e:
394
+ if attempt < retry_count:
395
+ delay = 2 ** attempt + random.uniform(0, 1)
396
+ logger.warning(f"Request to {url} failed (attempt {attempt+1}/{retry_count+1}): {str(e)}. Retrying in {delay:.2f}s")
397
+ time.sleep(delay)
398
+ else:
399
+ logger.error(f"Request to {url} failed after {retry_count+1} attempts: {str(e)}")
400
+ raise
401
+ except Exception as e:
402
+ logger.error(f"Error making request to {url}: {str(e)}")
403
+ success = False
404
+
405
+ # Update circuit breaker and error tracker
406
+ if circuit_breaker:
407
+ if success:
408
+ circuit_breaker.record_success()
409
+ else:
410
+ circuit_breaker.record_failure()
411
+
412
+ if error_tracker:
413
+ error_tracker.record_request(success)
414
+
415
+ return response if success else None
@@ -0,0 +1,203 @@
1
+ import os
2
+ import logging
3
+ from typing import Dict, Any, List, Optional, Tuple
4
+
5
+ from src.models.code_generator import OpenSCADCodeGenerator
6
+ from src.utils.cad_exporter import CADExporter
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class PrimitiveTester:
11
+ """Tests OpenSCAD primitives with different export formats."""
12
+
13
+ def __init__(self, code_generator: OpenSCADCodeGenerator, cad_exporter: CADExporter,
14
+ output_dir: str = "test_output"):
15
+ """
16
+ Initialize the primitive tester.
17
+
18
+ Args:
19
+ code_generator: CodeGenerator instance for generating OpenSCAD code
20
+ cad_exporter: CADExporter instance for exporting models
21
+ output_dir: Directory to store test output
22
+ """
23
+ self.code_generator = code_generator
24
+ self.cad_exporter = cad_exporter
25
+ self.output_dir = output_dir
26
+
27
+ # Create output directory
28
+ os.makedirs(output_dir, exist_ok=True)
29
+
30
+ # Primitive types to test
31
+ self.primitives = [
32
+ "cube", "sphere", "cylinder", "cone", "torus",
33
+ "rounded_box", "hexagonal_prism", "text"
34
+ ]
35
+
36
+ # Export formats to test (no STL per requirements)
37
+ self.formats = ["3mf", "amf", "csg", "scad"]
38
+
39
+ def test_all_primitives(self) -> Dict[str, Dict[str, Any]]:
40
+ """
41
+ Test all primitives with all formats.
42
+
43
+ Returns:
44
+ Dictionary of test results for each primitive
45
+ """
46
+ results = {}
47
+
48
+ for primitive in self.primitives:
49
+ results[primitive] = self.test_primitive(primitive)
50
+
51
+ return results
52
+
53
+ def test_primitive(self, primitive_type: str) -> Dict[str, Any]:
54
+ """
55
+ Test a single primitive with all formats.
56
+
57
+ Args:
58
+ primitive_type: Type of primitive to test
59
+
60
+ Returns:
61
+ Dictionary of test results for the primitive
62
+ """
63
+ results = {
64
+ "primitive": primitive_type,
65
+ "formats": {}
66
+ }
67
+
68
+ # Generate default parameters for the primitive
69
+ params = self._get_default_parameters(primitive_type)
70
+
71
+ # Generate the SCAD code
72
+ scad_file = self.code_generator.generate_code(primitive_type, params)
73
+
74
+ # Test export to each format
75
+ for format_type in self.formats:
76
+ success, output_file, error = self.cad_exporter.export_model(
77
+ scad_file,
78
+ format_type,
79
+ params,
80
+ metadata={"primitive_type": primitive_type}
81
+ )
82
+
83
+ results["formats"][format_type] = {
84
+ "success": success,
85
+ "output_file": output_file,
86
+ "error": error
87
+ }
88
+
89
+ return results
90
+
91
+ def _get_default_parameters(self, primitive_type: str) -> Dict[str, Any]:
92
+ """
93
+ Get default parameters for a primitive type.
94
+
95
+ Args:
96
+ primitive_type: Type of primitive
97
+
98
+ Returns:
99
+ Dictionary of default parameters
100
+ """
101
+ params = {}
102
+
103
+ if primitive_type == "cube":
104
+ params = {"width": 20, "depth": 20, "height": 20, "center": True}
105
+ elif primitive_type == "sphere":
106
+ params = {"radius": 10, "segments": 32}
107
+ elif primitive_type == "cylinder":
108
+ params = {"radius": 10, "height": 20, "center": True, "segments": 32}
109
+ elif primitive_type == "cone":
110
+ params = {"bottom_radius": 10, "top_radius": 0, "height": 20, "center": True}
111
+ elif primitive_type == "torus":
112
+ params = {"outer_radius": 20, "inner_radius": 5, "segments": 32}
113
+ elif primitive_type == "rounded_box":
114
+ params = {"width": 30, "depth": 20, "height": 15, "radius": 3}
115
+ elif primitive_type == "hexagonal_prism":
116
+ params = {"radius": 10, "height": 20}
117
+ elif primitive_type == "text":
118
+ params = {"text": "OpenSCAD", "size": 10, "height": 3}
119
+
120
+ return params
121
+
122
+ def test_with_parameter_variations(self, primitive_type: str) -> Dict[str, Any]:
123
+ """
124
+ Test a primitive with variations of parameters.
125
+
126
+ Args:
127
+ primitive_type: Type of primitive to test
128
+
129
+ Returns:
130
+ Dictionary of test results for different parameter variations
131
+ """
132
+ results = {
133
+ "primitive": primitive_type,
134
+ "variations": {}
135
+ }
136
+
137
+ # Define parameter variations for the primitive
138
+ variations = self._get_parameter_variations(primitive_type)
139
+
140
+ # Test each variation
141
+ for variation_name, params in variations.items():
142
+ # Generate the SCAD code
143
+ scad_file = self.code_generator.generate_code(primitive_type, params)
144
+
145
+ # Test export to each format
146
+ format_results = {}
147
+ for format_type in self.formats:
148
+ success, output_file, error = self.cad_exporter.export_model(
149
+ scad_file,
150
+ format_type,
151
+ params,
152
+ metadata={"primitive_type": primitive_type, "variation": variation_name}
153
+ )
154
+
155
+ format_results[format_type] = {
156
+ "success": success,
157
+ "output_file": output_file,
158
+ "error": error
159
+ }
160
+
161
+ results["variations"][variation_name] = {
162
+ "parameters": params,
163
+ "formats": format_results
164
+ }
165
+
166
+ return results
167
+
168
+ def _get_parameter_variations(self, primitive_type: str) -> Dict[str, Dict[str, Any]]:
169
+ """
170
+ Get parameter variations for a primitive type.
171
+
172
+ Args:
173
+ primitive_type: Type of primitive
174
+
175
+ Returns:
176
+ Dictionary of parameter variations
177
+ """
178
+ variations = {}
179
+
180
+ if primitive_type == "cube":
181
+ variations = {
182
+ "small": {"width": 5, "depth": 5, "height": 5, "center": True},
183
+ "large": {"width": 50, "depth": 50, "height": 50, "center": True},
184
+ "flat": {"width": 50, "depth": 50, "height": 2, "center": True},
185
+ "tall": {"width": 10, "depth": 10, "height": 100, "center": True}
186
+ }
187
+ elif primitive_type == "sphere":
188
+ variations = {
189
+ "small": {"radius": 2, "segments": 16},
190
+ "large": {"radius": 30, "segments": 64},
191
+ "low_res": {"radius": 10, "segments": 8},
192
+ "high_res": {"radius": 10, "segments": 128}
193
+ }
194
+ elif primitive_type == "cylinder":
195
+ variations = {
196
+ "small": {"radius": 2, "height": 5, "center": True, "segments": 16},
197
+ "large": {"radius": 30, "height": 50, "center": True, "segments": 64},
198
+ "thin": {"radius": 1, "height": 50, "center": True, "segments": 32},
199
+ "disc": {"radius": 30, "height": 2, "center": True, "segments": 32}
200
+ }
201
+ # Add variations for other primitives as needed
202
+
203
+ return variations