cad-server 0.10.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 (247) hide show
  1. cad_server/__init__.py +54 -0
  2. cad_server/_internal/__init__.py +1 -0
  3. cad_server/_internal/api.py +19 -0
  4. cad_server/_internal/apps.py +319 -0
  5. cad_server/_internal/attach.py +67 -0
  6. cad_server/_internal/base.py +682 -0
  7. cad_server/_internal/channel.py +165 -0
  8. cad_server/_internal/cli_errors.py +91 -0
  9. cad_server/_internal/cli_from_function.py +489 -0
  10. cad_server/_internal/editable.py +43 -0
  11. cad_server/_internal/engine_daemon.py +6 -0
  12. cad_server/_internal/engine_host.py +82 -0
  13. cad_server/_internal/engine_telemetry.py +206 -0
  14. cad_server/_internal/extras.py +577 -0
  15. cad_server/_internal/file_lock.py +70 -0
  16. cad_server/_internal/installations.py +637 -0
  17. cad_server/_internal/kernel_load_hint.py +125 -0
  18. cad_server/_internal/launch.py +48 -0
  19. cad_server/_internal/leftovers.py +391 -0
  20. cad_server/_internal/picker.py +127 -0
  21. cad_server/_internal/plugin.py +388 -0
  22. cad_server/_internal/primitive_mesh.py +245 -0
  23. cad_server/_internal/providers.py +2013 -0
  24. cad_server/_internal/proxy.py +94 -0
  25. cad_server/_internal/releases.py +211 -0
  26. cad_server/_internal/shared_read.py +62 -0
  27. cad_server/_internal/switch.py +304 -0
  28. cad_server/_internal/telemetry_send.py +108 -0
  29. cad_server/_internal/update.py +157 -0
  30. cad_server/_runtime/mcp/THIRD_PARTY_LICENSES.txt +1895 -0
  31. cad_server/_runtime/mcp/app.js +9 -0
  32. cad_server/_runtime/mcp/shell.html +13 -0
  33. cad_server/_runtime/plugin/.claude-plugin/icon.png +0 -0
  34. cad_server/_runtime/plugin/.claude-plugin/marketplace.json +35 -0
  35. cad_server/_runtime/plugin/.claude-plugin/plugin.json +28 -0
  36. cad_server/_runtime/plugin/.codex-plugin/logo.png +0 -0
  37. cad_server/_runtime/plugin/.codex-plugin/plugin.json +47 -0
  38. cad_server/_runtime/plugin/.cursor-plugin/plugin.json +29 -0
  39. cad_server/_runtime/plugin/LICENSE +21 -0
  40. cad_server/_runtime/plugin/claude.mcp.json +19 -0
  41. cad_server/_runtime/plugin/codex.mcp.json +20 -0
  42. cad_server/_runtime/plugin/cursor.mcp.json +20 -0
  43. cad_server/_runtime/plugin/gemini-extension.json +22 -0
  44. cad_server/_runtime/plugin/mcp.json +21 -0
  45. cad_server/_runtime/plugin/plugin.json +23 -0
  46. cad_server/_runtime/plugin/skills/bambu-labs/LICENSE +21 -0
  47. cad_server/_runtime/plugin/skills/bambu-labs/SKILL.md +67 -0
  48. cad_server/_runtime/plugin/skills/bambu-labs/agents/openai.yaml +4 -0
  49. cad_server/_runtime/plugin/skills/cad/LICENSE +21 -0
  50. cad_server/_runtime/plugin/skills/cad/SKILL.md +361 -0
  51. cad_server/_runtime/plugin/skills/cad/agents/openai.yaml +4 -0
  52. cad_server/_runtime/plugin/skills/cad/references/animation.md +139 -0
  53. cad_server/_runtime/plugin/skills/cad/references/build123d-modeling.md +159 -0
  54. cad_server/_runtime/plugin/skills/cad/references/exports.md +180 -0
  55. cad_server/_runtime/plugin/skills/cad/references/inspection-and-validation.md +340 -0
  56. cad_server/_runtime/plugin/skills/cad/references/kinematics.md +316 -0
  57. cad_server/_runtime/plugin/skills/cad/references/model-contract.md +380 -0
  58. cad_server/_runtime/plugin/skills/cad/references/positioning.md +118 -0
  59. cad_server/_runtime/plugin/skills/cad/references/project-layout.md +174 -0
  60. cad_server/_runtime/plugin/skills/cad/references/snapshot-review.md +248 -0
  61. cad_server/_runtime/plugin/skills/dfam-check/LICENSE +21 -0
  62. cad_server/_runtime/plugin/skills/dfam-check/SKILL.md +106 -0
  63. cad_server/_runtime/plugin/skills/dfam-check/agents/openai.yaml +4 -0
  64. cad_server/_runtime/plugin/skills/dfam-check/references/process-limits.md +58 -0
  65. cad_server/_runtime/plugin/skills/dfam-check/requirements.txt +11 -0
  66. cad_server/_runtime/plugin/skills/dfam-check/scripts/dfam_tool.py +399 -0
  67. cad_server/_runtime/plugin/skills/dfm/LICENSE +21 -0
  68. cad_server/_runtime/plugin/skills/dfm/SKILL.md +105 -0
  69. cad_server/_runtime/plugin/skills/dfm/agents/openai.yaml +4 -0
  70. cad_server/_runtime/plugin/skills/dfm/references/cnc.md +52 -0
  71. cad_server/_runtime/plugin/skills/dfm/references/injection-molding.md +173 -0
  72. cad_server/_runtime/plugin/skills/dfm/references/sheet-metal.md +47 -0
  73. cad_server/_runtime/plugin/skills/dfm/requirements.txt +10 -0
  74. cad_server/_runtime/plugin/skills/dfm/scripts/mold_tool.py +1039 -0
  75. cad_server/_runtime/plugin/skills/dxf/LICENSE +21 -0
  76. cad_server/_runtime/plugin/skills/dxf/SKILL.md +358 -0
  77. cad_server/_runtime/plugin/skills/dxf/agents/openai.yaml +4 -0
  78. cad_server/_runtime/plugin/skills/dxf/references/generator-templates.md +187 -0
  79. cad_server/_runtime/plugin/skills/engineering-drawing/LICENSE +21 -0
  80. cad_server/_runtime/plugin/skills/engineering-drawing/SKILL.md +137 -0
  81. cad_server/_runtime/plugin/skills/engineering-drawing/agents/openai.yaml +4 -0
  82. cad_server/_runtime/plugin/skills/engineering-drawing/references/sheet-api.md +142 -0
  83. cad_server/_runtime/plugin/skills/gcode/LICENSE +21 -0
  84. cad_server/_runtime/plugin/skills/gcode/SKILL.md +91 -0
  85. cad_server/_runtime/plugin/skills/gcode/agents/openai.yaml +4 -0
  86. cad_server/_runtime/plugin/skills/gcode/scripts/orca_presets.py +134 -0
  87. cad_server/_runtime/plugin/skills/sdf/LICENSE +21 -0
  88. cad_server/_runtime/plugin/skills/sdf/SKILL.md +193 -0
  89. cad_server/_runtime/plugin/skills/sdf/agents/openai.yaml +4 -0
  90. cad_server/_runtime/plugin/skills/sdf/references/design-ledger.md +113 -0
  91. cad_server/_runtime/plugin/skills/sdf/references/examples.md +141 -0
  92. cad_server/_runtime/plugin/skills/sdf/references/frame-semantics.md +87 -0
  93. cad_server/_runtime/plugin/skills/sdf/references/interoperability.md +78 -0
  94. cad_server/_runtime/plugin/skills/sdf/references/llm-guardrails.md +107 -0
  95. cad_server/_runtime/plugin/skills/sdf/references/sdf-workflow.md +80 -0
  96. cad_server/_runtime/plugin/skills/sdf/references/smoke-tests.md +99 -0
  97. cad_server/_runtime/plugin/skills/sdf/references/validation.md +161 -0
  98. cad_server/_runtime/plugin/skills/sendcutsend/LICENSE +21 -0
  99. cad_server/_runtime/plugin/skills/sendcutsend/SKILL.md +117 -0
  100. cad_server/_runtime/plugin/skills/sendcutsend/agents/openai.yaml +4 -0
  101. cad_server/_runtime/plugin/skills/sendcutsend/references/official-sources.md +40 -0
  102. cad_server/_runtime/plugin/skills/sendcutsend/references/report-template.md +40 -0
  103. cad_server/_runtime/plugin/skills/srdf/LICENSE +21 -0
  104. cad_server/_runtime/plugin/skills/srdf/SKILL.md +168 -0
  105. cad_server/_runtime/plugin/skills/srdf/agents/openai.yaml +4 -0
  106. cad_server/_runtime/plugin/skills/srdf/references/authoring-contract.md +70 -0
  107. cad_server/_runtime/plugin/skills/srdf/references/disabled-collisions.md +55 -0
  108. cad_server/_runtime/plugin/skills/srdf/references/end-effectors.md +50 -0
  109. cad_server/_runtime/plugin/skills/srdf/references/planning-ledger.md +102 -0
  110. cad_server/_runtime/plugin/skills/srdf/references/srdf-workflow.md +38 -0
  111. cad_server/_runtime/plugin/skills/srdf/references/validation.md +44 -0
  112. cad_server/_runtime/plugin/skills/step-parts/LICENSE +21 -0
  113. cad_server/_runtime/plugin/skills/step-parts/SKILL.md +68 -0
  114. cad_server/_runtime/plugin/skills/step-parts/agents/openai.yaml +4 -0
  115. cad_server/_runtime/plugin/skills/step-parts/references/step-parts-api.md +75 -0
  116. cad_server/_runtime/plugin/skills/step-parts/scripts/download_step_part.py +230 -0
  117. cad_server/_runtime/plugin/skills/urdf/LICENSE +21 -0
  118. cad_server/_runtime/plugin/skills/urdf/SKILL.md +156 -0
  119. cad_server/_runtime/plugin/skills/urdf/agents/openai.yaml +4 -0
  120. cad_server/_runtime/plugin/skills/urdf/references/authoring-contract.md +167 -0
  121. cad_server/_runtime/plugin/skills/urdf/references/design-ledger.md +112 -0
  122. cad_server/_runtime/plugin/skills/urdf/references/frame-semantics.md +105 -0
  123. cad_server/_runtime/plugin/skills/urdf/references/inertials.md +63 -0
  124. cad_server/_runtime/plugin/skills/urdf/references/meshes.md +49 -0
  125. cad_server/_runtime/plugin/skills/urdf/references/urdf-workflow.md +79 -0
  126. cad_server/_runtime/plugin/skills/urdf/references/validation.md +67 -0
  127. cad_server/_runtime/viewer/THIRD_PARTY_LICENSES.txt +1895 -0
  128. cad_server/_runtime/viewer/assets/BufferGeometryUtils-BO3Ss---.js +1 -0
  129. cad_server/_runtime/viewer/assets/DxfRenderer-CK-C88pp.js +6 -0
  130. cad_server/_runtime/viewer/assets/GLTFLoader-C-ZwhOwN.js +1 -0
  131. cad_server/_runtime/viewer/assets/GlbRenderer-CwtvgnsN.js +2 -0
  132. cad_server/_runtime/viewer/assets/MeshRenderer-CtKo5N8-.js +2 -0
  133. cad_server/_runtime/viewer/assets/RobotRenderer-D4v9vkun.js +2 -0
  134. cad_server/_runtime/viewer/assets/STLLoader-COjUjMBk.js +1 -0
  135. cad_server/_runtime/viewer/assets/SceneMeasure-DWhVhT3U.js +2 -0
  136. cad_server/_runtime/viewer/assets/VirtualRows-CcpJpJrq.js +2 -0
  137. cad_server/_runtime/viewer/assets/XbfRenderer-Cz77kpw3.js +8 -0
  138. cad_server/_runtime/viewer/assets/entryAssets-DMPboejr.js +2 -0
  139. cad_server/_runtime/viewer/assets/environmentMap-C2AYeUDk.js +2 -0
  140. cad_server/_runtime/viewer/assets/favicon-DwiaIWoe.png +0 -0
  141. cad_server/_runtime/viewer/assets/glbMeshWorker-DH0X5ko0.js +7 -0
  142. cad_server/_runtime/viewer/assets/index-BBVuT0Je.css +2 -0
  143. cad_server/_runtime/viewer/assets/index-KO9e-SAu.js +27 -0
  144. cad_server/_runtime/viewer/assets/loading-OUGzZRP4.avif +0 -0
  145. cad_server/_runtime/viewer/assets/loading-still-DWWO9OwM.webp +0 -0
  146. cad_server/_runtime/viewer/assets/meshObjects-BHfBp4CD.js +2 -0
  147. cad_server/_runtime/viewer/assets/meshopt_decoder-4Ef-c7zw.js +1 -0
  148. cad_server/_runtime/viewer/assets/photographicStudio-D1RYrfjy.js +475 -0
  149. cad_server/_runtime/viewer/assets/raycastBvhWorker-D5dKYzM6.js +6 -0
  150. cad_server/_runtime/viewer/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  151. cad_server/_runtime/viewer/assets/sceneExplode-DP8HU56N.js +2 -0
  152. cad_server/_runtime/viewer/assets/sceneHighlight-C5lLZODo.js +2 -0
  153. cad_server/_runtime/viewer/assets/sharedGeometry-BsLw_hx-.js +225 -0
  154. cad_server/_runtime/viewer/assets/stlMeshWorker-CuNjKOUV.js +1 -0
  155. cad_server/_runtime/viewer/assets/studioEnvironment.worker-utbnN6Yk.js +4231 -0
  156. cad_server/_runtime/viewer/assets/three.core-DdnNoxhu.js +6 -0
  157. cad_server/_runtime/viewer/assets/toolModes-Cc3imgJQ.js +66 -0
  158. cad_server/_runtime/viewer/assets/useSceneSelection-CqIqnfow.js +2 -0
  159. cad_server/_runtime/viewer/assets/vendor-icons-BmP3iO1o.js +2 -0
  160. cad_server/_runtime/viewer/assets/vendor-react-iRJfyluG.js +10 -0
  161. cad_server/_runtime/viewer/assets/vendor-three-DStQQdKw.js +4613 -0
  162. cad_server/_runtime/viewer/assets/vendor-ui-Cgj57w_6.js +47 -0
  163. cad_server/_runtime/viewer/index.html +39 -0
  164. cad_server/analytics.py +1865 -0
  165. cad_server/assets.py +157 -0
  166. cad_server/cli/__init__.py +277 -0
  167. cad_server/cli/__main__.py +10 -0
  168. cad_server/cli/delete.py +30 -0
  169. cad_server/cli/doctor.py +570 -0
  170. cad_server/cli/export.py +30 -0
  171. cad_server/cli/inspect.py +30 -0
  172. cad_server/cli/plugin.py +79 -0
  173. cad_server/cli/run.py +105 -0
  174. cad_server/cli/sdf_validate.py +30 -0
  175. cad_server/cli/serve.py +30 -0
  176. cad_server/cli/server.py +29 -0
  177. cad_server/cli/setup.py +1054 -0
  178. cad_server/cli/snapshot.py +30 -0
  179. cad_server/cli/srdf_validate.py +30 -0
  180. cad_server/cli/status.py +30 -0
  181. cad_server/cli/stop.py +29 -0
  182. cad_server/cli/telemetry.py +90 -0
  183. cad_server/cli/uninstall.py +29 -0
  184. cad_server/cli/update.py +65 -0
  185. cad_server/cli/urdf_validate.py +83 -0
  186. cad_server/cli/viewer.py +22 -0
  187. cad_server/drawing_payload.py +683 -0
  188. cad_server/engine/README.md +3131 -0
  189. cad_server/engine/__init__.py +3 -0
  190. cad_server/engine/__main__.py +7 -0
  191. cad_server/engine/cache.py +391 -0
  192. cad_server/engine/cli.py +823 -0
  193. cad_server/engine/daemon.py +1818 -0
  194. cad_server/engine/models.py +799 -0
  195. cad_server/engine/registry.py +326 -0
  196. cad_server/engine/renders.py +155 -0
  197. cad_server/engine/services.py +79 -0
  198. cad_server/engine/supervisor.py +1126 -0
  199. cad_server/jobs.py +153 -0
  200. cad_server/language.py +47 -0
  201. cad_server/mcp/__init__.py +0 -0
  202. cad_server/mcp/browser.py +43 -0
  203. cad_server/mcp/host.py +60 -0
  204. cad_server/mcp/http.py +230 -0
  205. cad_server/mcp/operations.py +374 -0
  206. cad_server/mcp/protocol.py +39 -0
  207. cad_server/mcp/server.py +1157 -0
  208. cad_server/mcp/sidebar.py +52 -0
  209. cad_server/mcp/tunnel.py +248 -0
  210. cad_server/mcp/ui.py +120 -0
  211. cad_server/mcp/views.py +202 -0
  212. cad_server/py.typed +0 -0
  213. cad_server/robot_payload.py +1768 -0
  214. cad_server/settings.py +83 -0
  215. cad_server/updates.py +252 -0
  216. cad_server/viewer/__init__.py +11 -0
  217. cad_server/viewer/__main__.py +7 -0
  218. cad_server/viewer/backend.py +50 -0
  219. cad_server/viewer/browser_open.py +107 -0
  220. cad_server/viewer/clipboard.py +28 -0
  221. cad_server/viewer/collation.json +1 -0
  222. cad_server/viewer/content_types.py +66 -0
  223. cad_server/viewer/documents.py +143 -0
  224. cad_server/viewer/drawings.py +157 -0
  225. cad_server/viewer/encoding.py +173 -0
  226. cad_server/viewer/engine_app.py +470 -0
  227. cad_server/viewer/exports.py +297 -0
  228. cad_server/viewer/folders.py +135 -0
  229. cad_server/viewer/handler.py +365 -0
  230. cad_server/viewer/http_app.py +1342 -0
  231. cad_server/viewer/kinds.py +45 -0
  232. cad_server/viewer/main.py +78 -0
  233. cad_server/viewer/model_catalog.py +37 -0
  234. cad_server/viewer/natural_sort.py +218 -0
  235. cad_server/viewer/prompt_files.py +234 -0
  236. cad_server/viewer/recents.py +254 -0
  237. cad_server/viewer/response.py +270 -0
  238. cad_server/viewer/reveal.py +49 -0
  239. cad_server/viewer/robots.py +98 -0
  240. cad_server/viewer/scanner.py +320 -0
  241. cad_server/viewer/url_norm.py +160 -0
  242. cad_server-0.10.0.dist-info/METADATA +156 -0
  243. cad_server-0.10.0.dist-info/RECORD +247 -0
  244. cad_server-0.10.0.dist-info/WHEEL +5 -0
  245. cad_server-0.10.0.dist-info/entry_points.txt +2 -0
  246. cad_server-0.10.0.dist-info/licenses/LICENSE +21 -0
  247. cad_server-0.10.0.dist-info/top_level.txt +1 -0
cad_server/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ """The CAD server: the device's one daemon that builds, keeps and serves documents, the CAD Viewer's and
2
+ the CAD app's servers on its listener, every verb of the ``cadgen`` command, the environments its kernel
3
+ processes run in, the installations and the one that is current, and telemetry.
4
+
5
+ It imports the library's (``cad``) kernel-free modules and never the CAD kernel: the kernel lives in the
6
+ environment models run in (``_internal/base.py``). Importing it installs what the server attaches to the
7
+ library: the engine's verbs a server process runs are counted (``_internal/engine_telemetry.py``); a
8
+ model's own process, where the server is not installed, counts nothing. Each is attached as the engine's
9
+ module is imported, never sooner (``_internal/attach.py``): importing the server imports no engine.
10
+ """
11
+
12
+ from functools import cache as _cache
13
+
14
+ from cad_server._internal import attach as _attach
15
+ from cad_server._internal import engine_telemetry as _engine_telemetry
16
+
17
+ _engine_telemetry.install()
18
+ del _engine_telemetry
19
+
20
+
21
+ def _unregistered() -> str:
22
+ """What a client in a process of cad-server's own says where no installation is registered: not the
23
+ library's line, which would tell a running cad-server that cad-server is not installed, but this one's
24
+ (``cad_server._internal.installations.unregistered``)."""
25
+ from cad_server._internal.installations import unregistered
26
+
27
+ return unregistered()
28
+
29
+
30
+ def _say_unregistered(said) -> None:
31
+ said.UNREGISTERED = _unregistered
32
+
33
+
34
+ _attach.when("cad.engine.said", _say_unregistered)
35
+ del _attach
36
+
37
+ __all__ = ["__version__"]
38
+
39
+
40
+ def __getattr__(name: str):
41
+ if name == "__version__":
42
+ return _version()
43
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
44
+
45
+
46
+ @_cache
47
+ def _version() -> str:
48
+ """The version declared beside this very package (``cad._internal.version.declared_version``): the
49
+ source tree's pyproject for a checkout, the dist-info beside it for an install."""
50
+ import pathlib
51
+
52
+ from cad._internal.version import declared_version
53
+
54
+ return declared_version(pathlib.Path(__file__).resolve().parent, "cad-server")
@@ -0,0 +1 @@
1
+ """The server's private modules: no import stability is promised between releases."""
@@ -0,0 +1,19 @@
1
+ """api.texttocad.dev: the one service text-to-cad talks to.
2
+
3
+ Two things use it: telemetry (``cad_server/analytics.py``: ``/events`` and ``/forget``) and the hourly
4
+ version check (``cad_server/updates.py``: ``/versions``). It is ours,
5
+ so the service behind it can change without a release. ``CAD_API_URL`` points both at another
6
+ -- a local receiver -- when it is http(s).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ API = "https://api.texttocad.dev/v1"
14
+
15
+
16
+ def api_url() -> str:
17
+ """The service's base: ``CAD_API_URL`` when it is http(s), else ours."""
18
+ override = str(os.environ.get("CAD_API_URL") or "").strip().rstrip("/")
19
+ return override if override.startswith(("https://", "http://")) else API
@@ -0,0 +1,319 @@
1
+ """Open in: the apps on this computer that open a document's files, found offline, and the opening.
2
+
3
+ A page's Open in menu (``GET /api/v1/local/open-in``, ``POST /api/v1/local/open``: the viewer's
4
+ ``http_app``) offers, for each file of a document, the apps of ``APPS`` that take its kind
5
+ (``KINDS``) and that this computer has. Which it has is found without the network and never on a
6
+ request: ``DETECTED`` looks in the background -- once as the server starts (``warm``: the folders
7
+ alone), then on use, at most every ``REFRESH_SECONDS`` -- and a request reads what it last found.
8
+
9
+ Where each platform's apps are, by the platform's own conventions, each function taking where to
10
+ look as arguments so every platform's lookup runs anywhere:
11
+
12
+ * macOS (``mac``): a bundle of a known name in ``/Applications``, ``/Applications/Setapp`` or
13
+ ``~/Applications`` (JetBrains Toolbox's PyCharm among them), else -- on use, never as the server
14
+ starts -- one Spotlight query (``mdfind``, ``SPOTLIGHT_SECONDS`` at most) for the bundles of the
15
+ known identifiers wherever they are.
16
+ * Windows (``windows``): the App Paths registry (the user's, then the machine's), else the
17
+ installers' own folders under ``%LOCALAPPDATA%`` and ``%ProgramFiles%``.
18
+ * Linux (``linux``): each app's command in one of PATH's absolute folders.
19
+
20
+ ``open_file`` starts the app on the file, or the operating system's own handler for it: detached,
21
+ never waited on, never through a shell, its output discarded. It is given a path and an app the
22
+ server found itself; nothing of a request is ever a command.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import glob
27
+ import os
28
+ import plistlib
29
+ import subprocess
30
+ import sys
31
+ import threading
32
+ import time
33
+ from collections.abc import Callable, Iterable, Mapping
34
+
35
+ from cad.engine import executables
36
+
37
+ __all__ = ["APPS", "DETECTED", "KINDS", "REFRESH_SECONDS", "Detected", "apps_for", "detect", "file_manager",
38
+ "linux", "mac", "open_file", "windows"]
39
+
40
+ # Every app Open in offers: its id and the name the menu shows.
41
+ APPS = {"vscode": "Visual Studio Code", "cursor": "Cursor", "zed": "Zed", "pycharm": "PyCharm",
42
+ "sublime": "Sublime Text", "fusion": "Autodesk Fusion", "freecad": "FreeCAD", "bambu": "Bambu Studio",
43
+ "orca": "OrcaSlicer", "prusa": "PrusaSlicer", "blender": "Blender"}
44
+ # The apps that take each kind of file, in the menu's order. A kind not here takes none: the file
45
+ # manager and the default app alone.
46
+ _MESHES = ("bambu", "orca", "prusa", "freecad", "blender")
47
+ KINDS = {"script": ("vscode", "cursor", "zed", "pycharm", "sublime"),
48
+ "step": ("fusion", "freecad", "bambu", "orca", "prusa"),
49
+ "stl": _MESHES, "3mf": _MESHES, "glb": ("blender",)}
50
+
51
+ REFRESH_SECONDS = 300.0
52
+ SPOTLIGHT_SECONDS = 2.0
53
+ # Windows' process creation flags (``subprocess``'s own, which it defines on Windows alone).
54
+ _DETACHED_PROCESS = 0x00000008
55
+ _CREATE_NEW_PROCESS_GROUP = 0x00000200
56
+
57
+ # macOS: each app's bundles, as globs under each folder of ``MAC_FOLDERS``, in order of preference,
58
+ # and its bundle identifiers, which Spotlight finds a bundle by wherever it is.
59
+ MAC_BUNDLES = {
60
+ "vscode": ("Visual Studio Code.app",),
61
+ "cursor": ("Cursor.app",),
62
+ "zed": ("Zed.app",),
63
+ "pycharm": ("PyCharm.app", "PyCharm CE.app", "PyCharm Professional Edition.app", "PyCharm*.app",
64
+ "JetBrains Toolbox/PyCharm*.app"),
65
+ "sublime": ("Sublime Text.app",),
66
+ "fusion": ("Autodesk Fusion.app", "Autodesk Fusion 360.app"),
67
+ "freecad": ("FreeCAD.app",),
68
+ "bambu": ("BambuStudio.app", "Bambu Studio.app"),
69
+ "orca": ("OrcaSlicer.app",),
70
+ "prusa": ("PrusaSlicer.app", "Original Prusa Drivers/PrusaSlicer.app"),
71
+ "blender": ("Blender.app",),
72
+ }
73
+ MAC_IDS = {
74
+ "vscode": ("com.microsoft.VSCode",),
75
+ "cursor": ("com.todesktop.230313mzl4w4u92",),
76
+ "zed": ("dev.zed.Zed",),
77
+ "pycharm": ("com.jetbrains.pycharm", "com.jetbrains.pycharm.ce"),
78
+ "sublime": ("com.sublimetext.4", "com.sublimetext.3"),
79
+ "fusion": ("com.autodesk.fusion360",),
80
+ "freecad": ("org.freecad.FreeCAD", "org.freecadweb.FreeCAD"),
81
+ "bambu": ("com.bambulab.bambu-studio",),
82
+ "orca": ("com.orcaslicer.OrcaSlicer", "com.softfever3d.orca-slicer"),
83
+ "prusa": ("com.prusa3d.slic3r",),
84
+ "blender": ("org.blenderfoundation.blender",),
85
+ }
86
+ MAC_FOLDERS = ("/Applications", "/Applications/Setapp", "~/Applications")
87
+
88
+ # Windows: each app's executable as App Paths names it, then where its installers put it, each
89
+ # under the folder an environment variable names (globs: the newest file a glob finds).
90
+ WINDOWS_EXES = {"vscode": "Code.exe", "cursor": "Cursor.exe", "zed": "Zed.exe", "pycharm": "pycharm64.exe",
91
+ "sublime": "sublime_text.exe", "fusion": "Fusion360.exe", "freecad": "FreeCAD.exe",
92
+ "bambu": "bambu-studio.exe", "orca": "orca-slicer.exe", "prusa": "prusa-slicer.exe",
93
+ "blender": "blender.exe"}
94
+ WINDOWS_FOLDERS = {
95
+ "vscode": (("LOCALAPPDATA", "Programs", "Microsoft VS Code", "Code.exe"),
96
+ ("ProgramFiles", "Microsoft VS Code", "Code.exe")),
97
+ "cursor": (("LOCALAPPDATA", "Programs", "cursor", "Cursor.exe"), ("ProgramFiles", "Cursor", "Cursor.exe")),
98
+ "zed": (("LOCALAPPDATA", "Programs", "Zed", "Zed.exe"), ("ProgramFiles", "Zed", "Zed.exe")),
99
+ "pycharm": (("LOCALAPPDATA", "Programs", "PyCharm*", "bin", "pycharm64.exe"),
100
+ ("ProgramFiles", "JetBrains", "PyCharm*", "bin", "pycharm64.exe")),
101
+ "sublime": (("ProgramFiles", "Sublime Text", "sublime_text.exe"),
102
+ ("ProgramFiles", "Sublime Text 3", "sublime_text.exe")),
103
+ "fusion": (("LOCALAPPDATA", "Autodesk", "webdeploy", "production", "*", "Fusion360.exe"),),
104
+ "freecad": (("LOCALAPPDATA", "Programs", "FreeCAD*", "bin", "FreeCAD.exe"),
105
+ ("ProgramFiles", "FreeCAD*", "bin", "FreeCAD.exe")),
106
+ "bambu": (("ProgramFiles", "Bambu Studio", "bambu-studio.exe"),),
107
+ "orca": (("ProgramFiles", "OrcaSlicer", "orca-slicer.exe"),),
108
+ "prusa": (("ProgramFiles", "Prusa3D", "PrusaSlicer", "prusa-slicer.exe"),),
109
+ "blender": (("ProgramFiles", "Blender Foundation", "Blender*", "blender.exe"),),
110
+ }
111
+ _APP_PATHS = r"Software\Microsoft\Windows\CurrentVersion\App Paths"
112
+
113
+ # Linux: each app's commands, in order of preference (Fusion has no Linux build).
114
+ LINUX_COMMANDS = {"vscode": ("code",), "cursor": ("cursor",), "zed": ("zed", "zeditor"),
115
+ "pycharm": ("pycharm", "pycharm-community", "pycharm-professional"), "sublime": ("subl",),
116
+ "freecad": ("freecad", "FreeCAD"), "bambu": ("bambu-studio",), "orca": ("orca-slicer",),
117
+ "prusa": ("prusa-slicer",), "blender": ("blender",)}
118
+
119
+
120
+ def apps_for(kind: str, found: Mapping[str, str]) -> list[dict[str, str]]:
121
+ """The apps of ``found`` (``detect``'s) that take files of ``kind``, in the menu's order."""
122
+ return [{"id": app, "name": APPS[app]} for app in KINDS.get(kind, ()) if app in found]
123
+
124
+
125
+ def file_manager(platform: str = sys.platform) -> str:
126
+ """The desktop's file manager, as the menu names it."""
127
+ return {"darwin": "finder", "win32": "explorer"}.get(platform, "files")
128
+
129
+
130
+ # --- macOS -------------------------------------------------------------------------------------
131
+
132
+
133
+ def mac(folders: Iterable[str | os.PathLike], spotlight: Callable[[list[str]], list[str]] | None = None) -> dict[str, str]:
134
+ """Each app's bundle: the first of its known names in the first folder that holds one, else,
135
+ with ``spotlight`` (bundle identifiers in, the paths of bundles found out), one it finds."""
136
+ found: dict[str, str] = {}
137
+ for app, names in MAC_BUNDLES.items():
138
+ for folder in folders:
139
+ root = glob.escape(os.path.expanduser(os.fspath(folder)))
140
+ hits = [path for name in names for path in sorted(glob.glob(os.path.join(root, name))) if os.path.isdir(path)]
141
+ if hits:
142
+ found[app] = hits[0]
143
+ break
144
+ missing = {ident: app for app, idents in MAC_IDS.items() if app not in found for ident in idents}
145
+ if spotlight is not None and missing:
146
+ for path in spotlight(list(missing)):
147
+ app = missing.get(_bundle_id(path) or "")
148
+ if app is not None and app not in found and os.path.isdir(path):
149
+ found[app] = path
150
+ return found
151
+
152
+
153
+ def _bundle_id(bundle: str) -> str | None:
154
+ try:
155
+ with open(os.path.join(bundle, "Contents", "Info.plist"), "rb") as handle:
156
+ ident = plistlib.load(handle).get("CFBundleIdentifier")
157
+ except (OSError, ValueError, plistlib.InvalidFileException, AttributeError):
158
+ return None
159
+ return ident if isinstance(ident, str) else None
160
+
161
+
162
+ def _spotlight(idents: list[str]) -> list[str]:
163
+ """The bundles Spotlight knows of ``idents``: one query, ``SPOTLIGHT_SECONDS`` at most."""
164
+ query = " || ".join(f"kMDItemCFBundleIdentifier == '{ident}'" for ident in idents)
165
+ try:
166
+ done = subprocess.run(["/usr/bin/mdfind", query], capture_output=True, text=True, timeout=SPOTLIGHT_SECONDS,
167
+ stdin=subprocess.DEVNULL)
168
+ except (OSError, subprocess.SubprocessError):
169
+ return []
170
+ return [line for line in done.stdout.splitlines() if line.endswith(".app")]
171
+
172
+
173
+ # --- Windows -----------------------------------------------------------------------------------
174
+
175
+
176
+ def windows(environ: Mapping[str, str], app_path: Callable[[str], str | None]) -> dict[str, str]:
177
+ """Each app's executable: where App Paths (``app_path``: an executable's name in, its path or
178
+ None out) says it is, else in its installers' folders under ``environ``'s."""
179
+ found: dict[str, str] = {}
180
+ for app, exe in WINDOWS_EXES.items():
181
+ registered = app_path(exe)
182
+ if registered and os.path.isfile(registered):
183
+ found[app] = registered
184
+ continue
185
+ for variable, *parts in WINDOWS_FOLDERS.get(app, ()):
186
+ base = environ.get(variable)
187
+ if not base or not os.path.isabs(base):
188
+ continue
189
+ hits = [path for path in glob.glob(os.path.join(glob.escape(base), *parts)) if os.path.isfile(path)]
190
+ if hits:
191
+ found[app] = max(hits, key=_modified)
192
+ break
193
+ return found
194
+
195
+
196
+ def _modified(path: str) -> float:
197
+ try:
198
+ return os.stat(path).st_mtime
199
+ except OSError:
200
+ return 0.0
201
+
202
+
203
+ def _registry_app_path(exe: str) -> str | None:
204
+ """The path App Paths gives ``exe``: the user's key, then the machine's."""
205
+ import winreg # type: ignore[import-not-found]
206
+
207
+ for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
208
+ try:
209
+ with winreg.OpenKey(hive, f"{_APP_PATHS}\\{exe}") as key:
210
+ value, _kind = winreg.QueryValueEx(key, None)
211
+ except OSError:
212
+ continue
213
+ if isinstance(value, str) and value.strip():
214
+ return os.path.expandvars(value.strip().strip('"'))
215
+ return None
216
+
217
+
218
+ # --- Linux -------------------------------------------------------------------------------------
219
+
220
+
221
+ def linux(find: Callable[[str], str | None]) -> dict[str, str]:
222
+ """Each app's command, by its absolute path (``find``: a bare name in, its path or None out)."""
223
+ found: dict[str, str] = {}
224
+ for app, commands in LINUX_COMMANDS.items():
225
+ for command in commands:
226
+ path = find(command)
227
+ if path:
228
+ found[app] = path
229
+ break
230
+ return found
231
+
232
+
233
+ def detect(platform: str = sys.platform, *, spotlight: bool = True) -> dict[str, str]:
234
+ """This computer's apps of ``APPS``, each by the path it is opened with. Runs a command only on
235
+ macOS, and only with ``spotlight``: the one query for bundles outside the usual folders."""
236
+ if platform == "darwin":
237
+ return mac(MAC_FOLDERS, _spotlight if spotlight else None)
238
+ if platform == "win32":
239
+ return windows(os.environ, _registry_app_path)
240
+ return linux(executables.find)
241
+
242
+
243
+ class Detected:
244
+ """What ``detect`` last found, read by every request and never computed on one: ``warm`` looks in the
245
+ background as the server starts (no command), and ``found`` starts a background look on use when
246
+ nothing has looked fully (with Spotlight) or the last look is ``REFRESH_SECONDS`` old."""
247
+
248
+ def __init__(self, detect: Callable[..., dict[str, str]] = detect, clock: Callable[[], float] = time.monotonic,
249
+ platform: str = sys.platform) -> None:
250
+ self._detect = detect
251
+ self._clock = clock
252
+ self._platform = platform
253
+ self._lock = threading.Lock()
254
+ self._found: dict[str, str] | None = None
255
+ self._at = 0.0
256
+ self._full = False
257
+ self._looking: threading.Thread | None = None
258
+
259
+ def warm(self) -> threading.Thread | None:
260
+ """Look now, in the background, without a command: the server's start."""
261
+ return self._start(spotlight=False)
262
+
263
+ def found(self) -> dict[str, str]:
264
+ """The apps last found (none before the first look ends); a look started if one is due."""
265
+ with self._lock:
266
+ found, due = self._found, (self._found is None or not self._full
267
+ or self._clock() - self._at >= REFRESH_SECONDS)
268
+ if due:
269
+ self._start(spotlight=True)
270
+ return dict(found or {})
271
+
272
+ def _start(self, *, spotlight: bool) -> threading.Thread | None:
273
+ with self._lock:
274
+ if self._looking is not None:
275
+ return None
276
+ thread = threading.Thread(target=self._look, args=(spotlight,), name="cad-open-in-apps", daemon=True)
277
+ self._looking = thread
278
+ thread.start()
279
+ return thread
280
+
281
+ def _look(self, spotlight: bool) -> None:
282
+ try:
283
+ found = self._detect(self._platform, spotlight=spotlight)
284
+ except Exception: # noqa: BLE001 - a look that fails leaves what was found; the next use looks again
285
+ found = None
286
+ with self._lock:
287
+ if found is not None:
288
+ self._found, self._at = found, self._clock()
289
+ # Full: a look that asked Spotlight, or one on a platform with nothing more to ask.
290
+ self._full = spotlight or self._platform != "darwin"
291
+ self._looking = None
292
+
293
+
294
+ DETECTED = Detected()
295
+
296
+
297
+ # --- opening -----------------------------------------------------------------------------------
298
+
299
+
300
+ def open_file(path: str, app: str | None = None, *, platform: str = sys.platform,
301
+ spawn: Callable[..., object] = subprocess.Popen, startfile: Callable[[str], None] | None = None) -> None:
302
+ """Start ``app`` (a path ``detect`` found) on the file at ``path``, or, with none, the operating
303
+ system's own handler for it. Detached and never waited on; never through a shell. Raises
304
+ ``FileNotFoundError`` when Linux has no ``xdg-open`` for the default."""
305
+ quiet = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "close_fds": True}
306
+ if platform == "darwin":
307
+ spawn(["/usr/bin/open", *(["-a", app] if app else []), path], start_new_session=True, **quiet)
308
+ elif platform == "win32":
309
+ if app is None:
310
+ (startfile or os.startfile)(path) # type: ignore[attr-defined]
311
+ return
312
+ spawn([app, path], creationflags=_DETACHED_PROCESS | _CREATE_NEW_PROCESS_GROUP, **quiet)
313
+ else:
314
+ if app is None:
315
+ app = executables.find("xdg-open")
316
+ if app is None:
317
+ raise FileNotFoundError("xdg-open is not installed")
318
+ spawn([app, path], start_new_session=True, **quiet)
319
+
@@ -0,0 +1,67 @@
1
+ """What the server attaches to the library's engine, as a module of it is imported and never sooner: the
2
+ engine imports nothing of the server's (its law 3), and a server process that uses no engine imports none
3
+ of it -- ``import cad_server.cli`` loads no ``cad.engine`` module (``engine_telemetry``'s law).
4
+
5
+ ``when(name, then)`` calls ``then`` with the module ``name`` once it has run: now, where it is imported
6
+ already, else as it is -- by a ``sys.meta_path`` finder for those modules alone (``_Attach``), which finds
7
+ them where Python finds them, by the path finder (asked itself, never the rest of ``sys.meta_path``, so no
8
+ two finders hand one lookup back and forth), and runs each one's own loader, then what is attached to it
9
+ (``_Then``) -- a reload's too. Every other import passes it by at one lookup. What the server attaches: the
10
+ engine telemetry's observer to the engine's verbs (``engine_telemetry.install``), and its words where no
11
+ installation is registered to the engine's (``cad_server``: ``cad.engine.said.UNREGISTERED``).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from typing import Any, Callable
18
+
19
+ # What is attached to a module as it runs, by its name (``when``).
20
+ _THEN: dict[str, list[Callable[[Any], None]]] = {}
21
+
22
+
23
+ def when(name: str, then: Callable[[Any], None]) -> None:
24
+ """``then`` called with the module ``name``: now, where it is imported already, else once it has run
25
+ (``_Attach``). Idempotent."""
26
+ module = sys.modules.get(name)
27
+ if module is not None:
28
+ then(module)
29
+ return
30
+ waiting = _THEN.setdefault(name, [])
31
+ if then not in waiting:
32
+ waiting.append(then)
33
+ if not any(isinstance(finder, _Attach) for finder in sys.meta_path):
34
+ sys.meta_path.insert(0, _Attach())
35
+
36
+
37
+ class _Attach:
38
+ """A ``sys.meta_path`` finder for the modules something is attached to (``_THEN``) alone."""
39
+
40
+ def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any:
41
+ if fullname not in _THEN:
42
+ return None
43
+ from importlib.machinery import PathFinder
44
+
45
+ spec = PathFinder.find_spec(fullname, path, target)
46
+ if spec is not None and spec.loader is not None and hasattr(spec.loader, "exec_module"):
47
+ spec.loader = _Then(spec.loader, fullname)
48
+ return spec
49
+
50
+
51
+ class _Then:
52
+ """A module's own loader, then what is attached to the module it ran."""
53
+
54
+ def __init__(self, loader: Any, name: str) -> None:
55
+ self._loader = loader
56
+ self._name = name
57
+
58
+ def __getattr__(self, name: str) -> Any:
59
+ return getattr(self._loader, name)
60
+
61
+ def create_module(self, spec: Any) -> Any:
62
+ return self._loader.create_module(spec)
63
+
64
+ def exec_module(self, module: Any) -> None:
65
+ self._loader.exec_module(module)
66
+ for then in list(_THEN.get(self._name, ())):
67
+ then(module)