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.
- cad_server/__init__.py +54 -0
- cad_server/_internal/__init__.py +1 -0
- cad_server/_internal/api.py +19 -0
- cad_server/_internal/apps.py +319 -0
- cad_server/_internal/attach.py +67 -0
- cad_server/_internal/base.py +682 -0
- cad_server/_internal/channel.py +165 -0
- cad_server/_internal/cli_errors.py +91 -0
- cad_server/_internal/cli_from_function.py +489 -0
- cad_server/_internal/editable.py +43 -0
- cad_server/_internal/engine_daemon.py +6 -0
- cad_server/_internal/engine_host.py +82 -0
- cad_server/_internal/engine_telemetry.py +206 -0
- cad_server/_internal/extras.py +577 -0
- cad_server/_internal/file_lock.py +70 -0
- cad_server/_internal/installations.py +637 -0
- cad_server/_internal/kernel_load_hint.py +125 -0
- cad_server/_internal/launch.py +48 -0
- cad_server/_internal/leftovers.py +391 -0
- cad_server/_internal/picker.py +127 -0
- cad_server/_internal/plugin.py +388 -0
- cad_server/_internal/primitive_mesh.py +245 -0
- cad_server/_internal/providers.py +2013 -0
- cad_server/_internal/proxy.py +94 -0
- cad_server/_internal/releases.py +211 -0
- cad_server/_internal/shared_read.py +62 -0
- cad_server/_internal/switch.py +304 -0
- cad_server/_internal/telemetry_send.py +108 -0
- cad_server/_internal/update.py +157 -0
- cad_server/_runtime/mcp/THIRD_PARTY_LICENSES.txt +1895 -0
- cad_server/_runtime/mcp/app.js +9 -0
- cad_server/_runtime/mcp/shell.html +13 -0
- cad_server/_runtime/plugin/.claude-plugin/icon.png +0 -0
- cad_server/_runtime/plugin/.claude-plugin/marketplace.json +35 -0
- cad_server/_runtime/plugin/.claude-plugin/plugin.json +28 -0
- cad_server/_runtime/plugin/.codex-plugin/logo.png +0 -0
- cad_server/_runtime/plugin/.codex-plugin/plugin.json +47 -0
- cad_server/_runtime/plugin/.cursor-plugin/plugin.json +29 -0
- cad_server/_runtime/plugin/LICENSE +21 -0
- cad_server/_runtime/plugin/claude.mcp.json +19 -0
- cad_server/_runtime/plugin/codex.mcp.json +20 -0
- cad_server/_runtime/plugin/cursor.mcp.json +20 -0
- cad_server/_runtime/plugin/gemini-extension.json +22 -0
- cad_server/_runtime/plugin/mcp.json +21 -0
- cad_server/_runtime/plugin/plugin.json +23 -0
- cad_server/_runtime/plugin/skills/bambu-labs/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/bambu-labs/SKILL.md +67 -0
- cad_server/_runtime/plugin/skills/bambu-labs/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/cad/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/cad/SKILL.md +361 -0
- cad_server/_runtime/plugin/skills/cad/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/cad/references/animation.md +139 -0
- cad_server/_runtime/plugin/skills/cad/references/build123d-modeling.md +159 -0
- cad_server/_runtime/plugin/skills/cad/references/exports.md +180 -0
- cad_server/_runtime/plugin/skills/cad/references/inspection-and-validation.md +340 -0
- cad_server/_runtime/plugin/skills/cad/references/kinematics.md +316 -0
- cad_server/_runtime/plugin/skills/cad/references/model-contract.md +380 -0
- cad_server/_runtime/plugin/skills/cad/references/positioning.md +118 -0
- cad_server/_runtime/plugin/skills/cad/references/project-layout.md +174 -0
- cad_server/_runtime/plugin/skills/cad/references/snapshot-review.md +248 -0
- cad_server/_runtime/plugin/skills/dfam-check/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/dfam-check/SKILL.md +106 -0
- cad_server/_runtime/plugin/skills/dfam-check/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/dfam-check/references/process-limits.md +58 -0
- cad_server/_runtime/plugin/skills/dfam-check/requirements.txt +11 -0
- cad_server/_runtime/plugin/skills/dfam-check/scripts/dfam_tool.py +399 -0
- cad_server/_runtime/plugin/skills/dfm/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/dfm/SKILL.md +105 -0
- cad_server/_runtime/plugin/skills/dfm/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/dfm/references/cnc.md +52 -0
- cad_server/_runtime/plugin/skills/dfm/references/injection-molding.md +173 -0
- cad_server/_runtime/plugin/skills/dfm/references/sheet-metal.md +47 -0
- cad_server/_runtime/plugin/skills/dfm/requirements.txt +10 -0
- cad_server/_runtime/plugin/skills/dfm/scripts/mold_tool.py +1039 -0
- cad_server/_runtime/plugin/skills/dxf/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/dxf/SKILL.md +358 -0
- cad_server/_runtime/plugin/skills/dxf/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/dxf/references/generator-templates.md +187 -0
- cad_server/_runtime/plugin/skills/engineering-drawing/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/engineering-drawing/SKILL.md +137 -0
- cad_server/_runtime/plugin/skills/engineering-drawing/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/engineering-drawing/references/sheet-api.md +142 -0
- cad_server/_runtime/plugin/skills/gcode/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/gcode/SKILL.md +91 -0
- cad_server/_runtime/plugin/skills/gcode/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/gcode/scripts/orca_presets.py +134 -0
- cad_server/_runtime/plugin/skills/sdf/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/sdf/SKILL.md +193 -0
- cad_server/_runtime/plugin/skills/sdf/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/sdf/references/design-ledger.md +113 -0
- cad_server/_runtime/plugin/skills/sdf/references/examples.md +141 -0
- cad_server/_runtime/plugin/skills/sdf/references/frame-semantics.md +87 -0
- cad_server/_runtime/plugin/skills/sdf/references/interoperability.md +78 -0
- cad_server/_runtime/plugin/skills/sdf/references/llm-guardrails.md +107 -0
- cad_server/_runtime/plugin/skills/sdf/references/sdf-workflow.md +80 -0
- cad_server/_runtime/plugin/skills/sdf/references/smoke-tests.md +99 -0
- cad_server/_runtime/plugin/skills/sdf/references/validation.md +161 -0
- cad_server/_runtime/plugin/skills/sendcutsend/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/sendcutsend/SKILL.md +117 -0
- cad_server/_runtime/plugin/skills/sendcutsend/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/sendcutsend/references/official-sources.md +40 -0
- cad_server/_runtime/plugin/skills/sendcutsend/references/report-template.md +40 -0
- cad_server/_runtime/plugin/skills/srdf/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/srdf/SKILL.md +168 -0
- cad_server/_runtime/plugin/skills/srdf/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/srdf/references/authoring-contract.md +70 -0
- cad_server/_runtime/plugin/skills/srdf/references/disabled-collisions.md +55 -0
- cad_server/_runtime/plugin/skills/srdf/references/end-effectors.md +50 -0
- cad_server/_runtime/plugin/skills/srdf/references/planning-ledger.md +102 -0
- cad_server/_runtime/plugin/skills/srdf/references/srdf-workflow.md +38 -0
- cad_server/_runtime/plugin/skills/srdf/references/validation.md +44 -0
- cad_server/_runtime/plugin/skills/step-parts/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/step-parts/SKILL.md +68 -0
- cad_server/_runtime/plugin/skills/step-parts/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/step-parts/references/step-parts-api.md +75 -0
- cad_server/_runtime/plugin/skills/step-parts/scripts/download_step_part.py +230 -0
- cad_server/_runtime/plugin/skills/urdf/LICENSE +21 -0
- cad_server/_runtime/plugin/skills/urdf/SKILL.md +156 -0
- cad_server/_runtime/plugin/skills/urdf/agents/openai.yaml +4 -0
- cad_server/_runtime/plugin/skills/urdf/references/authoring-contract.md +167 -0
- cad_server/_runtime/plugin/skills/urdf/references/design-ledger.md +112 -0
- cad_server/_runtime/plugin/skills/urdf/references/frame-semantics.md +105 -0
- cad_server/_runtime/plugin/skills/urdf/references/inertials.md +63 -0
- cad_server/_runtime/plugin/skills/urdf/references/meshes.md +49 -0
- cad_server/_runtime/plugin/skills/urdf/references/urdf-workflow.md +79 -0
- cad_server/_runtime/plugin/skills/urdf/references/validation.md +67 -0
- cad_server/_runtime/viewer/THIRD_PARTY_LICENSES.txt +1895 -0
- cad_server/_runtime/viewer/assets/BufferGeometryUtils-BO3Ss---.js +1 -0
- cad_server/_runtime/viewer/assets/DxfRenderer-CK-C88pp.js +6 -0
- cad_server/_runtime/viewer/assets/GLTFLoader-C-ZwhOwN.js +1 -0
- cad_server/_runtime/viewer/assets/GlbRenderer-CwtvgnsN.js +2 -0
- cad_server/_runtime/viewer/assets/MeshRenderer-CtKo5N8-.js +2 -0
- cad_server/_runtime/viewer/assets/RobotRenderer-D4v9vkun.js +2 -0
- cad_server/_runtime/viewer/assets/STLLoader-COjUjMBk.js +1 -0
- cad_server/_runtime/viewer/assets/SceneMeasure-DWhVhT3U.js +2 -0
- cad_server/_runtime/viewer/assets/VirtualRows-CcpJpJrq.js +2 -0
- cad_server/_runtime/viewer/assets/XbfRenderer-Cz77kpw3.js +8 -0
- cad_server/_runtime/viewer/assets/entryAssets-DMPboejr.js +2 -0
- cad_server/_runtime/viewer/assets/environmentMap-C2AYeUDk.js +2 -0
- cad_server/_runtime/viewer/assets/favicon-DwiaIWoe.png +0 -0
- cad_server/_runtime/viewer/assets/glbMeshWorker-DH0X5ko0.js +7 -0
- cad_server/_runtime/viewer/assets/index-BBVuT0Je.css +2 -0
- cad_server/_runtime/viewer/assets/index-KO9e-SAu.js +27 -0
- cad_server/_runtime/viewer/assets/loading-OUGzZRP4.avif +0 -0
- cad_server/_runtime/viewer/assets/loading-still-DWWO9OwM.webp +0 -0
- cad_server/_runtime/viewer/assets/meshObjects-BHfBp4CD.js +2 -0
- cad_server/_runtime/viewer/assets/meshopt_decoder-4Ef-c7zw.js +1 -0
- cad_server/_runtime/viewer/assets/photographicStudio-D1RYrfjy.js +475 -0
- cad_server/_runtime/viewer/assets/raycastBvhWorker-D5dKYzM6.js +6 -0
- cad_server/_runtime/viewer/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- cad_server/_runtime/viewer/assets/sceneExplode-DP8HU56N.js +2 -0
- cad_server/_runtime/viewer/assets/sceneHighlight-C5lLZODo.js +2 -0
- cad_server/_runtime/viewer/assets/sharedGeometry-BsLw_hx-.js +225 -0
- cad_server/_runtime/viewer/assets/stlMeshWorker-CuNjKOUV.js +1 -0
- cad_server/_runtime/viewer/assets/studioEnvironment.worker-utbnN6Yk.js +4231 -0
- cad_server/_runtime/viewer/assets/three.core-DdnNoxhu.js +6 -0
- cad_server/_runtime/viewer/assets/toolModes-Cc3imgJQ.js +66 -0
- cad_server/_runtime/viewer/assets/useSceneSelection-CqIqnfow.js +2 -0
- cad_server/_runtime/viewer/assets/vendor-icons-BmP3iO1o.js +2 -0
- cad_server/_runtime/viewer/assets/vendor-react-iRJfyluG.js +10 -0
- cad_server/_runtime/viewer/assets/vendor-three-DStQQdKw.js +4613 -0
- cad_server/_runtime/viewer/assets/vendor-ui-Cgj57w_6.js +47 -0
- cad_server/_runtime/viewer/index.html +39 -0
- cad_server/analytics.py +1865 -0
- cad_server/assets.py +157 -0
- cad_server/cli/__init__.py +277 -0
- cad_server/cli/__main__.py +10 -0
- cad_server/cli/delete.py +30 -0
- cad_server/cli/doctor.py +570 -0
- cad_server/cli/export.py +30 -0
- cad_server/cli/inspect.py +30 -0
- cad_server/cli/plugin.py +79 -0
- cad_server/cli/run.py +105 -0
- cad_server/cli/sdf_validate.py +30 -0
- cad_server/cli/serve.py +30 -0
- cad_server/cli/server.py +29 -0
- cad_server/cli/setup.py +1054 -0
- cad_server/cli/snapshot.py +30 -0
- cad_server/cli/srdf_validate.py +30 -0
- cad_server/cli/status.py +30 -0
- cad_server/cli/stop.py +29 -0
- cad_server/cli/telemetry.py +90 -0
- cad_server/cli/uninstall.py +29 -0
- cad_server/cli/update.py +65 -0
- cad_server/cli/urdf_validate.py +83 -0
- cad_server/cli/viewer.py +22 -0
- cad_server/drawing_payload.py +683 -0
- cad_server/engine/README.md +3131 -0
- cad_server/engine/__init__.py +3 -0
- cad_server/engine/__main__.py +7 -0
- cad_server/engine/cache.py +391 -0
- cad_server/engine/cli.py +823 -0
- cad_server/engine/daemon.py +1818 -0
- cad_server/engine/models.py +799 -0
- cad_server/engine/registry.py +326 -0
- cad_server/engine/renders.py +155 -0
- cad_server/engine/services.py +79 -0
- cad_server/engine/supervisor.py +1126 -0
- cad_server/jobs.py +153 -0
- cad_server/language.py +47 -0
- cad_server/mcp/__init__.py +0 -0
- cad_server/mcp/browser.py +43 -0
- cad_server/mcp/host.py +60 -0
- cad_server/mcp/http.py +230 -0
- cad_server/mcp/operations.py +374 -0
- cad_server/mcp/protocol.py +39 -0
- cad_server/mcp/server.py +1157 -0
- cad_server/mcp/sidebar.py +52 -0
- cad_server/mcp/tunnel.py +248 -0
- cad_server/mcp/ui.py +120 -0
- cad_server/mcp/views.py +202 -0
- cad_server/py.typed +0 -0
- cad_server/robot_payload.py +1768 -0
- cad_server/settings.py +83 -0
- cad_server/updates.py +252 -0
- cad_server/viewer/__init__.py +11 -0
- cad_server/viewer/__main__.py +7 -0
- cad_server/viewer/backend.py +50 -0
- cad_server/viewer/browser_open.py +107 -0
- cad_server/viewer/clipboard.py +28 -0
- cad_server/viewer/collation.json +1 -0
- cad_server/viewer/content_types.py +66 -0
- cad_server/viewer/documents.py +143 -0
- cad_server/viewer/drawings.py +157 -0
- cad_server/viewer/encoding.py +173 -0
- cad_server/viewer/engine_app.py +470 -0
- cad_server/viewer/exports.py +297 -0
- cad_server/viewer/folders.py +135 -0
- cad_server/viewer/handler.py +365 -0
- cad_server/viewer/http_app.py +1342 -0
- cad_server/viewer/kinds.py +45 -0
- cad_server/viewer/main.py +78 -0
- cad_server/viewer/model_catalog.py +37 -0
- cad_server/viewer/natural_sort.py +218 -0
- cad_server/viewer/prompt_files.py +234 -0
- cad_server/viewer/recents.py +254 -0
- cad_server/viewer/response.py +270 -0
- cad_server/viewer/reveal.py +49 -0
- cad_server/viewer/robots.py +98 -0
- cad_server/viewer/scanner.py +320 -0
- cad_server/viewer/url_norm.py +160 -0
- cad_server-0.10.0.dist-info/METADATA +156 -0
- cad_server-0.10.0.dist-info/RECORD +247 -0
- cad_server-0.10.0.dist-info/WHEEL +5 -0
- cad_server-0.10.0.dist-info/entry_points.txt +2 -0
- cad_server-0.10.0.dist-info/licenses/LICENSE +21 -0
- 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)
|