nodepyx 1.0.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 (184) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +399 -0
  3. package/binding.gyp +73 -0
  4. package/dist/core/PyCallable.d.ts +65 -0
  5. package/dist/core/PyCallable.d.ts.map +1 -0
  6. package/dist/core/PyCallable.js +109 -0
  7. package/dist/core/PyCallable.js.map +1 -0
  8. package/dist/core/PyContext.d.ts +76 -0
  9. package/dist/core/PyContext.d.ts.map +1 -0
  10. package/dist/core/PyContext.js +228 -0
  11. package/dist/core/PyContext.js.map +1 -0
  12. package/dist/core/PyIterator.d.ts +84 -0
  13. package/dist/core/PyIterator.d.ts.map +1 -0
  14. package/dist/core/PyIterator.js +243 -0
  15. package/dist/core/PyIterator.js.map +1 -0
  16. package/dist/core/PyModule.d.ts +55 -0
  17. package/dist/core/PyModule.d.ts.map +1 -0
  18. package/dist/core/PyModule.js +172 -0
  19. package/dist/core/PyModule.js.map +1 -0
  20. package/dist/core/PyProxy.d.ts +65 -0
  21. package/dist/core/PyProxy.d.ts.map +1 -0
  22. package/dist/core/PyProxy.js +483 -0
  23. package/dist/core/PyProxy.js.map +1 -0
  24. package/dist/core/PyRuntime.d.ts +105 -0
  25. package/dist/core/PyRuntime.d.ts.map +1 -0
  26. package/dist/core/PyRuntime.js +438 -0
  27. package/dist/core/PyRuntime.js.map +1 -0
  28. package/dist/env/CondaManager.d.ts +118 -0
  29. package/dist/env/CondaManager.d.ts.map +1 -0
  30. package/dist/env/CondaManager.js +401 -0
  31. package/dist/env/CondaManager.js.map +1 -0
  32. package/dist/env/PackageInstaller.d.ts +233 -0
  33. package/dist/env/PackageInstaller.d.ts.map +1 -0
  34. package/dist/env/PackageInstaller.js +609 -0
  35. package/dist/env/PackageInstaller.js.map +1 -0
  36. package/dist/env/PythonDetector.d.ts +103 -0
  37. package/dist/env/PythonDetector.d.ts.map +1 -0
  38. package/dist/env/PythonDetector.js +381 -0
  39. package/dist/env/PythonDetector.js.map +1 -0
  40. package/dist/env/VenvManager.d.ts +117 -0
  41. package/dist/env/VenvManager.d.ts.map +1 -0
  42. package/dist/env/VenvManager.js +331 -0
  43. package/dist/env/VenvManager.js.map +1 -0
  44. package/dist/index.d.ts +169 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +393 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/plugins/Plugin.interface.d.ts +41 -0
  49. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  50. package/dist/plugins/Plugin.interface.js +12 -0
  51. package/dist/plugins/Plugin.interface.js.map +1 -0
  52. package/dist/plugins/PluginManager.d.ts +26 -0
  53. package/dist/plugins/PluginManager.d.ts.map +1 -0
  54. package/dist/plugins/PluginManager.js +174 -0
  55. package/dist/plugins/PluginManager.js.map +1 -0
  56. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  57. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  58. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  59. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  60. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  61. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  62. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  63. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  64. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  65. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  66. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  67. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  68. package/dist/plugins/index.d.ts +7 -0
  69. package/dist/plugins/index.d.ts.map +1 -0
  70. package/dist/plugins/index.js +12 -0
  71. package/dist/plugins/index.js.map +1 -0
  72. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  73. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  74. package/dist/serialization/DataFrameBridge.js +355 -0
  75. package/dist/serialization/DataFrameBridge.js.map +1 -0
  76. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  77. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  78. package/dist/serialization/MsgPackSerializer.js +242 -0
  79. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  80. package/dist/serialization/NumpyBridge.d.ts +96 -0
  81. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  82. package/dist/serialization/NumpyBridge.js +323 -0
  83. package/dist/serialization/NumpyBridge.js.map +1 -0
  84. package/dist/serialization/Serializer.d.ts +78 -0
  85. package/dist/serialization/Serializer.d.ts.map +1 -0
  86. package/dist/serialization/Serializer.js +281 -0
  87. package/dist/serialization/Serializer.js.map +1 -0
  88. package/dist/types/PythonTypeMapper.d.ts +87 -0
  89. package/dist/types/PythonTypeMapper.d.ts.map +1 -0
  90. package/dist/types/PythonTypeMapper.js +449 -0
  91. package/dist/types/PythonTypeMapper.js.map +1 -0
  92. package/dist/types/StubCache.d.ts +109 -0
  93. package/dist/types/StubCache.d.ts.map +1 -0
  94. package/dist/types/StubCache.js +333 -0
  95. package/dist/types/StubCache.js.map +1 -0
  96. package/dist/types/TypeGenerator.d.ts +139 -0
  97. package/dist/types/TypeGenerator.d.ts.map +1 -0
  98. package/dist/types/TypeGenerator.js +372 -0
  99. package/dist/types/TypeGenerator.js.map +1 -0
  100. package/dist/types/addon.d.ts +114 -0
  101. package/dist/types/addon.d.ts.map +1 -0
  102. package/dist/types/addon.js +32 -0
  103. package/dist/types/addon.js.map +1 -0
  104. package/dist/types/config.d.ts +175 -0
  105. package/dist/types/config.d.ts.map +1 -0
  106. package/dist/types/config.js +35 -0
  107. package/dist/types/config.js.map +1 -0
  108. package/dist/types/index.d.ts +10 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/index.js +12 -0
  111. package/dist/types/index.js.map +1 -0
  112. package/dist/types/python.d.ts +235 -0
  113. package/dist/types/python.d.ts.map +1 -0
  114. package/dist/types/python.js +7 -0
  115. package/dist/types/python.js.map +1 -0
  116. package/dist/utils/ErrorTranslator.d.ts +83 -0
  117. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  118. package/dist/utils/ErrorTranslator.js +210 -0
  119. package/dist/utils/ErrorTranslator.js.map +1 -0
  120. package/dist/utils/Logger.d.ts +27 -0
  121. package/dist/utils/Logger.d.ts.map +1 -0
  122. package/dist/utils/Logger.js +115 -0
  123. package/dist/utils/Logger.js.map +1 -0
  124. package/dist/utils/MemoryMonitor.d.ts +44 -0
  125. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  126. package/dist/utils/MemoryMonitor.js +143 -0
  127. package/dist/utils/MemoryMonitor.js.map +1 -0
  128. package/package.json +177 -0
  129. package/python/error_handler.py +433 -0
  130. package/python/nodepyx_runtime.py +575 -0
  131. package/python/serializer.py +379 -0
  132. package/python/type_inspector.py +288 -0
  133. package/scripts/build-native.js +68 -0
  134. package/scripts/download-prebuilds.js +99 -0
  135. package/scripts/generate-stubs.js +405 -0
  136. package/scripts/install.js +260 -0
  137. package/src/core/PyCallable.ts +137 -0
  138. package/src/core/PyContext.ts +296 -0
  139. package/src/core/PyIterator.ts +294 -0
  140. package/src/core/PyModule.ts +194 -0
  141. package/src/core/PyProxy.ts +605 -0
  142. package/src/core/PyRuntime.ts +504 -0
  143. package/src/env/CondaManager.ts +451 -0
  144. package/src/env/PackageInstaller.ts +738 -0
  145. package/src/env/PythonDetector.ts +414 -0
  146. package/src/env/VenvManager.ts +396 -0
  147. package/src/index.ts +425 -0
  148. package/src/native/gil_guard.cpp +26 -0
  149. package/src/native/gil_guard.h +175 -0
  150. package/src/native/nodepyx_addon.cpp +886 -0
  151. package/src/native/python_bridge.cpp +790 -0
  152. package/src/native/python_bridge.h +257 -0
  153. package/src/native/thread_pool.cpp +336 -0
  154. package/src/native/thread_pool.h +175 -0
  155. package/src/native/type_converter.cpp +901 -0
  156. package/src/native/type_converter.h +272 -0
  157. package/src/nextjs/PyProvider.tsx +123 -0
  158. package/src/nextjs/index.ts +21 -0
  159. package/src/nextjs/usePython.ts +106 -0
  160. package/src/nextjs/withnodepyx.ts +88 -0
  161. package/src/plugins/Plugin.interface.ts +51 -0
  162. package/src/plugins/PluginManager.ts +155 -0
  163. package/src/plugins/builtin/NumpyPlugin.ts +36 -0
  164. package/src/plugins/builtin/PandasPlugin.ts +49 -0
  165. package/src/plugins/builtin/TorchPlugin.ts +56 -0
  166. package/src/plugins/index.ts +7 -0
  167. package/src/serialization/DataFrameBridge.ts +398 -0
  168. package/src/serialization/MsgPackSerializer.ts +220 -0
  169. package/src/serialization/NumpyBridge.ts +332 -0
  170. package/src/serialization/Serializer.ts +320 -0
  171. package/src/types/PythonTypeMapper.ts +495 -0
  172. package/src/types/StubCache.ts +340 -0
  173. package/src/types/TypeGenerator.ts +491 -0
  174. package/src/types/addon.ts +170 -0
  175. package/src/types/config.ts +226 -0
  176. package/src/types/index.ts +55 -0
  177. package/src/types/python.ts +309 -0
  178. package/src/types/stubs/numpy.d.ts +441 -0
  179. package/src/types/stubs/pandas.d.ts +575 -0
  180. package/src/types/stubs/sklearn.d.ts +728 -0
  181. package/src/types/stubs/torch.d.ts +694 -0
  182. package/src/utils/ErrorTranslator.ts +220 -0
  183. package/src/utils/Logger.ts +119 -0
  184. package/src/utils/MemoryMonitor.ts +175 -0
@@ -0,0 +1,504 @@
1
+ /**
2
+ * nodepyx — PyRuntime
3
+ * The central manager for the Python interpreter lifecycle.
4
+ * Creates, configures, and shuts down the embedded CPython instance.
5
+ */
6
+
7
+ import path from 'path';
8
+ import { loadNativeAddon } from '../types/addon';
9
+ import type { NativeAddon } from '../types/addon';
10
+ import { resolveConfig } from '../types/config';
11
+ import type { nodepyxConfig, ResolvednodepyxConfig } from '../types/config';
12
+ import type { PyObjectId } from '../types/python';
13
+ import { PyProxy } from './PyProxy';
14
+ import { PyModule } from './PyModule';
15
+ import { PyContext } from './PyContext';
16
+ import { Serializer } from '../serialization/Serializer';
17
+ import { TypeGenerator } from '../types/TypeGenerator';
18
+ import { PluginManager } from '../plugins/PluginManager';
19
+ import { VenvManager } from '../env/VenvManager';
20
+ import type { VenvManagerOptions } from '../env/VenvManager';
21
+ import type { CondaManagerOptions } from '../env/CondaManager';
22
+ import { PythonDetector } from '../env/PythonDetector';
23
+ import { PackageInstaller } from '../env/PackageInstaller';
24
+ import { MemoryMonitor } from '../utils/MemoryMonitor';
25
+ import { Logger } from '../utils/Logger';
26
+ import {
27
+ translatePythonError,
28
+ nodepyxShutdownError,
29
+ } from '../utils/ErrorTranslator';
30
+
31
+ const logger = new Logger('PyRuntime');
32
+
33
+ /**
34
+ * PyRuntime — manages the lifecycle of the embedded Python interpreter.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const runtime = await PyRuntime.create({
39
+ * virtualenv: { path: '.venv', packages: ['pandas'], autoInstall: true },
40
+ * });
41
+ *
42
+ * const pd = await runtime.import('pandas');
43
+ * const df = await pd.read_csv('data.csv');
44
+ * const result = await df.to_dict('records');
45
+ *
46
+ * await runtime.shutdown();
47
+ * ```
48
+ */
49
+ export class PyRuntime {
50
+ private readonly _config: ResolvednodepyxConfig;
51
+ private readonly _addon: NativeAddon;
52
+ private readonly _serializer: Serializer;
53
+ private readonly _typeGenerator: TypeGenerator;
54
+ private readonly _pluginManager: PluginManager;
55
+ private readonly _memoryMonitor: MemoryMonitor;
56
+ private readonly _moduleCache = new Map<string, PyModule>();
57
+ private _running = false;
58
+
59
+ private constructor(config: ResolvednodepyxConfig, addon: NativeAddon) {
60
+ this._config = config;
61
+ this._addon = addon;
62
+ this._serializer = Serializer.getInstance();
63
+ this._typeGenerator = new TypeGenerator({ stubsDir: config.stubsDir });
64
+ this._pluginManager = new PluginManager();
65
+ this._memoryMonitor = new MemoryMonitor({
66
+ gcThreshold: this._parseBytes(config.gcThreshold),
67
+ });
68
+ this._memoryMonitor.attach(addon);
69
+ }
70
+
71
+ /**
72
+ * Create and initialize a PyRuntime instance.
73
+ */
74
+ static async create(config: nodepyxConfig = {}): Promise<PyRuntime> {
75
+ const resolved = resolveConfig(config);
76
+
77
+ // Set log level
78
+ Logger.setGlobalLevel(resolved.logLevel);
79
+
80
+ logger.info('Creating PyRuntime...');
81
+
82
+ // Load the native addon
83
+ let addon: NativeAddon;
84
+ try {
85
+ addon = loadNativeAddon();
86
+ logger.debug('Native addon loaded');
87
+ } catch (err) {
88
+ logger.error('Failed to load native addon. Make sure nodepyx is built.', err);
89
+ throw new Error(
90
+ 'nodepyx native addon not found. Run "npm install" or "npm run build:native".\n' +
91
+ `Original error: ${(err as Error).message}`
92
+ );
93
+ }
94
+
95
+ const runtime = new PyRuntime(resolved, addon);
96
+ await runtime._initialize(config);
97
+ return runtime;
98
+ }
99
+
100
+ // ─── Initialization ────────────────────────────────────────────────────────
101
+
102
+ private async _initialize(config: nodepyxConfig): Promise<void> {
103
+ const startTime = performance.now();
104
+
105
+ // 1. Detect/setup Python environment
106
+ const pythonExecutable = await this._setupPythonEnvironment(config);
107
+
108
+ // 2. Initialize Python interpreter via C++ addon
109
+ logger.debug(`Initializing Python interpreter: ${pythonExecutable}`);
110
+
111
+ await this._addon.initializePython({
112
+ pythonHome: this._config.pythonHome || '',
113
+ pythonExecutable,
114
+ threadPoolSize: this._config.threadPoolSize,
115
+ maxQueueSize: this._config.maxQueueSize,
116
+ gilReleaseInterval: this._config.gil.releaseInterval,
117
+ callTimeout: this._config.callTimeout,
118
+ pythonPathExtra: [
119
+ path.join(__dirname, '..', '..', 'python'),
120
+ ...this._config.pythonPathExtra,
121
+ ],
122
+ env: this._config.env,
123
+ enableProfiling: this._config.profilingEnabled,
124
+ });
125
+
126
+ logger.debug('Python interpreter initialized');
127
+
128
+ // 3. Bootstrap Python runtime helpers
129
+ await this._bootstrapPythonRuntime();
130
+
131
+ // 4. Register built-in plugins
132
+ await this._pluginManager.registerBuiltins();
133
+
134
+ // 5. Start memory monitoring
135
+ this._memoryMonitor.startMonitoring(10000);
136
+
137
+ this._running = true;
138
+
139
+ const elapsed = (performance.now() - startTime).toFixed(0);
140
+ logger.success(`PyRuntime ready in ${elapsed}ms (Python: ${pythonExecutable})`);
141
+ }
142
+
143
+ private async _setupPythonEnvironment(config: nodepyxConfig): Promise<string> {
144
+ let pythonExecutable = this._config.pythonExecutable;
145
+
146
+ if (config.conda) {
147
+ // Conda environment
148
+ const { CondaManager } = await import('../env/CondaManager');
149
+ const condaManager = new CondaManager({ conda: config.conda } as CondaManagerOptions);
150
+ pythonExecutable = await condaManager.getPythonExecutable(config.conda.envName) ?? pythonExecutable;
151
+ logger.info(`Using conda environment: ${config.conda.envName}`);
152
+ } else if (config.virtualenv) {
153
+ // Virtualenv
154
+ const venvManager = new VenvManager({ virtualenv: config.virtualenv } as VenvManagerOptions);
155
+ const venvResult = await venvManager.setup();
156
+ pythonExecutable = typeof venvResult === 'string' ? venvResult : (venvResult as { pythonExecutable: string }).pythonExecutable ?? pythonExecutable;
157
+ logger.info(`Using virtualenv: ${config.virtualenv.path}`);
158
+ } else if (config.pythonPath) {
159
+ // Explicit Python path
160
+ pythonExecutable = config.pythonPath;
161
+ } else {
162
+ // Auto-detect
163
+ const detector = new PythonDetector();
164
+ try {
165
+ const detected = await detector.detect();
166
+ pythonExecutable = detected.executable;
167
+ logger.info(`Auto-detected Python: ${detected.executable} (${detected.version})`);
168
+ } catch {
169
+ logger.warn('No Python detected — using system default "python3"');
170
+ pythonExecutable = 'python3';
171
+ }
172
+ }
173
+
174
+ return pythonExecutable;
175
+ }
176
+
177
+ private async _bootstrapPythonRuntime(): Promise<void> {
178
+ try {
179
+ // Initialize nodepyx Python runtime helpers
180
+ const bootstrapCode = `
181
+ import sys
182
+ import os
183
+
184
+ # Add nodepyx runtime to path
185
+ _nodepyx_runtime_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'python')
186
+ if _nodepyx_runtime_dir not in sys.path:
187
+ sys.path.insert(0, _nodepyx_runtime_dir)
188
+
189
+ # Initialize runtime
190
+ try:
191
+ import nodepyx_runtime
192
+ nodepyx_runtime.initialize()
193
+ except ImportError:
194
+ pass # Runtime helpers not available, basic operation only
195
+ `;
196
+
197
+ await this._addon.runPythonCode(bootstrapCode);
198
+ logger.debug('Python runtime helpers bootstrapped');
199
+ } catch (err) {
200
+ // Non-fatal — runtime helpers are optional
201
+ logger.warn('Failed to bootstrap Python runtime helpers', err);
202
+ }
203
+ }
204
+
205
+ // ─── Module Import ──────────────────────────────────────────────────────────
206
+
207
+ /**
208
+ * Import a Python module and return a PyProxy.
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const pd = await runtime.import('pandas');
213
+ * const df = await pd.read_csv('data.csv');
214
+ * ```
215
+ */
216
+ async import(moduleName: string): Promise<PyModule> {
217
+ this._ensureRunning();
218
+
219
+ // Check cache
220
+ if (this._moduleCache.has(moduleName)) {
221
+ return this._moduleCache.get(moduleName)!;
222
+ }
223
+
224
+ logger.debug(`Importing Python module: ${moduleName}`);
225
+
226
+ const result = await this._addon.importModule(moduleName) as {
227
+ success?: boolean;
228
+ objectId?: number;
229
+ isObject?: boolean;
230
+ error?: { type: string; message: string; traceback: string };
231
+ };
232
+
233
+ if (!result.success) {
234
+ if (result.error) {
235
+ throw translatePythonError({
236
+ type: result.error.type,
237
+ message: result.error.message,
238
+ traceback: result.error.traceback || '',
239
+ });
240
+ }
241
+ throw new Error(`Failed to import Python module '${moduleName}'`);
242
+ }
243
+
244
+ if (!result.objectId) {
245
+ throw new Error(`Module '${moduleName}' imported but no object ID returned`);
246
+ }
247
+
248
+ // Run plugin hooks
249
+ await this._pluginManager.onModuleImported(moduleName, result.objectId as number);
250
+
251
+ // Create PyModule
252
+ const module = PyModule.fromRef(
253
+ result.objectId as PyObjectId,
254
+ moduleName,
255
+ this._addon,
256
+ this._config.callTimeout
257
+ );
258
+
259
+ // Generate TypeScript stubs (background, non-blocking)
260
+ // TypeScript stub generation (non-blocking, background)
261
+ if (this._config.proxyCache) {
262
+ this._typeGenerator.generateStub(moduleName, { name: moduleName, functions: [], classes: [], constants: [], submodules: [] }).catch((err: unknown) => {
263
+ logger.debug(`Stub generation for ${moduleName} failed (non-fatal)`, err);
264
+ });
265
+ }
266
+
267
+ // Cache
268
+ this._moduleCache.set(moduleName, module);
269
+
270
+ logger.debug(`Module '${moduleName}' imported (id=${result.objectId})`);
271
+ return module;
272
+ }
273
+
274
+ /**
275
+ * Import multiple modules at once (concurrent).
276
+ */
277
+ async importAll(moduleNames: string[]): Promise<Record<string, PyModule>> {
278
+ const modules = await Promise.all(
279
+ moduleNames.map(async name => [name, await this.import(name)] as const)
280
+ );
281
+ return Object.fromEntries(modules);
282
+ }
283
+
284
+ // ─── Code Execution ────────────────────────────────────────────────────────
285
+
286
+ /**
287
+ * Evaluate a Python expression.
288
+ */
289
+ async eval(expression: string): Promise<unknown> {
290
+ this._ensureRunning();
291
+
292
+ const result = await this._addon.evalPython(expression) as {
293
+ success?: boolean;
294
+ resultJson?: string;
295
+ objectId?: number;
296
+ isObject?: boolean;
297
+ error?: { type: string; message: string; traceback: string };
298
+ };
299
+
300
+ if (!result.success) {
301
+ if (result.error) {
302
+ throw translatePythonError({
303
+ type: result.error.type,
304
+ message: result.error.message,
305
+ traceback: result.error.traceback || '',
306
+ });
307
+ }
308
+ throw new Error('Python eval failed');
309
+ }
310
+
311
+ if (result.isObject && result.objectId) {
312
+ return PyProxy._createFromObjectId(
313
+ result.objectId as PyObjectId,
314
+ [],
315
+ this._addon,
316
+ this._config.callTimeout
317
+ );
318
+ }
319
+
320
+ if (result.resultJson !== undefined) {
321
+ try {
322
+ return JSON.parse(result.resultJson);
323
+ } catch {
324
+ return result.resultJson;
325
+ }
326
+ }
327
+
328
+ return null;
329
+ }
330
+
331
+ /**
332
+ * Execute Python statements.
333
+ */
334
+ async exec(code: string): Promise<void> {
335
+ this._ensureRunning();
336
+
337
+ const result = await this._addon.runPythonCode(code) as {
338
+ success?: boolean;
339
+ error?: { type: string; message: string; traceback: string };
340
+ };
341
+
342
+ if (!result.success && result.error) {
343
+ throw translatePythonError({
344
+ type: result.error.type,
345
+ message: result.error.message,
346
+ traceback: result.error.traceback || '',
347
+ });
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Run a Python script file.
353
+ */
354
+ async runFile(filePath: string): Promise<unknown> {
355
+ this._ensureRunning();
356
+
357
+ const result = await this._addon.runPythonFile(filePath) as {
358
+ success?: boolean;
359
+ resultJson?: string;
360
+ objectId?: number;
361
+ isObject?: boolean;
362
+ error?: { type: string; message: string; traceback: string };
363
+ };
364
+
365
+ if (!result.success) {
366
+ if (result.error) {
367
+ throw translatePythonError({
368
+ type: result.error.type,
369
+ message: result.error.message,
370
+ traceback: result.error.traceback || '',
371
+ });
372
+ }
373
+ throw new Error('Failed to run Python file');
374
+ }
375
+
376
+ return null;
377
+ }
378
+
379
+ // ─── Context Management ─────────────────────────────────────────────────────
380
+
381
+ /**
382
+ * Create an isolated Python execution context.
383
+ */
384
+ async createContext(): Promise<PyContext> {
385
+ this._ensureRunning();
386
+ return PyContext.create(this._addon);
387
+ }
388
+
389
+ // ─── Package Management ────────────────────────────────────────────────────
390
+
391
+ /**
392
+ * Install Python packages into the active environment.
393
+ */
394
+ async installPackages(packages: string[]): Promise<void> {
395
+ this._ensureRunning();
396
+
397
+ const installer = new PackageInstaller({ pythonExecutable: this._config.pythonExecutable });
398
+ await installer.install(packages);
399
+
400
+ // Clear module cache so re-imports work
401
+ this._moduleCache.clear();
402
+ }
403
+
404
+ // ─── Memory Management ─────────────────────────────────────────────────────
405
+
406
+ /**
407
+ * Get current memory statistics.
408
+ */
409
+ getMemoryStats(): ReturnType<NativeAddon['getMemoryStats']> {
410
+ this._ensureRunning();
411
+ return this._addon.getMemoryStats();
412
+ }
413
+
414
+ /**
415
+ * Trigger Python garbage collection.
416
+ */
417
+ async collectGarbage(): Promise<void> {
418
+ this._ensureRunning();
419
+ await this._addon.collectGarbage();
420
+ }
421
+
422
+ // ─── Direct Addon Access ───────────────────────────────────────────────────
423
+
424
+ /**
425
+ * Get the underlying native addon.
426
+ * Advanced use only.
427
+ */
428
+ getAddon(): NativeAddon {
429
+ return this._addon;
430
+ }
431
+
432
+ // ─── State ─────────────────────────────────────────────────────────────────
433
+
434
+ isRunning(): boolean {
435
+ return this._running;
436
+ }
437
+
438
+ getConfig(): ResolvednodepyxConfig {
439
+ return { ...this._config };
440
+ }
441
+
442
+ // ─── Shutdown ──────────────────────────────────────────────────────────────
443
+
444
+ /**
445
+ * Gracefully shut down the Python runtime.
446
+ * Releases all Python objects and finalizes the interpreter.
447
+ */
448
+ async shutdown(): Promise<void> {
449
+ if (!this._running) {return;}
450
+
451
+ logger.info('Shutting down PyRuntime...');
452
+ this._running = false;
453
+
454
+ // Stop memory monitoring
455
+ this._memoryMonitor.stopMonitoring();
456
+
457
+ // Clear caches
458
+ this._moduleCache.clear();
459
+
460
+ // Finalize Python interpreter
461
+ try {
462
+ await this._addon.finalizePython();
463
+ logger.success('PyRuntime shut down');
464
+ } catch (err) {
465
+ logger.error('Error during Python finalization', err);
466
+ throw err;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Synchronous shutdown (for process.on('exit') handler).
472
+ * Best-effort only — may not complete all cleanup.
473
+ */
474
+ shutdownSync(): void {
475
+ if (!this._running) {return;}
476
+ this._running = false;
477
+ this._memoryMonitor.stopMonitoring();
478
+ this._moduleCache.clear();
479
+ // Cannot await here — best effort
480
+ logger.debug('Synchronous shutdown initiated');
481
+ }
482
+
483
+ // ─── Private Helpers ───────────────────────────────────────────────────────
484
+
485
+ private _ensureRunning(): void {
486
+ if (!this._running) {
487
+ throw new nodepyxShutdownError(
488
+ 'PyRuntime has been shut down. Create a new runtime with PyRuntime.create().'
489
+ );
490
+ }
491
+ }
492
+
493
+ private _parseBytes(str: string): number {
494
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)?$/i);
495
+ if (!match) {return 1_073_741_824;}
496
+ const value = parseFloat(match[1]);
497
+ const unit = (match[2] ?? 'B').toUpperCase();
498
+ const units: Record<string, number> = {
499
+ B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4,
500
+ };
501
+ return Math.floor(value * (units[unit] ?? 1));
502
+ }
503
+ }
504
+