dbx-tools-node-runtime 0.9.54__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.
@@ -0,0 +1,31 @@
1
+ from .bootstrap import (
2
+ NodeEnvironment,
3
+ ensure_pythonmonkey,
4
+ node_environment,
5
+ prepare_node_environment,
6
+ runtime_lock_directory,
7
+ )
8
+ from .runtime import (
9
+ MISSING,
10
+ RUNTIME_ABI_VERSION,
11
+ NodeObject,
12
+ RuntimeBundle,
13
+ get_builtin,
14
+ get_runtime,
15
+ load_bundle,
16
+ )
17
+
18
+ __all__ = [
19
+ "MISSING",
20
+ "RUNTIME_ABI_VERSION",
21
+ "NodeEnvironment",
22
+ "NodeObject",
23
+ "RuntimeBundle",
24
+ "ensure_pythonmonkey",
25
+ "get_builtin",
26
+ "get_runtime",
27
+ "load_bundle",
28
+ "node_environment",
29
+ "prepare_node_environment",
30
+ "runtime_lock_directory",
31
+ ]
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from .bootstrap import ensure_pythonmonkey
8
+
9
+ """Command-line entrypoint for preparing PythonMonkey and executing CommonJS files."""
10
+
11
+
12
+ def _run_javascript(runtime: Any, path: Path) -> Any:
13
+ resolved = path.expanduser().resolve()
14
+ if not resolved.is_file():
15
+ raise FileNotFoundError(f"JavaScript file does not exist: {resolved}")
16
+ return runtime.require(str(resolved))
17
+
18
+
19
+ def main() -> None:
20
+ """Install or verify PythonMonkey and optionally execute JavaScript."""
21
+
22
+ parser = argparse.ArgumentParser(
23
+ description="Install the locked PythonMonkey runtime and optionally run JavaScript."
24
+ )
25
+ parser.add_argument(
26
+ "javascript",
27
+ nargs="?",
28
+ type=Path,
29
+ help="CommonJS JavaScript file to execute with PythonMonkey require.",
30
+ )
31
+ parser.add_argument(
32
+ "--lock-directory",
33
+ type=Path,
34
+ help="Override the directory used for cross-process installation locks.",
35
+ )
36
+ arguments = parser.parse_args()
37
+ runtime = ensure_pythonmonkey(arguments.lock_directory)
38
+ if arguments.javascript is None:
39
+ print(Path(runtime.__file__).resolve())
40
+ return
41
+ result = _run_javascript(runtime, arguments.javascript)
42
+ if result is not None:
43
+ print(result)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
@@ -0,0 +1,362 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import importlib
5
+ import importlib.metadata
6
+ import importlib.util
7
+ import os
8
+ import shlex
9
+ import shutil
10
+ import stat
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ from collections.abc import Iterator, Mapping, Sequence
15
+ from contextlib import contextmanager
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from threading import RLock
19
+ from types import ModuleType
20
+
21
+ """Install and load the locked PythonMonkey runtime in managed Python environments."""
22
+
23
+ _LOCK_DIRECTORY_ENVIRONMENT = "DBX_TOOLS_NODE_RUNTIME_LOCK_DIRECTORY"
24
+ _NODEJS_WHEEL_REQUIREMENT = "nodejs-wheel>=22.20,<23"
25
+ _PYTHONMONKEY_VERSION = "1.3.2"
26
+ _PROCESS_LOCK = RLock()
27
+ _PROCESS_RUNTIME: ModuleType | None = None
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class NodeEnvironment:
32
+ """Resolved Node.js files and executable launcher directory."""
33
+
34
+ package_root: Path
35
+ launcher_directory: Path
36
+ node: Path
37
+ npm_cli: Path
38
+
39
+
40
+ def runtime_lock_directory() -> Path:
41
+ """Return the directory used only for cross-process installation locks."""
42
+
43
+ configured = os.environ.get(_LOCK_DIRECTORY_ENVIRONMENT)
44
+ if configured:
45
+ return Path(configured).expanduser().resolve()
46
+ if sys.platform == "win32":
47
+ base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
48
+ elif sys.platform == "darwin":
49
+ base = Path.home() / "Library" / "Caches"
50
+ else:
51
+ base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
52
+ return base / "dbx-tools" / "node-runtime" / "locks"
53
+
54
+
55
+ def _environment_key() -> str:
56
+ identity = f"{Path(sys.prefix).resolve()}\0{Path(sys.executable).resolve()}"
57
+ return hashlib.sha256(identity.encode()).hexdigest()[:16]
58
+
59
+
60
+ def _lock_path(name: str, directory: str | Path | None = None) -> Path:
61
+ root = (
62
+ Path(directory).expanduser().resolve()
63
+ if directory is not None
64
+ else runtime_lock_directory()
65
+ )
66
+ return root / f"{_environment_key()}-{name}.lock"
67
+
68
+
69
+ def _distribution_version(name: str) -> str | None:
70
+ try:
71
+ return importlib.metadata.version(name)
72
+ except importlib.metadata.PackageNotFoundError:
73
+ return None
74
+
75
+
76
+ def _environment_pythonmonkey() -> ModuleType | None:
77
+ loaded = sys.modules.get("pythonmonkey")
78
+ if loaded is not None:
79
+ version = getattr(loaded, "__version__", None)
80
+ if version != _PYTHONMONKEY_VERSION:
81
+ raise RuntimeError(
82
+ f"PythonMonkey {version or 'with an unknown version'} is already loaded; "
83
+ f"dbx-tools-node-runtime requires {_PYTHONMONKEY_VERSION}"
84
+ )
85
+ importlib.import_module("pythonmonkey.require")
86
+ return loaded
87
+ if _distribution_version("pythonmonkey") != _PYTHONMONKEY_VERSION:
88
+ return None
89
+ module = importlib.import_module("pythonmonkey")
90
+ importlib.import_module("pythonmonkey.require")
91
+ return module
92
+
93
+
94
+ @contextmanager
95
+ def _file_lock(path: Path) -> Iterator[None]:
96
+ path.parent.mkdir(parents=True, exist_ok=True)
97
+ handle = path.open("a+b")
98
+ try:
99
+ if os.name == "nt":
100
+ import msvcrt
101
+
102
+ if path.stat().st_size == 0:
103
+ handle.write(b"0")
104
+ handle.flush()
105
+ handle.seek(0)
106
+ msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
107
+ else:
108
+ import fcntl
109
+
110
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
111
+ yield
112
+ finally:
113
+ if os.name == "nt":
114
+ import msvcrt
115
+
116
+ handle.seek(0)
117
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
118
+ else:
119
+ import fcntl
120
+
121
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
122
+ handle.close()
123
+
124
+
125
+ def _nodejs_wheel_root() -> Path | None:
126
+ importlib.invalidate_caches()
127
+ spec = importlib.util.find_spec("nodejs_wheel")
128
+ if spec is None or spec.origin is None:
129
+ return None
130
+ return Path(spec.origin).resolve().parent
131
+
132
+
133
+ def _write_posix_launcher(path: Path, command: Sequence[Path | str]) -> None:
134
+ arguments = " ".join(shlex.quote(str(argument)) for argument in command)
135
+ path.write_text(f'#!/bin/sh\nexec {arguments} "$@"\n', encoding="utf-8")
136
+ path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
137
+
138
+
139
+ def _write_windows_launcher(path: Path, command: Sequence[Path | str]) -> None:
140
+ arguments = " ".join(f'"{argument}"' for argument in command)
141
+ path.write_text(f"@echo off\r\n{arguments} %*\r\n", encoding="utf-8")
142
+
143
+
144
+ def prepare_node_environment(
145
+ directory: str | Path | None = None,
146
+ *,
147
+ package_root: str | Path | None = None,
148
+ ) -> NodeEnvironment:
149
+ """Create direct Node and npm launchers backed by `nodejs-wheel` files."""
150
+
151
+ resolved_root = (
152
+ Path(package_root).resolve() if package_root is not None else _nodejs_wheel_root()
153
+ )
154
+ if resolved_root is None:
155
+ raise RuntimeError(
156
+ "nodejs-wheel is unavailable; call ensure_pythonmonkey() to install the locked runtime"
157
+ )
158
+ node = resolved_root / "bin" / ("node.exe" if os.name == "nt" else "node")
159
+ npm_cli = resolved_root / "lib" / "node_modules" / "npm" / "bin" / "npm-cli.js"
160
+ for required in (node, npm_cli):
161
+ if not required.is_file():
162
+ raise RuntimeError(f"nodejs-wheel is missing required file: {required}")
163
+
164
+ launcher_directory = (
165
+ Path(directory).resolve()
166
+ if directory is not None
167
+ else Path(tempfile.mkdtemp(prefix="dbx-tools-node-launchers-"))
168
+ )
169
+ launcher_directory.mkdir(parents=True, exist_ok=True)
170
+ if os.name == "nt":
171
+ _write_windows_launcher(launcher_directory / "node.cmd", [node])
172
+ _write_windows_launcher(launcher_directory / "npm.cmd", [node, npm_cli])
173
+ else:
174
+ _write_posix_launcher(launcher_directory / "node", [node])
175
+ _write_posix_launcher(launcher_directory / "npm", [node, npm_cli])
176
+ return NodeEnvironment(
177
+ package_root=resolved_root,
178
+ launcher_directory=launcher_directory,
179
+ node=node,
180
+ npm_cli=npm_cli,
181
+ )
182
+
183
+
184
+ @contextmanager
185
+ def node_environment(
186
+ directory: str | Path | None = None,
187
+ *,
188
+ package_root: str | Path | None = None,
189
+ ) -> Iterator[NodeEnvironment]:
190
+ """Temporarily prepend direct Node and npm launchers to `PATH`."""
191
+
192
+ temporary = directory is None
193
+ environment = prepare_node_environment(directory, package_root=package_root)
194
+ previous = os.environ.get("PATH")
195
+ os.environ["PATH"] = os.pathsep.join(
196
+ value for value in (str(environment.launcher_directory), previous) if value
197
+ )
198
+ try:
199
+ yield environment
200
+ finally:
201
+ if previous is None:
202
+ os.environ.pop("PATH", None)
203
+ else:
204
+ os.environ["PATH"] = previous
205
+ if temporary:
206
+ shutil.rmtree(environment.launcher_directory, ignore_errors=True)
207
+
208
+
209
+ def _pip_install(
210
+ requirement: str,
211
+ *,
212
+ environment: Mapping[str, str] | None = None,
213
+ only_binary: Sequence[str] = (),
214
+ ) -> None:
215
+ _ensure_pip()
216
+ command = [
217
+ sys.executable,
218
+ "-m",
219
+ "pip",
220
+ "install",
221
+ "--disable-pip-version-check",
222
+ "--no-input",
223
+ ]
224
+ if only_binary:
225
+ command.extend(["--only-binary", ",".join(only_binary)])
226
+ command.append(requirement)
227
+ result = subprocess.run(
228
+ command,
229
+ check=False,
230
+ env=dict(environment) if environment is not None else None,
231
+ stdout=subprocess.PIPE,
232
+ stderr=subprocess.STDOUT,
233
+ text=True,
234
+ )
235
+ if result.returncode != 0:
236
+ raise RuntimeError(
237
+ f"Could not install locked runtime requirement {requirement} "
238
+ f"into {sys.prefix}:\n{result.stdout}"
239
+ )
240
+ importlib.invalidate_caches()
241
+
242
+
243
+ def _ensure_pip() -> None:
244
+ """Install CPython's bundled pip when the active environment omits it."""
245
+ if importlib.util.find_spec("pip") is not None:
246
+ return
247
+ command = [sys.executable, "-m", "ensurepip", "--upgrade"]
248
+ result = subprocess.run(
249
+ command,
250
+ check=False,
251
+ stdout=subprocess.PIPE,
252
+ stderr=subprocess.STDOUT,
253
+ text=True,
254
+ )
255
+ importlib.invalidate_caches()
256
+ if result.returncode != 0 or importlib.util.find_spec("pip") is None:
257
+ raise RuntimeError(
258
+ f"Could not install pip into {sys.prefix} with ensurepip:\n{result.stdout}"
259
+ )
260
+
261
+
262
+ def _npm_available() -> bool:
263
+ return shutil.which("npm") is not None
264
+
265
+
266
+ def _ensure_nodejs_wheel(lock_directory: str | Path | None = None) -> Path | None:
267
+ if _npm_available():
268
+ return None
269
+ package_root = _nodejs_wheel_root()
270
+ if package_root is not None:
271
+ return package_root
272
+
273
+ with _file_lock(_lock_path("nodejs-wheel", lock_directory)):
274
+ if _npm_available():
275
+ return None
276
+ package_root = _nodejs_wheel_root()
277
+ if package_root is None:
278
+ _pip_install(
279
+ _NODEJS_WHEEL_REQUIREMENT,
280
+ only_binary=("nodejs-wheel", "nodejs-wheel-binaries"),
281
+ )
282
+ package_root = _nodejs_wheel_root()
283
+ if package_root is None:
284
+ raise RuntimeError(
285
+ "The locked nodejs-wheel installation did not provide its package files"
286
+ )
287
+ return package_root
288
+
289
+
290
+ @contextmanager
291
+ def _pythonmonkey_install_environment(
292
+ lock_directory: str | Path | None = None,
293
+ ) -> Iterator[Mapping[str, str] | None]:
294
+ package_root = _ensure_nodejs_wheel(lock_directory)
295
+ if package_root is None:
296
+ yield None
297
+ return
298
+
299
+ launcher_directory = Path(tempfile.mkdtemp(prefix="dbx-tools-node-launchers-"))
300
+ try:
301
+ launchers = prepare_node_environment(
302
+ launcher_directory,
303
+ package_root=package_root,
304
+ )
305
+ environment = dict(os.environ)
306
+ environment["PATH"] = os.pathsep.join(
307
+ value for value in (str(launchers.launcher_directory), environment.get("PATH")) if value
308
+ )
309
+ yield environment
310
+ finally:
311
+ shutil.rmtree(launcher_directory, ignore_errors=True)
312
+
313
+
314
+ def _verify_runtime() -> None:
315
+ result = subprocess.run(
316
+ [
317
+ sys.executable,
318
+ "-c",
319
+ (
320
+ "import pythonmonkey as pm; import pythonmonkey.require; "
321
+ "assert pm.eval('1 + 2') == 3; assert callable(pm.require)"
322
+ ),
323
+ ],
324
+ check=False,
325
+ stdout=subprocess.PIPE,
326
+ stderr=subprocess.STDOUT,
327
+ text=True,
328
+ )
329
+ if result.returncode != 0:
330
+ raise RuntimeError(f"Installed PythonMonkey runtime failed verification:\n{result.stdout}")
331
+
332
+
333
+ def ensure_pythonmonkey(lock_directory: str | Path | None = None) -> ModuleType:
334
+ """Return PythonMonkey, installing its locked version into the active environment."""
335
+
336
+ global _PROCESS_RUNTIME
337
+ with _PROCESS_LOCK:
338
+ if _PROCESS_RUNTIME is not None:
339
+ return _PROCESS_RUNTIME
340
+ environment_runtime = _environment_pythonmonkey()
341
+ if environment_runtime is not None:
342
+ _PROCESS_RUNTIME = environment_runtime
343
+ return environment_runtime
344
+
345
+ with _file_lock(_lock_path("pythonmonkey", lock_directory)):
346
+ environment_runtime = _environment_pythonmonkey()
347
+ if environment_runtime is None:
348
+ with _pythonmonkey_install_environment(lock_directory) as environment:
349
+ _pip_install(
350
+ f"pythonmonkey=={_PYTHONMONKEY_VERSION}",
351
+ environment=environment,
352
+ only_binary=("pythonmonkey",),
353
+ )
354
+ _verify_runtime()
355
+ environment_runtime = _environment_pythonmonkey()
356
+ if environment_runtime is None:
357
+ raise RuntimeError(
358
+ "The locked PythonMonkey installation is unavailable in the active "
359
+ f"Python environment {sys.prefix}"
360
+ )
361
+ _PROCESS_RUNTIME = environment_runtime
362
+ return environment_runtime
@@ -0,0 +1 @@
1
+