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,901 @@
1
+ /**
2
+ * nodepyx — Type Converter Implementation
3
+ * Python ↔ JavaScript/JSON type conversion with object registry.
4
+ */
5
+
6
+ #include "type_converter.h"
7
+ #include "gil_guard.h"
8
+
9
+ #include <Python.h>
10
+ #include <sstream>
11
+ #include <map>
12
+ #include <mutex>
13
+ #include <atomic>
14
+ #include <cassert>
15
+ #include <cstring>
16
+ #include <algorithm>
17
+ #include <stdexcept>
18
+
19
+ namespace nodepyx {
20
+
21
+ // ─── Object Registry (Thread-safe) ──────────────────────────────────────────
22
+
23
+ struct RegistryEntry {
24
+ PyObject* obj;
25
+ std::atomic<int32_t> refCount{1};
26
+
27
+ explicit RegistryEntry(PyObject* o) : obj(o) {}
28
+ };
29
+
30
+ static std::map<uint32_t, RegistryEntry*> g_registry;
31
+ static std::mutex g_registryMutex;
32
+ static std::atomic<uint32_t> g_nextId{1};
33
+
34
+ uint32_t TypeConverter::registerObject(PyObject* obj) {
35
+ if (!obj) return 0;
36
+
37
+ // GIL must be held by caller
38
+ Py_INCREF(obj);
39
+
40
+ uint32_t id = g_nextId.fetch_add(1, std::memory_order_relaxed);
41
+
42
+ std::lock_guard<std::mutex> lock(g_registryMutex);
43
+ g_registry.emplace(id, new RegistryEntry(obj));
44
+
45
+ return id;
46
+ }
47
+
48
+ PyObject* TypeConverter::getObject(uint32_t id) {
49
+ std::lock_guard<std::mutex> lock(g_registryMutex);
50
+ auto it = g_registry.find(id);
51
+ if (it == g_registry.end()) return nullptr;
52
+ return it->second->obj;
53
+ }
54
+
55
+ void TypeConverter::incRef(uint32_t id) {
56
+ std::lock_guard<std::mutex> lock(g_registryMutex);
57
+ auto it = g_registry.find(id);
58
+ if (it != g_registry.end()) {
59
+ it->second->refCount.fetch_add(1);
60
+ }
61
+ }
62
+
63
+ void TypeConverter::decRef(uint32_t id) {
64
+ RegistryEntry* toDelete = nullptr;
65
+ {
66
+ std::lock_guard<std::mutex> lock(g_registryMutex);
67
+ auto it = g_registry.find(id);
68
+ if (it == g_registry.end()) return;
69
+
70
+ int32_t newCount = it->second->refCount.fetch_sub(1) - 1;
71
+ if (newCount <= 0) {
72
+ toDelete = it->second;
73
+ g_registry.erase(it);
74
+ }
75
+ }
76
+
77
+ if (toDelete) {
78
+ // Must hold GIL to decref Python object
79
+ GILAcquire gil;
80
+ Py_XDECREF(toDelete->obj);
81
+ delete toDelete;
82
+ }
83
+ }
84
+
85
+ size_t TypeConverter::getRegisteredCount() {
86
+ std::lock_guard<std::mutex> lock(g_registryMutex);
87
+ return g_registry.size();
88
+ }
89
+
90
+ void TypeConverter::clearRegistry() {
91
+ std::map<uint32_t, RegistryEntry*> toDelete;
92
+ {
93
+ std::lock_guard<std::mutex> lock(g_registryMutex);
94
+ toDelete.swap(g_registry);
95
+ }
96
+
97
+ // Decref all objects with GIL held
98
+ GILAcquire gil;
99
+ for (auto& [id, entry] : toDelete) {
100
+ Py_XDECREF(entry->obj);
101
+ delete entry;
102
+ }
103
+ }
104
+
105
+ // ─── Type Name ─────────────────────────────────────────────────────────────
106
+
107
+ std::string TypeConverter::getTypeName(PyObject* obj) {
108
+ if (!obj) return "NoneType";
109
+ PyObject* type = reinterpret_cast<PyObject*>(Py_TYPE(obj));
110
+ PyObject* name = PyObject_GetAttrString(type, "__name__");
111
+ if (!name) {
112
+ PyErr_Clear();
113
+ return "unknown";
114
+ }
115
+ PyRef nameRef = PyRef::steal(name);
116
+ const char* str = PyUnicode_AsUTF8(name);
117
+ if (!str) {
118
+ PyErr_Clear();
119
+ return "unknown";
120
+ }
121
+ return str;
122
+ }
123
+
124
+ std::string TypeConverter::getQualifiedTypeName(PyObject* obj) {
125
+ if (!obj) return "NoneType";
126
+ PyObject* type = reinterpret_cast<PyObject*>(Py_TYPE(obj));
127
+
128
+ // Try __module__.__qualname__ first
129
+ PyObject* module = PyObject_GetAttrString(type, "__module__");
130
+ PyObject* qualname = PyObject_GetAttrString(type, "__qualname__");
131
+ PyErr_Clear();
132
+
133
+ std::string result;
134
+ if (module && PyUnicode_Check(module)) {
135
+ const char* mod = PyUnicode_AsUTF8(module);
136
+ if (mod) {
137
+ result = std::string(mod) + ".";
138
+ }
139
+ }
140
+ if (qualname && PyUnicode_Check(qualname)) {
141
+ const char* qn = PyUnicode_AsUTF8(qualname);
142
+ if (qn) result += qn;
143
+ } else {
144
+ result += getTypeName(obj);
145
+ }
146
+
147
+ Py_XDECREF(module);
148
+ Py_XDECREF(qualname);
149
+ return result;
150
+ }
151
+
152
+ bool TypeConverter::isCallable(PyObject* obj) {
153
+ return obj && PyCallable_Check(obj);
154
+ }
155
+
156
+ bool TypeConverter::isIterable(PyObject* obj) {
157
+ if (!obj) return false;
158
+ PyObject* iter = PyObject_GetIter(obj);
159
+ if (!iter) {
160
+ PyErr_Clear();
161
+ return false;
162
+ }
163
+ Py_DECREF(iter);
164
+ return true;
165
+ }
166
+
167
+ bool TypeConverter::isAsyncIterable(PyObject* obj) {
168
+ if (!obj) return false;
169
+ // Check for __aiter__ method
170
+ return PyObject_HasAttrString(obj, "__aiter__") == 1;
171
+ }
172
+
173
+ bool TypeConverter::isModule(PyObject* obj) {
174
+ return obj && PyModule_Check(obj);
175
+ }
176
+
177
+ bool TypeConverter::isClass(PyObject* obj) {
178
+ return obj && PyType_Check(obj);
179
+ }
180
+
181
+ // ─── Type Checks (Library-specific) ─────────────────────────────────────────
182
+
183
+ bool TypeConverter::isNumpyArray(PyObject* obj) {
184
+ if (!obj) return false;
185
+ // Check by type name to avoid hard dependency on numpy C API
186
+ std::string typeName = getQualifiedTypeName(obj);
187
+ return typeName == "numpy.ndarray" || typeName == "numpy.core.multiarray.ndarray";
188
+ }
189
+
190
+ bool TypeConverter::isDataFrame(PyObject* obj) {
191
+ if (!obj) return false;
192
+ std::string typeName = getQualifiedTypeName(obj);
193
+ return typeName == "pandas.core.frame.DataFrame" ||
194
+ typeName.find("DataFrame") != std::string::npos;
195
+ }
196
+
197
+ bool TypeConverter::isSeries(PyObject* obj) {
198
+ if (!obj) return false;
199
+ std::string typeName = getQualifiedTypeName(obj);
200
+ return typeName == "pandas.core.series.Series" ||
201
+ (typeName.find("Series") != std::string::npos &&
202
+ typeName.find("pandas") != std::string::npos);
203
+ }
204
+
205
+ bool TypeConverter::isTorchTensor(PyObject* obj) {
206
+ if (!obj) return false;
207
+ std::string typeName = getQualifiedTypeName(obj);
208
+ return typeName == "torch.Tensor" ||
209
+ typeName == "torch._C._TensorBase";
210
+ }
211
+
212
+ // ─── Python → SerializedValue ───────────────────────────────────────────────
213
+
214
+ SerializedValue TypeConverter::pythonToSerialized(PyObject* obj, int depth) {
215
+ SerializedValue result;
216
+
217
+ if (depth > MAX_DEPTH) {
218
+ // Too deep — register as opaque ref
219
+ result.format = SerializedFormat::PYTHON_REF;
220
+ result.metadata.objectId = registerObject(obj);
221
+ return result;
222
+ }
223
+
224
+ // None
225
+ if (obj == Py_None) {
226
+ result.format = SerializedFormat::JSON;
227
+ result.jsonData = "null";
228
+ return result;
229
+ }
230
+
231
+ // Bool (must check before int!)
232
+ if (PyBool_Check(obj)) {
233
+ result.format = SerializedFormat::JSON;
234
+ result.jsonData = (obj == Py_True) ? "true" : "false";
235
+ return result;
236
+ }
237
+
238
+ // Integer
239
+ if (PyLong_Check(obj)) {
240
+ long long val = PyLong_AsLongLong(obj);
241
+ if (PyErr_Occurred()) {
242
+ PyErr_Clear();
243
+ // Try as unsigned long long
244
+ unsigned long long uval = PyLong_AsUnsignedLongLong(obj);
245
+ if (PyErr_Occurred()) {
246
+ PyErr_Clear();
247
+ // Very large integer — convert to string
248
+ PyObject* strObj = PyObject_Str(obj);
249
+ if (strObj) {
250
+ PyRef strRef = PyRef::steal(strObj);
251
+ result.format = SerializedFormat::JSON;
252
+ result.jsonData = "\"" + std::string(PyUnicode_AsUTF8(strObj)) + "\"";
253
+ return result;
254
+ }
255
+ }
256
+ result.format = SerializedFormat::JSON;
257
+ result.jsonData = std::to_string(uval);
258
+ return result;
259
+ }
260
+ result.format = SerializedFormat::JSON;
261
+ result.jsonData = std::to_string(val);
262
+ return result;
263
+ }
264
+
265
+ // Float
266
+ if (PyFloat_Check(obj)) {
267
+ double val = PyFloat_AsDouble(obj);
268
+ if (val != val) { // NaN
269
+ result.format = SerializedFormat::JSON;
270
+ result.jsonData = "null"; // JSON doesn't have NaN
271
+ } else if (val == std::numeric_limits<double>::infinity()) {
272
+ result.format = SerializedFormat::JSON;
273
+ result.jsonData = "1e308"; // Large number approximation
274
+ } else if (val == -std::numeric_limits<double>::infinity()) {
275
+ result.format = SerializedFormat::JSON;
276
+ result.jsonData = "-1e308";
277
+ } else {
278
+ result.format = SerializedFormat::JSON;
279
+ std::ostringstream oss;
280
+ oss.precision(17);
281
+ oss << val;
282
+ result.jsonData = oss.str();
283
+ }
284
+ return result;
285
+ }
286
+
287
+ // String
288
+ if (PyUnicode_Check(obj)) {
289
+ const char* str = PyUnicode_AsUTF8(obj);
290
+ if (!str) {
291
+ PyErr_Clear();
292
+ result.format = SerializedFormat::JSON;
293
+ result.jsonData = "\"\"";
294
+ return result;
295
+ }
296
+ result.format = SerializedFormat::JSON;
297
+ result.jsonData = "\"" + jsonEscape(str) + "\"";
298
+ return result;
299
+ }
300
+
301
+ // Bytes
302
+ if (PyBytes_Check(obj)) {
303
+ Py_ssize_t size = PyBytes_Size(obj);
304
+ const char* data = PyBytes_AsString(obj);
305
+ result.format = SerializedFormat::BYTES;
306
+ result.binaryData.assign(data, data + size);
307
+ result.metadata.size_bytes = size;
308
+ return result;
309
+ }
310
+
311
+ // Numpy array — check before list/tuple
312
+ if (isNumpyArray(obj)) {
313
+ return numpyArrayToBuffer(obj);
314
+ }
315
+
316
+ // Pandas DataFrame
317
+ if (isDataFrame(obj)) {
318
+ return dataFrameToBuffer(obj);
319
+ }
320
+
321
+ // Pandas Series
322
+ if (isSeries(obj)) {
323
+ return seriesToBuffer(obj);
324
+ }
325
+
326
+ // List — serialize as JSON if small enough
327
+ if (PyList_Check(obj)) {
328
+ Py_ssize_t size = PyList_Size(obj);
329
+ if (size <= static_cast<Py_ssize_t>(MAX_INLINE_SIZE)) {
330
+ result.format = SerializedFormat::JSON;
331
+ result.jsonData = listToJson(obj, depth + 1);
332
+ } else {
333
+ // Large list — serialize as msgpack (fallback to JSON for now)
334
+ result.format = SerializedFormat::JSON;
335
+ result.jsonData = listToJson(obj, depth + 1);
336
+ }
337
+ result.metadata.length = size;
338
+ return result;
339
+ }
340
+
341
+ // Tuple
342
+ if (PyTuple_Check(obj)) {
343
+ result.format = SerializedFormat::JSON;
344
+ // Convert tuple to JSON array
345
+ PyObject* list = PySequence_List(obj);
346
+ if (list) {
347
+ PyRef listRef = PyRef::steal(list);
348
+ result.jsonData = listToJson(list, depth + 1);
349
+ result.metadata.length = PyList_Size(list);
350
+ } else {
351
+ PyErr_Clear();
352
+ result.jsonData = "[]";
353
+ }
354
+ return result;
355
+ }
356
+
357
+ // Dict
358
+ if (PyDict_Check(obj)) {
359
+ result.format = SerializedFormat::JSON;
360
+ result.jsonData = dictToJson(obj, depth + 1);
361
+ result.metadata.length = PyDict_Size(obj);
362
+ return result;
363
+ }
364
+
365
+ // Everything else → opaque Python reference
366
+ result.format = SerializedFormat::PYTHON_REF;
367
+ result.metadata.objectId = registerObject(obj);
368
+ return result;
369
+ }
370
+
371
+ // ─── Collection serialization ───────────────────────────────────────────────
372
+
373
+ std::string TypeConverter::listToJson(PyObject* seq, int depth) {
374
+ std::ostringstream oss;
375
+ oss << "[";
376
+
377
+ Py_ssize_t size = PySequence_Length(seq);
378
+ for (Py_ssize_t i = 0; i < size; ++i) {
379
+ if (i > 0) oss << ",";
380
+
381
+ PyObject* item = PySequence_GetItem(seq, i);
382
+ if (!item) {
383
+ PyErr_Clear();
384
+ oss << "null";
385
+ continue;
386
+ }
387
+ PyRef itemRef = PyRef::steal(item);
388
+
389
+ SerializedValue sv = pythonToSerialized(item, depth);
390
+ if (sv.format == SerializedFormat::JSON) {
391
+ oss << sv.jsonData;
392
+ } else {
393
+ // Non-JSON item in list — represent as object ref
394
+ oss << "{\"__pyref__\":" << sv.metadata.objectId << "}";
395
+ }
396
+ }
397
+
398
+ oss << "]";
399
+ return oss.str();
400
+ }
401
+
402
+ std::string TypeConverter::dictToJson(PyObject* mapping, int depth) {
403
+ std::ostringstream oss;
404
+ oss << "{";
405
+
406
+ PyObject* keys = PyMapping_Keys(mapping);
407
+ if (!keys) {
408
+ PyErr_Clear();
409
+ return "{}";
410
+ }
411
+ PyRef keysRef = PyRef::steal(keys);
412
+
413
+ Py_ssize_t size = PyList_Size(keys);
414
+ bool first = true;
415
+
416
+ for (Py_ssize_t i = 0; i < size; ++i) {
417
+ PyObject* key = PyList_GetItem(keys, i); // borrowed
418
+ if (!key) continue;
419
+
420
+ // Convert key to string
421
+ std::string keyStr;
422
+ if (PyUnicode_Check(key)) {
423
+ const char* k = PyUnicode_AsUTF8(key);
424
+ if (k) keyStr = k;
425
+ } else {
426
+ PyObject* strKey = PyObject_Str(key);
427
+ if (strKey) {
428
+ PyRef strRef = PyRef::steal(strKey);
429
+ const char* k = PyUnicode_AsUTF8(strKey);
430
+ if (k) keyStr = k;
431
+ } else {
432
+ PyErr_Clear();
433
+ }
434
+ }
435
+
436
+ PyObject* value = PyObject_GetItem(mapping, key);
437
+ if (!value) {
438
+ PyErr_Clear();
439
+ continue;
440
+ }
441
+ PyRef valueRef = PyRef::steal(value);
442
+
443
+ if (!first) oss << ",";
444
+ first = false;
445
+
446
+ oss << "\"" << jsonEscape(keyStr) << "\":";
447
+
448
+ SerializedValue sv = pythonToSerialized(value, depth);
449
+ if (sv.format == SerializedFormat::JSON) {
450
+ oss << sv.jsonData;
451
+ } else {
452
+ oss << "{\"__pyref__\":" << sv.metadata.objectId << "}";
453
+ }
454
+ }
455
+
456
+ oss << "}";
457
+ return oss.str();
458
+ }
459
+
460
+ // ─── Numpy Array Serialization ───────────────────────────────────────────────
461
+
462
+ SerializedValue TypeConverter::numpyArrayToBuffer(PyObject* array) {
463
+ SerializedValue result;
464
+ result.format = SerializedFormat::NUMPY_ARRAY;
465
+
466
+ // Get dtype name
467
+ PyObject* dtypeObj = PyObject_GetAttrString(array, "dtype");
468
+ if (dtypeObj) {
469
+ PyRef dtypeRef = PyRef::steal(dtypeObj);
470
+ PyObject* dtypeName = PyObject_GetAttrString(dtypeObj, "name");
471
+ if (dtypeName) {
472
+ PyRef nameRef = PyRef::steal(dtypeName);
473
+ const char* name = PyUnicode_AsUTF8(dtypeName);
474
+ if (name) result.metadata.dtype = name;
475
+ } else { PyErr_Clear(); }
476
+ } else { PyErr_Clear(); }
477
+
478
+ // Get shape
479
+ PyObject* shapeObj = PyObject_GetAttrString(array, "shape");
480
+ if (shapeObj) {
481
+ PyRef shapeRef = PyRef::steal(shapeObj);
482
+ Py_ssize_t ndim = PyTuple_Size(shapeObj);
483
+ for (Py_ssize_t i = 0; i < ndim; ++i) {
484
+ PyObject* dim = PyTuple_GetItem(shapeObj, i);
485
+ result.metadata.shape.push_back(PyLong_AsLongLong(dim));
486
+ }
487
+ } else { PyErr_Clear(); }
488
+
489
+ // Get the raw data buffer via numpy's tobytes() method
490
+ PyObject* tobytes = PyObject_GetAttrString(array, "tobytes");
491
+ if (tobytes) {
492
+ PyRef tobytesRef = PyRef::steal(tobytes);
493
+ PyObject* bytesObj = PyObject_CallNoArgs(tobytes);
494
+ if (bytesObj) {
495
+ PyRef bytesRef = PyRef::steal(bytesObj);
496
+ char* buffer;
497
+ Py_ssize_t bufLen;
498
+ if (PyBytes_AsStringAndSize(bytesObj, &buffer, &bufLen) == 0) {
499
+ result.binaryData.assign(buffer, buffer + bufLen);
500
+ result.metadata.size_bytes = bufLen;
501
+ } else {
502
+ PyErr_Clear();
503
+ }
504
+ } else { PyErr_Clear(); }
505
+ } else { PyErr_Clear(); }
506
+
507
+ // Get contiguity info
508
+ PyObject* flagsObj = PyObject_GetAttrString(array, "flags");
509
+ if (flagsObj) {
510
+ PyRef flagsRef = PyRef::steal(flagsObj);
511
+ PyObject* cContig = PyObject_GetAttrString(flagsObj, "c_contiguous");
512
+ if (cContig) {
513
+ PyRef ccRef = PyRef::steal(cContig);
514
+ result.metadata.isC_contiguous = PyObject_IsTrue(cContig) == 1;
515
+ } else { PyErr_Clear(); }
516
+ } else { PyErr_Clear(); }
517
+
518
+ return result;
519
+ }
520
+
521
+ // ─── Pandas DataFrame Serialization ─────────────────────────────────────────
522
+
523
+ SerializedValue TypeConverter::dataFrameToBuffer(PyObject* df) {
524
+ SerializedValue result;
525
+ result.format = SerializedFormat::PANDAS_DATAFRAME;
526
+
527
+ // Get column names
528
+ PyObject* columns = PyObject_GetAttrString(df, "columns");
529
+ if (columns) {
530
+ PyRef colRef = PyRef::steal(columns);
531
+ PyObject* tolist = PyObject_GetAttrString(columns, "tolist");
532
+ if (tolist) {
533
+ PyRef tolistRef = PyRef::steal(tolist);
534
+ PyObject* colList = PyObject_CallNoArgs(tolist);
535
+ if (colList) {
536
+ PyRef colListRef = PyRef::steal(colList);
537
+ Py_ssize_t ncols = PyList_Size(colList);
538
+ for (Py_ssize_t i = 0; i < ncols; ++i) {
539
+ PyObject* col = PyList_GetItem(colList, i);
540
+ if (col && PyUnicode_Check(col)) {
541
+ const char* colName = PyUnicode_AsUTF8(col);
542
+ if (colName) result.metadata.columns.push_back(colName);
543
+ }
544
+ }
545
+ } else { PyErr_Clear(); }
546
+ } else { PyErr_Clear(); }
547
+ } else { PyErr_Clear(); }
548
+
549
+ // Serialize to JSON via to_json() for broad compatibility
550
+ // In production, use Arrow format for better performance
551
+ PyObject* toJson = PyObject_GetAttrString(df, "to_json");
552
+ if (toJson) {
553
+ PyRef toJsonRef = PyRef::steal(toJson);
554
+
555
+ // Call df.to_json(orient='records')
556
+ PyObject* kwargs = PyDict_New();
557
+ PyObject* orientStr = PyUnicode_FromString("records");
558
+ PyDict_SetItemString(kwargs, "orient", orientStr);
559
+ Py_DECREF(orientStr);
560
+
561
+ PyObject* jsonStr = PyObject_Call(toJson, PyTuple_New(0), kwargs);
562
+ Py_DECREF(kwargs);
563
+
564
+ if (jsonStr) {
565
+ PyRef jsonRef = PyRef::steal(jsonStr);
566
+ const char* json = PyUnicode_AsUTF8(jsonStr);
567
+ if (json) {
568
+ result.jsonData = json;
569
+ result.format = SerializedFormat::PANDAS_DATAFRAME;
570
+ }
571
+ } else { PyErr_Clear(); }
572
+ } else { PyErr_Clear(); }
573
+
574
+ return result;
575
+ }
576
+
577
+ // ─── Pandas Series Serialization ─────────────────────────────────────────────
578
+
579
+ SerializedValue TypeConverter::seriesToBuffer(PyObject* series) {
580
+ SerializedValue result;
581
+ result.format = SerializedFormat::PANDAS_SERIES;
582
+
583
+ PyObject* toJson = PyObject_GetAttrString(series, "to_json");
584
+ if (toJson) {
585
+ PyRef toJsonRef = PyRef::steal(toJson);
586
+ PyObject* jsonStr = PyObject_CallNoArgs(toJson);
587
+ if (jsonStr) {
588
+ PyRef jsonRef = PyRef::steal(jsonStr);
589
+ const char* json = PyUnicode_AsUTF8(jsonStr);
590
+ if (json) result.jsonData = json;
591
+ } else { PyErr_Clear(); }
592
+ } else { PyErr_Clear(); }
593
+
594
+ // Get series name
595
+ PyObject* nameObj = PyObject_GetAttrString(series, "name");
596
+ if (nameObj && nameObj != Py_None) {
597
+ PyRef nameRef = PyRef::steal(nameObj);
598
+ if (PyUnicode_Check(nameObj)) {
599
+ const char* name = PyUnicode_AsUTF8(nameObj);
600
+ if (name) result.metadata.columns.push_back(name);
601
+ }
602
+ } else if (nameObj) {
603
+ Py_DECREF(nameObj);
604
+ }
605
+ PyErr_Clear();
606
+
607
+ return result;
608
+ }
609
+
610
+ // ─── JSON Deserialization ───────────────────────────────────────────────────
611
+
612
+ PyObject* TypeConverter::jsonToPython(const std::string& json) {
613
+ // Use Python's built-in json module
614
+ PyObject* jsonModule = PyImport_ImportModule("json");
615
+ if (!jsonModule) {
616
+ PyErr_Clear();
617
+ Py_RETURN_NONE;
618
+ }
619
+ PyRef jsonRef = PyRef::steal(jsonModule);
620
+
621
+ PyObject* loads = PyObject_GetAttrString(jsonModule, "loads");
622
+ if (!loads) {
623
+ PyErr_Clear();
624
+ Py_RETURN_NONE;
625
+ }
626
+ PyRef loadsRef = PyRef::steal(loads);
627
+
628
+ PyObject* strObj = PyUnicode_FromStringAndSize(json.c_str(), json.size());
629
+ if (!strObj) {
630
+ PyErr_Clear();
631
+ Py_RETURN_NONE;
632
+ }
633
+ PyRef strRef = PyRef::steal(strObj);
634
+
635
+ PyObject* result = PyObject_CallOneArg(loads, strObj);
636
+ if (!result) {
637
+ PyErr_Clear();
638
+ Py_RETURN_NONE;
639
+ }
640
+
641
+ return result; // new reference
642
+ }
643
+
644
+ PyObject* TypeConverter::msgpackToPython(const std::vector<uint8_t>& data) {
645
+ // Use msgpack Python library
646
+ PyObject* msgpackModule = PyImport_ImportModule("msgpack");
647
+ if (!msgpackModule) {
648
+ PyErr_Clear();
649
+ // Fallback: treat as bytes
650
+ return PyBytes_FromStringAndSize(
651
+ reinterpret_cast<const char*>(data.data()),
652
+ data.size()
653
+ );
654
+ }
655
+ PyRef mpRef = PyRef::steal(msgpackModule);
656
+
657
+ PyObject* unpackb = PyObject_GetAttrString(msgpackModule, "unpackb");
658
+ if (!unpackb) {
659
+ PyErr_Clear();
660
+ return PyBytes_FromStringAndSize(
661
+ reinterpret_cast<const char*>(data.data()),
662
+ data.size()
663
+ );
664
+ }
665
+ PyRef unpackbRef = PyRef::steal(unpackb);
666
+
667
+ PyObject* bytesObj = PyBytes_FromStringAndSize(
668
+ reinterpret_cast<const char*>(data.data()),
669
+ data.size()
670
+ );
671
+ if (!bytesObj) return nullptr;
672
+ PyRef bytesRef = PyRef::steal(bytesObj);
673
+
674
+ PyObject* kwargs = PyDict_New();
675
+ PyObject* rawFalse = Py_False;
676
+ Py_INCREF(rawFalse);
677
+ PyDict_SetItemString(kwargs, "raw", rawFalse);
678
+ Py_DECREF(rawFalse);
679
+
680
+ PyObject* result = PyObject_Call(unpackb, PyTuple_Pack(1, bytesObj), kwargs);
681
+ Py_DECREF(kwargs);
682
+
683
+ if (!result) {
684
+ PyErr_Clear();
685
+ Py_RETURN_NONE;
686
+ }
687
+
688
+ return result;
689
+ }
690
+
691
+ PyObject* TypeConverter::bufferToNumpyArray(
692
+ const std::vector<uint8_t>& data,
693
+ const std::string& dtype,
694
+ const std::vector<int64_t>& shape
695
+ ) {
696
+ PyObject* np = PyImport_ImportModule("numpy");
697
+ if (!np) {
698
+ PyErr_Clear();
699
+ return nullptr;
700
+ }
701
+ PyRef npRef = PyRef::steal(np);
702
+
703
+ PyObject* frombuffer = PyObject_GetAttrString(np, "frombuffer");
704
+ if (!frombuffer) {
705
+ PyErr_Clear();
706
+ return nullptr;
707
+ }
708
+ PyRef frombufferRef = PyRef::steal(frombuffer);
709
+
710
+ PyObject* bytesObj = PyBytes_FromStringAndSize(
711
+ reinterpret_cast<const char*>(data.data()),
712
+ data.size()
713
+ );
714
+ if (!bytesObj) return nullptr;
715
+ PyRef bytesRef = PyRef::steal(bytesObj);
716
+
717
+ PyObject* dtypeStr = PyUnicode_FromString(dtype.c_str());
718
+ PyRef dtypeRef = PyRef::steal(dtypeStr);
719
+
720
+ PyObject* kwargs = PyDict_New();
721
+ PyDict_SetItemString(kwargs, "dtype", dtypeStr);
722
+
723
+ PyObject* arr = PyObject_Call(frombuffer, PyTuple_Pack(1, bytesObj), kwargs);
724
+ Py_DECREF(kwargs);
725
+
726
+ if (!arr) {
727
+ PyErr_Clear();
728
+ return nullptr;
729
+ }
730
+ PyRef arrRef = PyRef::steal(arr);
731
+
732
+ // Reshape if needed
733
+ if (shape.size() > 1) {
734
+ PyObject* reshape = PyObject_GetAttrString(arr, "reshape");
735
+ if (reshape) {
736
+ PyRef reshapeRef = PyRef::steal(reshape);
737
+
738
+ PyObject* shapeList = PyList_New(shape.size());
739
+ for (size_t i = 0; i < shape.size(); ++i) {
740
+ PyList_SetItem(shapeList, i, PyLong_FromLongLong(shape[i]));
741
+ }
742
+ PyRef shapeListRef = PyRef::steal(shapeList);
743
+
744
+ PyObject* reshaped = PyObject_CallOneArg(reshape, shapeList);
745
+ if (reshaped) {
746
+ return reshaped;
747
+ }
748
+ PyErr_Clear();
749
+ } else {
750
+ PyErr_Clear();
751
+ }
752
+ }
753
+
754
+ return arrRef.release();
755
+ }
756
+
757
+ // ─── String / Repr ──────────────────────────────────────────────────────────
758
+
759
+ std::string TypeConverter::getRepr(PyObject* obj) {
760
+ if (!obj) return "<null>";
761
+ PyObject* repr = PyObject_Repr(obj);
762
+ if (!repr) {
763
+ PyErr_Clear();
764
+ return "<error getting repr>";
765
+ }
766
+ PyRef reprRef = PyRef::steal(repr);
767
+ const char* str = PyUnicode_AsUTF8(repr);
768
+ if (!str) {
769
+ PyErr_Clear();
770
+ return "<encoding error>";
771
+ }
772
+ return str;
773
+ }
774
+
775
+ std::string TypeConverter::getStr(PyObject* obj) {
776
+ if (!obj) return "None";
777
+ PyObject* str = PyObject_Str(obj);
778
+ if (!str) {
779
+ PyErr_Clear();
780
+ return "<error>";
781
+ }
782
+ PyRef strRef = PyRef::steal(str);
783
+ const char* cstr = PyUnicode_AsUTF8(str);
784
+ if (!cstr) {
785
+ PyErr_Clear();
786
+ return "";
787
+ }
788
+ return cstr;
789
+ }
790
+
791
+ // ─── Exception Capture ──────────────────────────────────────────────────────
792
+
793
+ std::tuple<std::string, std::string, std::string> TypeConverter::captureException() {
794
+ std::string typeName, message, traceback;
795
+
796
+ PyObject* excType = nullptr;
797
+ PyObject* excValue = nullptr;
798
+ PyObject* excTb = nullptr;
799
+
800
+ PyErr_Fetch(&excType, &excValue, &excTb);
801
+ PyErr_NormalizeException(&excType, &excValue, &excTb);
802
+
803
+ PyRef typeRef = PyRef::steal(excType);
804
+ PyRef valueRef = PyRef::steal(excValue);
805
+ PyRef tbRef = PyRef::steal(excTb);
806
+
807
+ // Get exception type name
808
+ if (excType) {
809
+ PyObject* name = PyObject_GetAttrString(excType, "__name__");
810
+ if (name) {
811
+ PyRef nameRef = PyRef::steal(name);
812
+ const char* n = PyUnicode_AsUTF8(name);
813
+ if (n) typeName = n;
814
+ } else { PyErr_Clear(); }
815
+ }
816
+
817
+ // Get exception message
818
+ if (excValue) {
819
+ PyObject* str = PyObject_Str(excValue);
820
+ if (str) {
821
+ PyRef strRef = PyRef::steal(str);
822
+ const char* s = PyUnicode_AsUTF8(str);
823
+ if (s) message = s;
824
+ } else { PyErr_Clear(); }
825
+ }
826
+
827
+ // Get traceback
828
+ if (excTb) {
829
+ PyObject* tbModule = PyImport_ImportModule("traceback");
830
+ if (tbModule) {
831
+ PyRef tbModRef = PyRef::steal(tbModule);
832
+ PyObject* formatTb = PyObject_GetAttrString(tbModule, "format_tb");
833
+ if (formatTb) {
834
+ PyRef formatTbRef = PyRef::steal(formatTb);
835
+ PyObject* tbLines = PyObject_CallOneArg(formatTb, excTb);
836
+ if (tbLines) {
837
+ PyRef tbLinesRef = PyRef::steal(tbLines);
838
+ std::ostringstream oss;
839
+ oss << "Traceback (most recent call last):\n";
840
+ Py_ssize_t nlines = PyList_Size(tbLines);
841
+ for (Py_ssize_t i = 0; i < nlines; ++i) {
842
+ PyObject* line = PyList_GetItem(tbLines, i);
843
+ if (line && PyUnicode_Check(line)) {
844
+ const char* l = PyUnicode_AsUTF8(line);
845
+ if (l) oss << l;
846
+ }
847
+ }
848
+ oss << typeName << ": " << message;
849
+ traceback = oss.str();
850
+ } else { PyErr_Clear(); }
851
+ } else { PyErr_Clear(); }
852
+ } else { PyErr_Clear(); }
853
+ }
854
+
855
+ if (traceback.empty()) {
856
+ traceback = typeName + ": " + message;
857
+ }
858
+
859
+ return {typeName, message, traceback};
860
+ }
861
+
862
+ std::string TypeConverter::exceptionToJson() {
863
+ auto [type, msg, tb] = captureException();
864
+ std::ostringstream oss;
865
+ oss << "{\"type\":\"" << jsonEscape(type) << "\","
866
+ << "\"message\":\"" << jsonEscape(msg) << "\","
867
+ << "\"traceback\":\"" << jsonEscape(tb) << "\"}";
868
+ return oss.str();
869
+ }
870
+
871
+ // ─── JSON Escape ────────────────────────────────────────────────────────────
872
+
873
+ std::string TypeConverter::jsonEscape(const std::string& s) {
874
+ std::string result;
875
+ result.reserve(s.size() + 16);
876
+
877
+ for (unsigned char c : s) {
878
+ switch (c) {
879
+ case '"': result += "\\\""; break;
880
+ case '\\': result += "\\\\"; break;
881
+ case '\b': result += "\\b"; break;
882
+ case '\f': result += "\\f"; break;
883
+ case '\n': result += "\\n"; break;
884
+ case '\r': result += "\\r"; break;
885
+ case '\t': result += "\\t"; break;
886
+ default:
887
+ if (c < 0x20) {
888
+ char buf[8];
889
+ snprintf(buf, sizeof(buf), "\\u%04x", c);
890
+ result += buf;
891
+ } else {
892
+ result += static_cast<char>(c);
893
+ }
894
+ }
895
+ }
896
+
897
+ return result;
898
+ }
899
+
900
+ } // namespace nodepyx
901
+