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.
- package/LICENSE +22 -0
- package/README.md +399 -0
- package/binding.gyp +73 -0
- package/dist/core/PyCallable.d.ts +65 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +109 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +228 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +55 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +172 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +65 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +483 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +105 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +438 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/CondaManager.d.ts +118 -0
- package/dist/env/CondaManager.d.ts.map +1 -0
- package/dist/env/CondaManager.js +401 -0
- package/dist/env/CondaManager.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +233 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +609 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +103 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +381 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/VenvManager.d.ts +117 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +331 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/PythonTypeMapper.d.ts +87 -0
- package/dist/types/PythonTypeMapper.d.ts.map +1 -0
- package/dist/types/PythonTypeMapper.js +449 -0
- package/dist/types/PythonTypeMapper.js.map +1 -0
- package/dist/types/StubCache.d.ts +109 -0
- package/dist/types/StubCache.d.ts.map +1 -0
- package/dist/types/StubCache.js +333 -0
- package/dist/types/StubCache.js.map +1 -0
- package/dist/types/TypeGenerator.d.ts +139 -0
- package/dist/types/TypeGenerator.d.ts.map +1 -0
- package/dist/types/TypeGenerator.js +372 -0
- package/dist/types/TypeGenerator.js.map +1 -0
- package/dist/types/addon.d.ts +114 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +175 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +44 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +177 -0
- package/python/error_handler.py +433 -0
- package/python/nodepyx_runtime.py +575 -0
- package/python/serializer.py +379 -0
- package/python/type_inspector.py +288 -0
- package/scripts/build-native.js +68 -0
- package/scripts/download-prebuilds.js +99 -0
- package/scripts/generate-stubs.js +405 -0
- package/scripts/install.js +260 -0
- package/src/core/PyCallable.ts +137 -0
- package/src/core/PyContext.ts +296 -0
- package/src/core/PyIterator.ts +294 -0
- package/src/core/PyModule.ts +194 -0
- package/src/core/PyProxy.ts +605 -0
- package/src/core/PyRuntime.ts +504 -0
- package/src/env/CondaManager.ts +451 -0
- package/src/env/PackageInstaller.ts +738 -0
- package/src/env/PythonDetector.ts +414 -0
- package/src/env/VenvManager.ts +396 -0
- package/src/index.ts +425 -0
- package/src/native/gil_guard.cpp +26 -0
- package/src/native/gil_guard.h +175 -0
- package/src/native/nodepyx_addon.cpp +886 -0
- package/src/native/python_bridge.cpp +790 -0
- package/src/native/python_bridge.h +257 -0
- package/src/native/thread_pool.cpp +336 -0
- package/src/native/thread_pool.h +175 -0
- package/src/native/type_converter.cpp +901 -0
- package/src/native/type_converter.h +272 -0
- package/src/nextjs/PyProvider.tsx +123 -0
- package/src/nextjs/index.ts +21 -0
- package/src/nextjs/usePython.ts +106 -0
- package/src/nextjs/withnodepyx.ts +88 -0
- package/src/plugins/Plugin.interface.ts +51 -0
- package/src/plugins/PluginManager.ts +155 -0
- package/src/plugins/builtin/NumpyPlugin.ts +36 -0
- package/src/plugins/builtin/PandasPlugin.ts +49 -0
- package/src/plugins/builtin/TorchPlugin.ts +56 -0
- package/src/plugins/index.ts +7 -0
- package/src/serialization/DataFrameBridge.ts +398 -0
- package/src/serialization/MsgPackSerializer.ts +220 -0
- package/src/serialization/NumpyBridge.ts +332 -0
- package/src/serialization/Serializer.ts +320 -0
- package/src/types/PythonTypeMapper.ts +495 -0
- package/src/types/StubCache.ts +340 -0
- package/src/types/TypeGenerator.ts +491 -0
- package/src/types/addon.ts +170 -0
- package/src/types/config.ts +226 -0
- package/src/types/index.ts +55 -0
- package/src/types/python.ts +309 -0
- package/src/types/stubs/numpy.d.ts +441 -0
- package/src/types/stubs/pandas.d.ts +575 -0
- package/src/types/stubs/sklearn.d.ts +728 -0
- package/src/types/stubs/torch.d.ts +694 -0
- package/src/utils/ErrorTranslator.ts +220 -0
- package/src/utils/Logger.ts +119 -0
- 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
|
+
|