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,790 @@
1
+ /**
2
+ * nodepyx — Python Bridge Implementation
3
+ * Core Python operations: import, eval, attribute access, function calls.
4
+ */
5
+
6
+ #include "python_bridge.h"
7
+ #include "gil_guard.h"
8
+ #include "type_converter.h"
9
+
10
+ #include <Python.h>
11
+ #include <sstream>
12
+ #include <stdexcept>
13
+ #include <iostream>
14
+ #include <algorithm>
15
+
16
+ namespace nodepyx {
17
+
18
+ // ─── Singleton ──────────────────────────────────────────────────────────────
19
+
20
+ PythonBridge& PythonBridge::getInstance() {
21
+ static PythonBridge instance;
22
+ return instance;
23
+ }
24
+
25
+ // ─── Initialize Python ──────────────────────────────────────────────────────
26
+
27
+ void PythonBridge::initializePython(
28
+ const std::string& pythonHome,
29
+ const std::string& pythonExecutable,
30
+ const std::vector<std::string>& extraSysPaths,
31
+ const std::vector<std::pair<std::string, std::string>>& envVars
32
+ ) {
33
+ if (_initialized) {
34
+ throw std::runtime_error("Python already initialized");
35
+ }
36
+
37
+ // Set environment variables before initialization
38
+ for (const auto& [key, val] : envVars) {
39
+ #ifdef _WIN32
40
+ _putenv_s(key.c_str(), val.c_str());
41
+ #else
42
+ setenv(key.c_str(), val.c_str(), 1);
43
+ #endif
44
+ }
45
+
46
+ // Configure Python home if specified
47
+ if (!pythonHome.empty()) {
48
+ PyConfig config;
49
+ PyConfig_InitPythonConfig(&config);
50
+ PyConfig_SetBytesString(&config, &config.home, pythonHome.c_str());
51
+ Py_InitializeFromConfig(&config);
52
+ PyConfig_Clear(&config);
53
+ } else {
54
+ Py_Initialize();
55
+ }
56
+
57
+ if (!Py_IsInitialized()) {
58
+ throw std::runtime_error("Failed to initialize Python interpreter");
59
+ }
60
+
61
+ // Enable thread support
62
+ PyEval_InitThreads();
63
+
64
+ // Save main thread state and release GIL so threads can run
65
+ _mainThreadState = PyEval_SaveThread();
66
+
67
+ // Add extra sys.path entries (done with GIL held)
68
+ if (!extraSysPaths.empty()) {
69
+ GILAcquire gil;
70
+ PyObject* sysModule = PyImport_ImportModule("sys");
71
+ if (sysModule) {
72
+ PyRef sysRef = PyRef::steal(sysModule);
73
+ PyObject* sysPath = PyObject_GetAttrString(sysModule, "path");
74
+ if (sysPath) {
75
+ PyRef pathRef = PyRef::steal(sysPath);
76
+ for (const auto& p : extraSysPaths) {
77
+ PyObject* pathStr = PyUnicode_FromString(p.c_str());
78
+ if (pathStr) {
79
+ PyList_Insert(sysPath, 0, pathStr);
80
+ Py_DECREF(pathStr);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ _initialized = true;
88
+ }
89
+
90
+ // ─── Finalize Python ────────────────────────────────────────────────────────
91
+
92
+ void PythonBridge::finalizePython() {
93
+ if (!_initialized) return;
94
+
95
+ // Re-acquire main thread's GIL before calling Py_Finalize
96
+ if (_mainThreadState) {
97
+ PyEval_RestoreThread(_mainThreadState);
98
+ _mainThreadState = nullptr;
99
+ }
100
+
101
+ // Clear iterators
102
+ {
103
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
104
+ for (auto& [id, iter] : _iterators) {
105
+ Py_XDECREF(iter);
106
+ }
107
+ _iterators.clear();
108
+ }
109
+
110
+ // Clear object registry
111
+ TypeConverter::clearRegistry();
112
+
113
+ // Finalize Python
114
+ Py_Finalize();
115
+ _initialized = false;
116
+ }
117
+
118
+ // ─── Helper: Make Error Result ───────────────────────────────────────────────
119
+
120
+ TaskResult PythonBridge::_makeErrorResult() {
121
+ TaskResult result;
122
+ result.success = false;
123
+ auto [type, msg, tb] = TypeConverter::captureException();
124
+ result.errorType = type;
125
+ result.errorMessage = msg;
126
+ result.errorTraceback = tb;
127
+ return result;
128
+ }
129
+
130
+ // ─── Helper: Make Success Result ────────────────────────────────────────────
131
+
132
+ TaskResult PythonBridge::_makeSuccessResult(PyObject* obj) {
133
+ TaskResult result;
134
+ result.success = true;
135
+
136
+ if (!obj || obj == Py_None) {
137
+ result.resultJson = "null";
138
+ return result;
139
+ }
140
+
141
+ // For objects that can't be fully serialized, register as opaque ref
142
+ if (TypeConverter::isNumpyArray(obj) ||
143
+ TypeConverter::isDataFrame(obj) ||
144
+ TypeConverter::isSeries(obj) ||
145
+ TypeConverter::isTorchTensor(obj) ||
146
+ TypeConverter::isModule(obj) ||
147
+ TypeConverter::isClass(obj)) {
148
+ // Always return as object reference for these types
149
+ result.objectId = TypeConverter::registerObject(obj);
150
+ result.isObject = true;
151
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
152
+ return result;
153
+ }
154
+
155
+ // For callable objects
156
+ if (PyCallable_Check(obj) && !PyFunction_Check(obj) &&
157
+ !PyModule_Check(obj) && !PyType_Check(obj)) {
158
+ // Instance of a class with __call__ — usually want as reference
159
+ result.objectId = TypeConverter::registerObject(obj);
160
+ result.isObject = true;
161
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
162
+ return result;
163
+ }
164
+
165
+ SerializedValue sv = TypeConverter::pythonToSerialized(obj);
166
+
167
+ if (sv.format == SerializedFormat::PYTHON_REF) {
168
+ result.objectId = sv.metadata.objectId;
169
+ result.isObject = true;
170
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
171
+ } else if (sv.format == SerializedFormat::JSON) {
172
+ result.resultJson = sv.jsonData;
173
+ result.isObject = false;
174
+ } else {
175
+ // Binary format — register as object for now
176
+ // In production, pass binary data through
177
+ result.objectId = TypeConverter::registerObject(obj);
178
+ result.isObject = true;
179
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
180
+ }
181
+
182
+ return result;
183
+ }
184
+
185
+ // ─── Module Import ──────────────────────────────────────────────────────────
186
+
187
+ TaskResult PythonBridge::importModuleSync(const std::string& moduleName) {
188
+ // GIL must be held by caller
189
+ PyObject* module = PyImport_ImportModule(moduleName.c_str());
190
+ if (!module) {
191
+ return _makeErrorResult();
192
+ }
193
+
194
+ PyRef modRef = PyRef::steal(module);
195
+ TaskResult result;
196
+ result.success = true;
197
+ result.objectId = TypeConverter::registerObject(module);
198
+ result.isObject = true;
199
+ result.resultJson = "{\"__pyref__\":" + std::to_string(result.objectId) + "}";
200
+ return result;
201
+ }
202
+
203
+ TaskResult PythonBridge::reloadModuleSync(uint32_t objectId) {
204
+ PyObject* module = TypeConverter::getObject(objectId);
205
+ if (!module) {
206
+ TaskResult r;
207
+ r.success = false;
208
+ r.errorType = "ValueError";
209
+ r.errorMessage = "Object ID " + std::to_string(objectId) + " not found";
210
+ return r;
211
+ }
212
+
213
+ PyObject* reloaded = PyImport_ReloadModule(module);
214
+ if (!reloaded) {
215
+ return _makeErrorResult();
216
+ }
217
+
218
+ PyRef reloadedRef = PyRef::steal(reloaded);
219
+
220
+ // Update registry
221
+ TypeConverter::decRef(objectId);
222
+ uint32_t newId = TypeConverter::registerObject(reloaded);
223
+
224
+ TaskResult result;
225
+ result.success = true;
226
+ result.objectId = newId;
227
+ result.isObject = true;
228
+ result.resultJson = "{\"__pyref__\":" + std::to_string(newId) + "}";
229
+ return result;
230
+ }
231
+
232
+ // ─── Attribute Access ────────────────────────────────────────────────────────
233
+
234
+ TaskResult PythonBridge::getAttributeSync(uint32_t objectId, const std::string& attrName) {
235
+ PyObject* obj = TypeConverter::getObject(objectId);
236
+ if (!obj) {
237
+ TaskResult r;
238
+ r.success = false;
239
+ r.errorType = "ValueError";
240
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
241
+ return r;
242
+ }
243
+
244
+ PyObject* attr = PyObject_GetAttrString(obj, attrName.c_str());
245
+ if (!attr) {
246
+ return _makeErrorResult();
247
+ }
248
+
249
+ PyRef attrRef = PyRef::steal(attr);
250
+ return _makeSuccessResult(attr);
251
+ }
252
+
253
+ TaskResult PythonBridge::getAttributePathSync(
254
+ uint32_t objectId,
255
+ const std::vector<std::string>& path
256
+ ) {
257
+ PyObject* obj = TypeConverter::getObject(objectId);
258
+ if (!obj) {
259
+ TaskResult r;
260
+ r.success = false;
261
+ r.errorType = "ValueError";
262
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
263
+ return r;
264
+ }
265
+
266
+ if (path.empty()) {
267
+ return _makeSuccessResult(obj);
268
+ }
269
+
270
+ PyObject* resolved = _resolveAttributePath(obj, path);
271
+ if (!resolved) {
272
+ return _makeErrorResult();
273
+ }
274
+
275
+ PyRef resolvedRef = PyRef::steal(resolved);
276
+ return _makeSuccessResult(resolved);
277
+ }
278
+
279
+ PyObject* PythonBridge::_resolveAttributePath(
280
+ PyObject* obj,
281
+ const std::vector<std::string>& path
282
+ ) {
283
+ if (!obj || path.empty()) {
284
+ if (obj) Py_INCREF(obj);
285
+ return obj;
286
+ }
287
+
288
+ Py_INCREF(obj);
289
+ PyObject* current = obj;
290
+
291
+ for (const auto& attr : path) {
292
+ PyObject* next = PyObject_GetAttrString(current, attr.c_str());
293
+ Py_DECREF(current);
294
+
295
+ if (!next) {
296
+ return nullptr; // Exception set
297
+ }
298
+ current = next;
299
+ }
300
+
301
+ return current; // new reference
302
+ }
303
+
304
+ TaskResult PythonBridge::setAttributeSync(
305
+ uint32_t objectId,
306
+ const std::string& attrName,
307
+ const SerializedValue& value
308
+ ) {
309
+ PyObject* obj = TypeConverter::getObject(objectId);
310
+ if (!obj) {
311
+ TaskResult r;
312
+ r.success = false;
313
+ r.errorType = "ValueError";
314
+ r.errorMessage = "Object not found";
315
+ return r;
316
+ }
317
+
318
+ PyObject* pyValue = _deserialize(value);
319
+ if (!pyValue) {
320
+ TaskResult r;
321
+ r.success = false;
322
+ r.errorType = "ValueError";
323
+ r.errorMessage = "Failed to deserialize value";
324
+ return r;
325
+ }
326
+ PyRef valueRef = PyRef::steal(pyValue);
327
+
328
+ int ret = PyObject_SetAttrString(obj, attrName.c_str(), pyValue);
329
+ if (ret != 0) {
330
+ return _makeErrorResult();
331
+ }
332
+
333
+ TaskResult result;
334
+ result.success = true;
335
+ result.resultJson = "true";
336
+ return result;
337
+ }
338
+
339
+ bool PythonBridge::hasAttributeSync(uint32_t objectId, const std::string& attrName) {
340
+ PyObject* obj = TypeConverter::getObject(objectId);
341
+ if (!obj) return false;
342
+ return PyObject_HasAttrString(obj, attrName.c_str()) == 1;
343
+ }
344
+
345
+ TaskResult PythonBridge::deleteAttributeSync(uint32_t objectId, const std::string& attrName) {
346
+ PyObject* obj = TypeConverter::getObject(objectId);
347
+ if (!obj) {
348
+ TaskResult r;
349
+ r.success = false;
350
+ r.errorType = "ValueError";
351
+ r.errorMessage = "Object not found";
352
+ return r;
353
+ }
354
+
355
+ int ret = PyObject_DelAttrString(obj, attrName.c_str());
356
+ if (ret != 0) {
357
+ return _makeErrorResult();
358
+ }
359
+
360
+ TaskResult result;
361
+ result.success = true;
362
+ result.resultJson = "true";
363
+ return result;
364
+ }
365
+
366
+ // ─── Function Calls ──────────────────────────────────────────────────────────
367
+
368
+ TaskResult PythonBridge::callFunctionSync(
369
+ uint32_t objectId,
370
+ const std::vector<std::string>& path,
371
+ const CallOptions& options
372
+ ) {
373
+ PyObject* obj = TypeConverter::getObject(objectId);
374
+ if (!obj) {
375
+ TaskResult r;
376
+ r.success = false;
377
+ r.errorType = "ValueError";
378
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
379
+ return r;
380
+ }
381
+
382
+ // Resolve path to callable
383
+ PyObject* callable;
384
+ if (path.empty()) {
385
+ Py_INCREF(obj);
386
+ callable = obj;
387
+ } else {
388
+ callable = _resolveAttributePath(obj, path);
389
+ if (!callable) {
390
+ return _makeErrorResult();
391
+ }
392
+ }
393
+ PyRef callableRef = PyRef::steal(callable);
394
+
395
+ if (!PyCallable_Check(callable)) {
396
+ // The result is the object at the path, not a callable
397
+ // Return it as a value
398
+ return _makeSuccessResult(callable);
399
+ }
400
+
401
+ // Build args and kwargs
402
+ PyObject* argsObj = nullptr;
403
+ PyObject* kwargsObj = nullptr;
404
+
405
+ if (!_buildCallArgs(options, argsObj, kwargsObj)) {
406
+ Py_XDECREF(argsObj);
407
+ Py_XDECREF(kwargsObj);
408
+ return _makeErrorResult();
409
+ }
410
+
411
+ PyRef argsRef = PyRef::steal(argsObj);
412
+ PyRef kwargsRef = PyRef::steal(kwargsObj);
413
+
414
+ // Make the call
415
+ PyObject* result = PyObject_Call(callable, argsObj, kwargsObj && PyDict_Size(kwargsObj) > 0 ? kwargsObj : nullptr);
416
+
417
+ if (!result) {
418
+ return _makeErrorResult();
419
+ }
420
+
421
+ PyRef resultRef = PyRef::steal(result);
422
+ return _makeSuccessResult(result);
423
+ }
424
+
425
+ TaskResult PythonBridge::callMethodSync(
426
+ uint32_t objectId,
427
+ const std::string& methodName,
428
+ const CallOptions& options
429
+ ) {
430
+ return callFunctionSync(objectId, {methodName}, options);
431
+ }
432
+
433
+ // ─── Build Call Args ─────────────────────────────────────────────────────────
434
+
435
+ bool PythonBridge::_buildCallArgs(
436
+ const CallOptions& options,
437
+ PyObject*& argsOut,
438
+ PyObject*& kwargsOut
439
+ ) {
440
+ // Build positional args tuple
441
+ argsOut = PyTuple_New(static_cast<Py_ssize_t>(options.args.size()));
442
+ if (!argsOut) return false;
443
+
444
+ for (size_t i = 0; i < options.args.size(); ++i) {
445
+ PyObject* arg = _deserialize(options.args[i]);
446
+ if (!arg) {
447
+ Py_DECREF(argsOut);
448
+ argsOut = nullptr;
449
+ return false;
450
+ }
451
+ PyTuple_SET_ITEM(argsOut, static_cast<Py_ssize_t>(i), arg); // steals reference
452
+ }
453
+
454
+ // Build kwargs dict
455
+ kwargsOut = PyDict_New();
456
+ if (!kwargsOut) {
457
+ Py_DECREF(argsOut);
458
+ argsOut = nullptr;
459
+ return false;
460
+ }
461
+
462
+ for (const auto& [key, val] : options.kwargs) {
463
+ PyObject* pyVal = _deserialize(val);
464
+ if (!pyVal) {
465
+ Py_DECREF(argsOut);
466
+ Py_DECREF(kwargsOut);
467
+ argsOut = nullptr;
468
+ kwargsOut = nullptr;
469
+ return false;
470
+ }
471
+ PyDict_SetItemString(kwargsOut, key.c_str(), pyVal);
472
+ Py_DECREF(pyVal);
473
+ }
474
+
475
+ return true;
476
+ }
477
+
478
+ // ─── Deserialize ─────────────────────────────────────────────────────────────
479
+
480
+ PyObject* PythonBridge::_deserialize(const SerializedValue& value) {
481
+ switch (value.format) {
482
+ case SerializedFormat::JSON:
483
+ return TypeConverter::jsonToPython(value.jsonData);
484
+
485
+ case SerializedFormat::MSGPACK:
486
+ return TypeConverter::msgpackToPython(value.binaryData);
487
+
488
+ case SerializedFormat::NUMPY_ARRAY:
489
+ return TypeConverter::bufferToNumpyArray(
490
+ value.binaryData,
491
+ value.metadata.dtype,
492
+ value.metadata.shape
493
+ );
494
+
495
+ case SerializedFormat::PYTHON_REF: {
496
+ PyObject* obj = TypeConverter::getObject(value.metadata.objectId);
497
+ if (!obj) {
498
+ PyErr_SetString(PyExc_ValueError, "Python object reference not found");
499
+ return nullptr;
500
+ }
501
+ Py_INCREF(obj);
502
+ return obj;
503
+ }
504
+
505
+ case SerializedFormat::BYTES:
506
+ return PyBytes_FromStringAndSize(
507
+ reinterpret_cast<const char*>(value.binaryData.data()),
508
+ static_cast<Py_ssize_t>(value.binaryData.size())
509
+ );
510
+
511
+ case SerializedFormat::NONE:
512
+ Py_RETURN_NONE;
513
+
514
+ default:
515
+ // Try JSON fallback
516
+ if (!value.jsonData.empty()) {
517
+ return TypeConverter::jsonToPython(value.jsonData);
518
+ }
519
+ Py_RETURN_NONE;
520
+ }
521
+ }
522
+
523
+ // ─── Code Execution ──────────────────────────────────────────────────────────
524
+
525
+ TaskResult PythonBridge::runCodeSync(const std::string& code) {
526
+ PyObject* mainModule = PyImport_AddModule("__main__");
527
+ if (!mainModule) return _makeErrorResult();
528
+
529
+ PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
530
+
531
+ PyObject* result = PyRun_String(code.c_str(), Py_file_input, globalDict, globalDict);
532
+ if (!result) {
533
+ return _makeErrorResult();
534
+ }
535
+
536
+ PyRef resultRef = PyRef::steal(result);
537
+ TaskResult r;
538
+ r.success = true;
539
+ r.resultJson = "null";
540
+ return r;
541
+ }
542
+
543
+ TaskResult PythonBridge::evalExpressionSync(const std::string& expression) {
544
+ PyObject* mainModule = PyImport_AddModule("__main__");
545
+ if (!mainModule) return _makeErrorResult();
546
+
547
+ PyObject* globalDict = PyModule_GetDict(mainModule); // borrowed
548
+
549
+ PyObject* result = PyRun_String(expression.c_str(), Py_eval_input, globalDict, globalDict);
550
+ if (!result) {
551
+ return _makeErrorResult();
552
+ }
553
+
554
+ PyRef resultRef = PyRef::steal(result);
555
+ return _makeSuccessResult(result);
556
+ }
557
+
558
+ TaskResult PythonBridge::runFileSync(const std::string& filePath) {
559
+ FILE* fp = fopen(filePath.c_str(), "r");
560
+ if (!fp) {
561
+ TaskResult r;
562
+ r.success = false;
563
+ r.errorType = "FileNotFoundError";
564
+ r.errorMessage = "Cannot open file: " + filePath;
565
+ return r;
566
+ }
567
+
568
+ PyObject* mainModule = PyImport_AddModule("__main__");
569
+ PyObject* globalDict = PyModule_GetDict(mainModule);
570
+
571
+ PyObject* result = PyRun_File(fp, filePath.c_str(), Py_file_input, globalDict, globalDict);
572
+ fclose(fp);
573
+
574
+ if (!result) {
575
+ return _makeErrorResult();
576
+ }
577
+
578
+ PyRef resultRef = PyRef::steal(result);
579
+ TaskResult r;
580
+ r.success = true;
581
+ r.resultJson = "null";
582
+ return r;
583
+ }
584
+
585
+ // ─── Object Operations ───────────────────────────────────────────────────────
586
+
587
+ TaskResult PythonBridge::getObjectValueSync(uint32_t objectId) {
588
+ PyObject* obj = TypeConverter::getObject(objectId);
589
+ if (!obj) {
590
+ TaskResult r;
591
+ r.success = false;
592
+ r.errorType = "ValueError";
593
+ r.errorMessage = "Object not found: id=" + std::to_string(objectId);
594
+ return r;
595
+ }
596
+ return _makeSuccessResult(obj);
597
+ }
598
+
599
+ std::string PythonBridge::getObjectReprSync(uint32_t objectId) {
600
+ PyObject* obj = TypeConverter::getObject(objectId);
601
+ if (!obj) return "<deleted object>";
602
+ return TypeConverter::getRepr(obj);
603
+ }
604
+
605
+ TaskResult PythonBridge::getObjectLengthSync(uint32_t objectId) {
606
+ PyObject* obj = TypeConverter::getObject(objectId);
607
+ if (!obj) {
608
+ TaskResult r;
609
+ r.success = false;
610
+ r.errorType = "ValueError";
611
+ r.errorMessage = "Object not found";
612
+ return r;
613
+ }
614
+
615
+ Py_ssize_t len = PyObject_Length(obj);
616
+ if (len < 0) {
617
+ return _makeErrorResult();
618
+ }
619
+
620
+ TaskResult result;
621
+ result.success = true;
622
+ result.resultJson = std::to_string(len);
623
+ return result;
624
+ }
625
+
626
+ TaskResult PythonBridge::getObjectKeysSync(uint32_t objectId) {
627
+ PyObject* obj = TypeConverter::getObject(objectId);
628
+ if (!obj) {
629
+ TaskResult r;
630
+ r.success = false;
631
+ r.errorType = "ValueError";
632
+ r.errorMessage = "Object not found";
633
+ return r;
634
+ }
635
+
636
+ PyObject* dirList = PyObject_Dir(obj);
637
+ if (!dirList) {
638
+ return _makeErrorResult();
639
+ }
640
+ PyRef dirRef = PyRef::steal(dirList);
641
+
642
+ std::ostringstream oss;
643
+ oss << "[";
644
+ Py_ssize_t size = PyList_Size(dirList);
645
+ bool first = true;
646
+
647
+ for (Py_ssize_t i = 0; i < size; ++i) {
648
+ PyObject* name = PyList_GetItem(dirList, i);
649
+ if (!name || !PyUnicode_Check(name)) continue;
650
+
651
+ const char* nameStr = PyUnicode_AsUTF8(name);
652
+ if (!nameStr) continue;
653
+
654
+ // Filter out private names
655
+ if (nameStr[0] == '_') continue;
656
+
657
+ if (!first) oss << ",";
658
+ first = false;
659
+ oss << "\"" << nameStr << "\"";
660
+ }
661
+ oss << "]";
662
+
663
+ TaskResult result;
664
+ result.success = true;
665
+ result.resultJson = oss.str();
666
+ return result;
667
+ }
668
+
669
+ // ─── Iterator Support ─────────────────────────────────────────────────────────
670
+
671
+ TaskResult PythonBridge::createIteratorSync(
672
+ uint32_t objectId,
673
+ const std::vector<std::string>& path
674
+ ) {
675
+ PyObject* obj = TypeConverter::getObject(objectId);
676
+ if (!obj) {
677
+ TaskResult r;
678
+ r.success = false;
679
+ r.errorType = "ValueError";
680
+ r.errorMessage = "Object not found";
681
+ return r;
682
+ }
683
+
684
+ // Resolve path
685
+ PyObject* target;
686
+ if (path.empty()) {
687
+ Py_INCREF(obj);
688
+ target = obj;
689
+ } else {
690
+ target = _resolveAttributePath(obj, path);
691
+ if (!target) return _makeErrorResult();
692
+ }
693
+ PyRef targetRef = PyRef::steal(target);
694
+
695
+ // Create iterator
696
+ PyObject* iter = PyObject_GetIter(target);
697
+ if (!iter) {
698
+ return _makeErrorResult();
699
+ }
700
+
701
+ uint32_t iteratorId = _nextIteratorId.fetch_add(1, std::memory_order_relaxed);
702
+ {
703
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
704
+ _iterators[iteratorId] = iter; // takes ownership
705
+ }
706
+
707
+ TaskResult result;
708
+ result.success = true;
709
+ result.objectId = iteratorId;
710
+ result.resultJson = "{\"iteratorId\":" + std::to_string(iteratorId) + "}";
711
+ return result;
712
+ }
713
+
714
+ TaskResult PythonBridge::iteratorNextSync(uint32_t iteratorId) {
715
+ PyObject* iter = nullptr;
716
+ {
717
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
718
+ auto it = _iterators.find(iteratorId);
719
+ if (it == _iterators.end()) {
720
+ TaskResult r;
721
+ r.success = false;
722
+ r.errorType = "ValueError";
723
+ r.errorMessage = "Iterator not found: id=" + std::to_string(iteratorId);
724
+ return r;
725
+ }
726
+ iter = it->second;
727
+ }
728
+
729
+ PyObject* next = PyIter_Next(iter);
730
+
731
+ if (!next) {
732
+ if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
733
+ PyErr_Clear();
734
+ // Normal end of iteration
735
+ TaskResult result;
736
+ result.success = true;
737
+ result.resultJson = "{\"done\":true}";
738
+ return result;
739
+ }
740
+ return _makeErrorResult();
741
+ }
742
+
743
+ PyRef nextRef = PyRef::steal(next);
744
+ TaskResult itemResult = _makeSuccessResult(next);
745
+
746
+ if (itemResult.isObject) {
747
+ itemResult.resultJson = "{\"done\":false,\"value\":{\"__pyref__\":" +
748
+ std::to_string(itemResult.objectId) + "}}";
749
+ } else {
750
+ itemResult.resultJson = "{\"done\":false,\"value\":" + itemResult.resultJson + "}";
751
+ }
752
+
753
+ return itemResult;
754
+ }
755
+
756
+ void PythonBridge::destroyIteratorSync(uint32_t iteratorId) {
757
+ PyObject* iter = nullptr;
758
+ {
759
+ std::lock_guard<std::mutex> lock(_iteratorsMutex);
760
+ auto it = _iterators.find(iteratorId);
761
+ if (it != _iterators.end()) {
762
+ iter = it->second;
763
+ _iterators.erase(it);
764
+ }
765
+ }
766
+ Py_XDECREF(iter);
767
+ }
768
+
769
+ // ─── Memory ─────────────────────────────────────────────────────────────────
770
+
771
+ void PythonBridge::collectGarbage() {
772
+ GILAcquire gil;
773
+ PyObject* gcModule = PyImport_ImportModule("gc");
774
+ if (gcModule) {
775
+ PyRef gcRef = PyRef::steal(gcModule);
776
+ PyObject* collect = PyObject_GetAttrString(gcModule, "collect");
777
+ if (collect) {
778
+ PyRef collectRef = PyRef::steal(collect);
779
+ PyObject* result = PyObject_CallNoArgs(collect);
780
+ Py_XDECREF(result);
781
+ } else { PyErr_Clear(); }
782
+ } else { PyErr_Clear(); }
783
+ }
784
+
785
+ size_t PythonBridge::getTrackedObjectCount() const {
786
+ return TypeConverter::getRegisteredCount();
787
+ }
788
+
789
+ } // namespace nodepyx
790
+