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
package/src/index.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Run Python libraries from Node.js as if they were Native.
|
|
5
|
+
* Embeds CPython in-process via N-API addon with full TypeScript types,
|
|
6
|
+
* Proxy-based API, async/await support, and Next.js integration.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { init, py } from 'nodepyx';
|
|
11
|
+
*
|
|
12
|
+
* await init({
|
|
13
|
+
* virtualenv: {
|
|
14
|
+
* path: './.venv',
|
|
15
|
+
* packages: ['pandas', 'numpy'],
|
|
16
|
+
* autoInstall: true,
|
|
17
|
+
* },
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* const pd = await py.import('pandas');
|
|
21
|
+
* const df = await pd.read_csv('data.csv');
|
|
22
|
+
* const result = await df.describe().to_dict('records');
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { PyRuntime } from './core/PyRuntime';
|
|
27
|
+
import { Logger } from './utils/Logger';
|
|
28
|
+
import { nodepyxNotInitializedError } from './utils/ErrorTranslator';
|
|
29
|
+
import type { nodepyxConfig } from './types/config';
|
|
30
|
+
import type { PyProxyInterface } from './types/python';
|
|
31
|
+
|
|
32
|
+
// ─── Singleton Runtime Management ──────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
let _runtime: PyRuntime | null = null;
|
|
35
|
+
let _initializing = false;
|
|
36
|
+
let _initPromise: Promise<void> | null = null;
|
|
37
|
+
|
|
38
|
+
const _logger = new Logger('nodepyx');
|
|
39
|
+
|
|
40
|
+
// ─── Main py Object (Proxy over PyRuntime) ─────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The main `py` object — a proxy over the PyRuntime singleton.
|
|
44
|
+
* Provides access to all PyRuntime methods after initialization.
|
|
45
|
+
*
|
|
46
|
+
* @throws {nodepyxNotInitializedError} if accessed before calling `init()`
|
|
47
|
+
*/
|
|
48
|
+
export const py: PyRuntime = new Proxy({} as PyRuntime, {
|
|
49
|
+
get(_target, prop: string | symbol): unknown {
|
|
50
|
+
if (!_runtime) {
|
|
51
|
+
// Provide helpful error for common misuse
|
|
52
|
+
if (prop === 'then') {
|
|
53
|
+
// Prevent silent "awaiting py" mistakes
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
throw new nodepyxNotInitializedError(
|
|
57
|
+
`Cannot access py.${String(prop)} — nodepyx is not initialized. ` +
|
|
58
|
+
`Call 'await nodepyx.init()' before using 'py'.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const value = (_runtime as unknown as Record<string | symbol, unknown>)[prop];
|
|
62
|
+
if (typeof value === 'function') {
|
|
63
|
+
return value.bind(_runtime);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
set(_target, prop: string | symbol, value: unknown): boolean {
|
|
69
|
+
if (!_runtime) {
|
|
70
|
+
throw new nodepyxNotInitializedError('nodepyx is not initialized.');
|
|
71
|
+
}
|
|
72
|
+
(_runtime as unknown as Record<string | symbol, unknown>)[prop] = value;
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
has(_target, prop: string | symbol): boolean {
|
|
77
|
+
if (!_runtime) {return false;}
|
|
78
|
+
return prop in _runtime;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── Lifecycle Functions ────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initialize the nodepyx runtime.
|
|
86
|
+
* Must be called before using `py` or any other nodepyx API.
|
|
87
|
+
*
|
|
88
|
+
* Calling `init()` multiple times is safe — subsequent calls are no-ops
|
|
89
|
+
* unless `shutdown()` was called first, or `force: true` is passed.
|
|
90
|
+
*
|
|
91
|
+
* @param config - nodepyx configuration options
|
|
92
|
+
* @param options - Additional initialization options
|
|
93
|
+
* @returns Promise that resolves when Python is ready
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* await init({
|
|
98
|
+
* virtualenv: { path: '.venv', packages: ['pandas'], autoInstall: true },
|
|
99
|
+
* logLevel: 'info',
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export async function init(
|
|
104
|
+
config?: nodepyxConfig,
|
|
105
|
+
options?: { force?: boolean }
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
// Already initialized
|
|
108
|
+
if (_runtime && !options?.force) {
|
|
109
|
+
_logger.debug('nodepyx already initialized, skipping');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Initialization in progress — wait for it
|
|
114
|
+
if (_initializing && _initPromise) {
|
|
115
|
+
return _initPromise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Force re-init: shut down first
|
|
119
|
+
if (_runtime && options?.force) {
|
|
120
|
+
await shutdown();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_initializing = true;
|
|
124
|
+
_initPromise = _doInit(config);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await _initPromise;
|
|
128
|
+
} finally {
|
|
129
|
+
_initializing = false;
|
|
130
|
+
_initPromise = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function _doInit(config?: nodepyxConfig): Promise<void> {
|
|
135
|
+
_logger.info('Initializing nodepyx runtime...');
|
|
136
|
+
const startTime = performance.now();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
_runtime = await PyRuntime.create(config);
|
|
140
|
+
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
141
|
+
_logger.success(`nodepyx runtime ready in ${elapsed}ms`);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
_runtime = null;
|
|
144
|
+
_logger.error('Failed to initialize nodepyx runtime', err);
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Shut down the nodepyx runtime and release all Python resources.
|
|
151
|
+
* After calling this, `py` is no longer accessible until `init()` is called again.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* await shutdown();
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export async function shutdown(): Promise<void> {
|
|
159
|
+
if (!_runtime) {
|
|
160
|
+
_logger.debug('nodepyx not initialized, nothing to shut down');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_logger.info('Shutting down nodepyx runtime...');
|
|
165
|
+
const runtime = _runtime;
|
|
166
|
+
_runtime = null;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await runtime.shutdown();
|
|
170
|
+
_logger.success('nodepyx runtime shut down');
|
|
171
|
+
} catch (err) {
|
|
172
|
+
_logger.error('Error during nodepyx shutdown', err);
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if nodepyx is initialized and ready to use.
|
|
179
|
+
*/
|
|
180
|
+
export function isInitialized(): boolean {
|
|
181
|
+
return _runtime !== null && _runtime.isRunning();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the current PyRuntime instance.
|
|
186
|
+
* @throws {nodepyxNotInitializedError} if not initialized
|
|
187
|
+
*/
|
|
188
|
+
export function getRuntime(): PyRuntime {
|
|
189
|
+
if (!_runtime) {
|
|
190
|
+
throw new nodepyxNotInitializedError();
|
|
191
|
+
}
|
|
192
|
+
return _runtime;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Convenience API ────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Import a Python module and return a Proxy.
|
|
199
|
+
* Shorthand for `py.import(moduleName)`.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* const np = await importModule('numpy');
|
|
204
|
+
* const arr = await np.array([1, 2, 3]);
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export async function importModule(moduleName: string): Promise<PyProxyInterface> {
|
|
208
|
+
if (!_runtime) {throw new nodepyxNotInitializedError();}
|
|
209
|
+
return _runtime.import(moduleName);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Evaluate a Python expression and return the result.
|
|
214
|
+
* Shorthand for `py.eval(expression)`.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const result = await evalPython('1 + 2 + 3');
|
|
219
|
+
* // result === 6
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
export async function evalPython(expression: string): Promise<unknown> {
|
|
223
|
+
if (!_runtime) {throw new nodepyxNotInitializedError();}
|
|
224
|
+
return _runtime.eval(expression);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Execute Python code (statements, not expressions).
|
|
229
|
+
* Shorthand for `py.exec(code)`.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* await execPython(`
|
|
234
|
+
* import sys
|
|
235
|
+
* print(f"Python {sys.version}")
|
|
236
|
+
* `);
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export async function execPython(code: string): Promise<void> {
|
|
240
|
+
if (!_runtime) {throw new nodepyxNotInitializedError();}
|
|
241
|
+
return _runtime.exec(code);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run a Python script file.
|
|
246
|
+
*/
|
|
247
|
+
export async function runFile(filePath: string): Promise<unknown> {
|
|
248
|
+
if (!_runtime) {throw new nodepyxNotInitializedError();}
|
|
249
|
+
return _runtime.runFile(filePath);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Install Python packages into the configured virtualenv.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* await installPackages(['pandas', 'numpy>=1.24.0', 'scikit-learn']);
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
export async function installPackages(packages: string[]): Promise<void> {
|
|
261
|
+
if (!_runtime) {throw new nodepyxNotInitializedError();}
|
|
262
|
+
return _runtime.installPackages(packages);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get memory statistics for the Python heap.
|
|
267
|
+
*/
|
|
268
|
+
export async function getMemoryStats(): Promise<{
|
|
269
|
+
pythonHeapBytes: number;
|
|
270
|
+
pythonHeapPeakBytes: number;
|
|
271
|
+
trackedObjects: number;
|
|
272
|
+
pendingCallbacks: number;
|
|
273
|
+
}> {
|
|
274
|
+
if (!_runtime) {throw new nodepyxNotInitializedError();}
|
|
275
|
+
return _runtime.getMemoryStats();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Trigger Python garbage collection.
|
|
280
|
+
*/
|
|
281
|
+
export async function collectGarbage(): Promise<void> {
|
|
282
|
+
if (!_runtime) {throw new nodepyxNotInitializedError();}
|
|
283
|
+
return _runtime.collectGarbage();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Auto-cleanup on process exit ──────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
let _cleanupRegistered = false;
|
|
289
|
+
|
|
290
|
+
function _registerCleanup(): void {
|
|
291
|
+
if (_cleanupRegistered) {return;}
|
|
292
|
+
_cleanupRegistered = true;
|
|
293
|
+
|
|
294
|
+
const cleanup = async () => {
|
|
295
|
+
if (_runtime) {
|
|
296
|
+
try {
|
|
297
|
+
await shutdown();
|
|
298
|
+
} catch {
|
|
299
|
+
// Ignore errors during cleanup
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
process.on('exit', () => {
|
|
305
|
+
// Synchronous cleanup on exit (best-effort)
|
|
306
|
+
if (_runtime) {
|
|
307
|
+
try {
|
|
308
|
+
_runtime.shutdownSync();
|
|
309
|
+
} catch {
|
|
310
|
+
// ignore
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
process.on('SIGINT', async () => {
|
|
316
|
+
await cleanup();
|
|
317
|
+
process.exit(0);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
process.on('SIGTERM', async () => {
|
|
321
|
+
await cleanup();
|
|
322
|
+
process.exit(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
process.on('uncaughtException', async (err) => {
|
|
326
|
+
_logger.error('Uncaught exception, shutting down nodepyx', err);
|
|
327
|
+
await cleanup();
|
|
328
|
+
throw err;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Register cleanup automatically
|
|
333
|
+
_registerCleanup();
|
|
334
|
+
|
|
335
|
+
// ─── Exports ───────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
// Core
|
|
338
|
+
export { PyRuntime } from './core/PyRuntime';
|
|
339
|
+
export { PyProxy } from './core/PyProxy';
|
|
340
|
+
export { PyModule } from './core/PyModule';
|
|
341
|
+
export { PyCallable } from './core/PyCallable';
|
|
342
|
+
export { PyIterator } from './core/PyIterator';
|
|
343
|
+
export { PyContext } from './core/PyContext';
|
|
344
|
+
|
|
345
|
+
// Serialization
|
|
346
|
+
export { Serializer } from './serialization/Serializer';
|
|
347
|
+
export { NumpyBridge } from './serialization/NumpyBridge';
|
|
348
|
+
export { DataFrameBridge } from './serialization/DataFrameBridge';
|
|
349
|
+
|
|
350
|
+
// Error types
|
|
351
|
+
export {
|
|
352
|
+
PythonError,
|
|
353
|
+
PythonValueError,
|
|
354
|
+
PythonTypeError,
|
|
355
|
+
PythonKeyError,
|
|
356
|
+
PythonIndexError,
|
|
357
|
+
PythonAttributeError,
|
|
358
|
+
PythonImportError,
|
|
359
|
+
PythonModuleNotFoundError,
|
|
360
|
+
PythonFileNotFoundError,
|
|
361
|
+
PythonPermissionError,
|
|
362
|
+
PythonRuntimeError,
|
|
363
|
+
PythonMemoryError,
|
|
364
|
+
PythonStopIteration,
|
|
365
|
+
PythonTimeoutError,
|
|
366
|
+
nodepyxNotInitializedError,
|
|
367
|
+
nodepyxShutdownError,
|
|
368
|
+
translatePythonError,
|
|
369
|
+
isPythonError,
|
|
370
|
+
isPythonErrorOfType,
|
|
371
|
+
} from './utils/ErrorTranslator';
|
|
372
|
+
|
|
373
|
+
// Type generation
|
|
374
|
+
export { TypeGenerator } from './types/TypeGenerator';
|
|
375
|
+
|
|
376
|
+
// Environment management
|
|
377
|
+
export { PythonDetector } from './env/PythonDetector';
|
|
378
|
+
export { VenvManager } from './env/VenvManager';
|
|
379
|
+
export { CondaManager } from './env/CondaManager';
|
|
380
|
+
export { PackageInstaller } from './env/PackageInstaller';
|
|
381
|
+
|
|
382
|
+
// Plugins
|
|
383
|
+
export { PluginManager } from './plugins/PluginManager';
|
|
384
|
+
|
|
385
|
+
// Types
|
|
386
|
+
export type {
|
|
387
|
+
nodepyxConfig,
|
|
388
|
+
ResolvednodepyxConfig,
|
|
389
|
+
VirtualenvConfig,
|
|
390
|
+
CondaConfig,
|
|
391
|
+
LogLevel,
|
|
392
|
+
PyObjectId,
|
|
393
|
+
PyTypeInfo,
|
|
394
|
+
PyParamInfo,
|
|
395
|
+
PyCallableInfo,
|
|
396
|
+
PyClassInfo,
|
|
397
|
+
PyModuleInspection,
|
|
398
|
+
SerializedFormat,
|
|
399
|
+
SerializedValue,
|
|
400
|
+
PyCallResult,
|
|
401
|
+
PyErrorInfo,
|
|
402
|
+
DataFrameResult,
|
|
403
|
+
SeriesResult,
|
|
404
|
+
NumPyArrayResult,
|
|
405
|
+
PyProxyInterface,
|
|
406
|
+
AttributePath,
|
|
407
|
+
NativeAddon,
|
|
408
|
+
} from './types/index';
|
|
409
|
+
|
|
410
|
+
// Default export
|
|
411
|
+
export default {
|
|
412
|
+
init,
|
|
413
|
+
shutdown,
|
|
414
|
+
isInitialized,
|
|
415
|
+
getRuntime,
|
|
416
|
+
py,
|
|
417
|
+
import: importModule,
|
|
418
|
+
eval: evalPython,
|
|
419
|
+
exec: execPython,
|
|
420
|
+
runFile,
|
|
421
|
+
installPackages,
|
|
422
|
+
getMemoryStats,
|
|
423
|
+
collectGarbage,
|
|
424
|
+
};
|
|
425
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — GIL Guard Implementation
|
|
3
|
+
* The GIL guard is header-only; this file provides explicit template
|
|
4
|
+
* instantiations and any platform-specific workarounds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#include "gil_guard.h"
|
|
8
|
+
|
|
9
|
+
// All implementation is in the header file (RAII classes are trivial inline).
|
|
10
|
+
// This .cpp exists to satisfy the build system and future non-inline additions.
|
|
11
|
+
|
|
12
|
+
namespace nodepyx {
|
|
13
|
+
|
|
14
|
+
// Verify at compile time that our RAII types are not accidentally copied
|
|
15
|
+
static_assert(!std::is_copy_constructible<GILAcquire>::value,
|
|
16
|
+
"GILAcquire must not be copy-constructible");
|
|
17
|
+
static_assert(!std::is_copy_constructible<GILRelease>::value,
|
|
18
|
+
"GILRelease must not be copy-constructible");
|
|
19
|
+
static_assert(!std::is_copy_constructible<PyRef>::value,
|
|
20
|
+
"PyRef must not be copy-constructible");
|
|
21
|
+
|
|
22
|
+
static_assert(std::is_move_constructible<PyRef>::value,
|
|
23
|
+
"PyRef must be move-constructible");
|
|
24
|
+
|
|
25
|
+
} // namespace nodepyx
|
|
26
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — GIL Guard Header
|
|
3
|
+
* RAII wrappers for the Python Global Interpreter Lock (GIL).
|
|
4
|
+
*
|
|
5
|
+
* The GIL ensures only one thread executes Python bytecode at a time.
|
|
6
|
+
* These guards make GIL management safe via RAII idiom.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#pragma once
|
|
10
|
+
|
|
11
|
+
#include <Python.h>
|
|
12
|
+
#include <stdexcept>
|
|
13
|
+
|
|
14
|
+
namespace nodepyx {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GILAcquire — RAII guard that acquires the GIL on construction
|
|
18
|
+
* and releases it on destruction.
|
|
19
|
+
*
|
|
20
|
+
* Use when Python code needs to run in a non-Python thread (e.g. a worker thread).
|
|
21
|
+
* The calling thread must NOT already hold the GIL.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```cpp
|
|
25
|
+
* void worker_thread_func() {
|
|
26
|
+
* GILAcquire gil; // GIL acquired
|
|
27
|
+
* PyObject* result = PyEval_EvalCode(...);
|
|
28
|
+
* // GIL released when gil goes out of scope
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
class GILAcquire {
|
|
33
|
+
public:
|
|
34
|
+
GILAcquire() noexcept : _state(PyGILState_Ensure()) {}
|
|
35
|
+
~GILAcquire() noexcept { PyGILState_Release(_state); }
|
|
36
|
+
|
|
37
|
+
// Non-copyable, non-movable
|
|
38
|
+
GILAcquire(const GILAcquire&) = delete;
|
|
39
|
+
GILAcquire& operator=(const GILAcquire&) = delete;
|
|
40
|
+
GILAcquire(GILAcquire&&) = delete;
|
|
41
|
+
GILAcquire& operator=(GILAcquire&&) = delete;
|
|
42
|
+
|
|
43
|
+
private:
|
|
44
|
+
PyGILState_STATE _state;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* GILRelease — RAII guard that releases the GIL on construction
|
|
49
|
+
* and re-acquires it on destruction.
|
|
50
|
+
*
|
|
51
|
+
* Use to temporarily release the GIL while performing blocking I/O or
|
|
52
|
+
* other operations that don't need Python, allowing other Python threads to run.
|
|
53
|
+
* The calling thread MUST already hold the GIL.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```cpp
|
|
57
|
+
* // Inside Python-holding code:
|
|
58
|
+
* {
|
|
59
|
+
* GILRelease release; // GIL released — other Python threads can run
|
|
60
|
+
* perform_blocking_io();
|
|
61
|
+
* } // GIL re-acquired here
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
class GILRelease {
|
|
65
|
+
public:
|
|
66
|
+
GILRelease() noexcept : _state(PyEval_SaveThread()) {}
|
|
67
|
+
~GILRelease() noexcept { PyEval_RestoreThread(_state); }
|
|
68
|
+
|
|
69
|
+
// Non-copyable, non-movable
|
|
70
|
+
GILRelease(const GILRelease&) = delete;
|
|
71
|
+
GILRelease& operator=(const GILRelease&) = delete;
|
|
72
|
+
GILRelease(GILRelease&&) = delete;
|
|
73
|
+
GILRelease& operator=(GILRelease&&) = delete;
|
|
74
|
+
|
|
75
|
+
private:
|
|
76
|
+
PyThreadState* _state;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* PyRef — RAII wrapper for PyObject* reference counting.
|
|
81
|
+
*
|
|
82
|
+
* Automatically calls Py_XDECREF on destruction.
|
|
83
|
+
* Use steal() for objects returned from Python C API with new references.
|
|
84
|
+
* Use borrow() for borrowed references (won't decref).
|
|
85
|
+
*/
|
|
86
|
+
class PyRef {
|
|
87
|
+
public:
|
|
88
|
+
PyRef() noexcept : _obj(nullptr), _owned(false) {}
|
|
89
|
+
|
|
90
|
+
~PyRef() noexcept {
|
|
91
|
+
reset();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Take ownership of an object (new reference from Python API) */
|
|
95
|
+
static PyRef steal(PyObject* obj) noexcept {
|
|
96
|
+
PyRef ref;
|
|
97
|
+
ref._obj = obj;
|
|
98
|
+
ref._owned = true;
|
|
99
|
+
return ref;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Borrow a reference (will Py_INCREF and Py_DECREF) */
|
|
103
|
+
static PyRef borrow(PyObject* obj) noexcept {
|
|
104
|
+
PyRef ref;
|
|
105
|
+
ref._obj = obj;
|
|
106
|
+
ref._owned = false;
|
|
107
|
+
if (obj) {
|
|
108
|
+
Py_INCREF(obj);
|
|
109
|
+
ref._owned = true;
|
|
110
|
+
}
|
|
111
|
+
return ref;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Increment reference count and get raw pointer */
|
|
115
|
+
PyObject* incref() const noexcept {
|
|
116
|
+
Py_XINCREF(_obj);
|
|
117
|
+
return _obj;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get raw pointer without changing reference count */
|
|
121
|
+
PyObject* get() const noexcept { return _obj; }
|
|
122
|
+
|
|
123
|
+
/** Release ownership without decrementing ref count */
|
|
124
|
+
PyObject* release() noexcept {
|
|
125
|
+
PyObject* obj = _obj;
|
|
126
|
+
_obj = nullptr;
|
|
127
|
+
_owned = false;
|
|
128
|
+
return obj;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Reset — decrements ref count if owned */
|
|
132
|
+
void reset() noexcept {
|
|
133
|
+
if (_owned && _obj) {
|
|
134
|
+
Py_DECREF(_obj);
|
|
135
|
+
}
|
|
136
|
+
_obj = nullptr;
|
|
137
|
+
_owned = false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Check if the object is valid (non-null) */
|
|
141
|
+
explicit operator bool() const noexcept { return _obj != nullptr; }
|
|
142
|
+
|
|
143
|
+
/** Check for Python exception state */
|
|
144
|
+
bool hasException() const noexcept {
|
|
145
|
+
return _obj == nullptr && PyErr_Occurred() != nullptr;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Non-copyable
|
|
149
|
+
PyRef(const PyRef&) = delete;
|
|
150
|
+
PyRef& operator=(const PyRef&) = delete;
|
|
151
|
+
|
|
152
|
+
// Movable
|
|
153
|
+
PyRef(PyRef&& other) noexcept : _obj(other._obj), _owned(other._owned) {
|
|
154
|
+
other._obj = nullptr;
|
|
155
|
+
other._owned = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
PyRef& operator=(PyRef&& other) noexcept {
|
|
159
|
+
if (this != &other) {
|
|
160
|
+
reset();
|
|
161
|
+
_obj = other._obj;
|
|
162
|
+
_owned = other._owned;
|
|
163
|
+
other._obj = nullptr;
|
|
164
|
+
other._owned = false;
|
|
165
|
+
}
|
|
166
|
+
return *this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private:
|
|
170
|
+
PyObject* _obj;
|
|
171
|
+
bool _owned;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
} // namespace nodepyx
|
|
175
|
+
|