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,605 @@
1
+ /**
2
+ * nodepyx — PyProxy
3
+ *
4
+ * The heart of nodepyx's JavaScript API.
5
+ * Every Python object is represented as a PyProxy — a JavaScript Proxy that:
6
+ *
7
+ * 1. Intercepts property access → creates lazy child PyProxy
8
+ * 2. Intercepts function call → sends to Python worker thread
9
+ * 3. Intercepts `then` → resolves the proxy chain into a value (makes it awaitable)
10
+ * 4. Intercepts Symbol.iterator → enables for...of loops
11
+ * 5. Intercepts Symbol.asyncIterator → enables for await...of loops
12
+ * 6. Intercepts property assignment → sets Python attribute
13
+ *
14
+ * The key insight: attributes are NOT resolved eagerly. The chain
15
+ * `df.groupby('country').agg({'sales': 'sum'}).reset_index()`
16
+ * creates a chain of PyProxy objects, each recording the operation,
17
+ * and only executes when you `await` the final result.
18
+ */
19
+
20
+ import type { NativeAddon, AddonCallOptions } from '../types/addon';
21
+ import type {
22
+ PyObjectId,
23
+ AttributePath,
24
+ SerializedValue,
25
+ } from '../types/python';
26
+ import { translatePythonError } from '../utils/ErrorTranslator';
27
+ import { Logger } from '../utils/Logger';
28
+
29
+ const logger = new Logger('PyProxy');
30
+
31
+ // Internal symbols used to brand PyProxy and access internals
32
+ export const PYPROXY_INTERNAL = Symbol('nodepyx.internal');
33
+ export const PYPROXY_OBJECTID = Symbol('nodepyx.objectId');
34
+ export const PYPROXY_PATH = Symbol('nodepyx.path');
35
+
36
+ /**
37
+ * Internal state of a PyProxy.
38
+ */
39
+ export interface PyProxyInternal {
40
+ objectId: PyObjectId;
41
+ path: AttributePath;
42
+ addon: NativeAddon;
43
+ isResolved: boolean;
44
+ callTimeout?: number;
45
+ /** Cache of child proxies keyed by attribute name */
46
+ childCache: Map<string, PyProxy>;
47
+ }
48
+
49
+ /**
50
+ * PyProxy — wraps any Python object in JavaScript.
51
+ *
52
+ * Usage:
53
+ * ```typescript
54
+ * const pd = await py.import('pandas');
55
+ * const df = await pd.read_csv('data.csv'); // df is a PyProxy
56
+ * const rows = await df.describe().to_dict('records'); // chained calls
57
+ * ```
58
+ */
59
+ export class PyProxy {
60
+ /** Internal state — do not access directly from outside */
61
+ readonly [PYPROXY_INTERNAL]: PyProxyInternal;
62
+
63
+ constructor(internal: PyProxyInternal) {
64
+ this[PYPROXY_INTERNAL] = internal;
65
+ return new Proxy(this, PyProxy.handler) as PyProxy;
66
+ }
67
+
68
+ // ─── Proxy Handler ─────────────────────────────────────────────────────────
69
+
70
+ static readonly handler: ProxyHandler<PyProxy> = {
71
+ get(target: PyProxy, prop: string | symbol, receiver: unknown): unknown {
72
+ const internal = target[PYPROXY_INTERNAL];
73
+
74
+ // ── Internal Symbol Access ──────────────────────────────────────────
75
+ if (prop === PYPROXY_INTERNAL || prop === PYPROXY_OBJECTID || prop === PYPROXY_PATH) {
76
+ return Reflect.get(target, prop, receiver);
77
+ }
78
+
79
+ // ── JavaScript built-in symbols ──────────────────────────────────────
80
+ if (typeof prop === 'symbol') {
81
+ // Make for...of work (synchronous iteration via async iterator)
82
+ if (prop === Symbol.asyncIterator) {
83
+ return () => target._makeAsyncIterator(internal);
84
+ }
85
+
86
+ // Symbol.iterator — not directly supported for Python objects
87
+ if (prop === Symbol.iterator) {
88
+ return () => {
89
+ throw new Error(
90
+ 'Python objects are not synchronously iterable from JavaScript. ' +
91
+ 'Use "for await...of" instead.'
92
+ );
93
+ };
94
+ }
95
+
96
+ // Symbol.toPrimitive — for coercion
97
+ if (prop === Symbol.toPrimitive) {
98
+ return (hint: string) => {
99
+ if (hint === 'string') {return `[PyProxy id=${internal.objectId}]`;}
100
+ return NaN;
101
+ };
102
+ }
103
+
104
+ // Symbol.toStringTag
105
+ if (prop === Symbol.toStringTag) {
106
+ return 'PyProxy';
107
+ }
108
+
109
+ return Reflect.get(target, prop, receiver);
110
+ }
111
+
112
+ // ── toString / valueOf ────────────────────────────────────────────────
113
+ if (prop === 'toString') {
114
+ return () => `[PyProxy id=${internal.objectId} path=${internal.path.join('.')}]`;
115
+ }
116
+
117
+ if (prop === 'valueOf') {
118
+ return () => target;
119
+ }
120
+
121
+ // ── then (Thenable — makes it work with await) ─────────────────────────
122
+ if (prop === 'then') {
123
+ return (
124
+ onfulfilled?: (value: unknown) => unknown,
125
+ onrejected?: (reason: unknown) => unknown
126
+ ): Promise<unknown> => {
127
+ return target._resolve(internal).then(onfulfilled, onrejected);
128
+ };
129
+ }
130
+
131
+ // ── catch / finally (Promise chain support) ───────────────────────────
132
+ if (prop === 'catch') {
133
+ return (onrejected?: (reason: unknown) => unknown) =>
134
+ target._resolve(internal).catch(onrejected);
135
+ }
136
+
137
+ if (prop === 'finally') {
138
+ return (onfinally?: () => void) =>
139
+ target._resolve(internal).finally(onfinally);
140
+ }
141
+
142
+ // ── Internal nodepyx methods ───────────────────────────────────────────
143
+ if (prop.startsWith('__nodepyx_')) {
144
+ return Reflect.get(target, prop, receiver);
145
+ }
146
+
147
+ // ── Check child cache (optimization) ──────────────────────────────────
148
+ if (internal.childCache.has(prop)) {
149
+ return internal.childCache.get(prop);
150
+ }
151
+
152
+ // ── Create lazy child PyProxy ──────────────────────────────────────────
153
+ const childInternal: PyProxyInternal = {
154
+ objectId: internal.objectId,
155
+ path: [...internal.path, prop],
156
+ addon: internal.addon,
157
+ isResolved: false,
158
+ callTimeout: internal.callTimeout,
159
+ childCache: new Map(),
160
+ };
161
+
162
+ const child = new PyProxy(childInternal);
163
+
164
+ // Cache it for repeated access
165
+ if (internal.addon) {
166
+ internal.childCache.set(prop, child);
167
+ }
168
+
169
+ return child;
170
+ },
171
+
172
+ // ── Function call (proxy() or proxy.method()) ──────────────────────────
173
+ apply(target: PyProxy, _thisArg: unknown, args: unknown[]): Promise<unknown> {
174
+ const internal = target[PYPROXY_INTERNAL];
175
+ return target._call(internal, args);
176
+ },
177
+
178
+ // ── Property assignment ────────────────────────────────────────────────
179
+ set(target: PyProxy, prop: string | symbol, value: unknown): boolean {
180
+ const internal = target[PYPROXY_INTERNAL];
181
+
182
+ if (typeof prop === 'symbol' || prop.startsWith('__nodepyx_')) {
183
+ return Reflect.set(target, prop, value);
184
+ }
185
+
186
+ // Schedule async setAttribute (best-effort, no await)
187
+ target._setAttr(internal, String(prop), value).catch(err => {
188
+ logger.error(`Failed to set Python attribute '${String(prop)}'`, err);
189
+ });
190
+
191
+ return true;
192
+ },
193
+
194
+ // ── has() — needed for 'in' operator ──────────────────────────────────
195
+ has(_target: PyProxy, _prop: string | symbol): boolean {
196
+ // We can't know without checking Python; return true for everything
197
+ return true;
198
+ },
199
+
200
+ // ── deleteProperty ────────────────────────────────────────────────────
201
+ deleteProperty(target: PyProxy, prop: string | symbol): boolean {
202
+ if (typeof prop === 'string') {
203
+ const internal = target[PYPROXY_INTERNAL];
204
+ internal.addon.deleteAttribute(internal.objectId as number, prop).catch(err => {
205
+ logger.error(`Failed to delete Python attribute '${prop}'`, err);
206
+ });
207
+ internal.childCache.delete(prop);
208
+ }
209
+ return true;
210
+ },
211
+
212
+ // ── ownKeys — for Object.keys() ───────────────────────────────────────
213
+ ownKeys(_target: PyProxy): (string | symbol)[] {
214
+ // Can't synchronously get Python keys; return empty
215
+ return [];
216
+ },
217
+
218
+ getOwnPropertyDescriptor(_target: PyProxy, prop: string | symbol): PropertyDescriptor | undefined {
219
+ if (typeof prop === 'symbol') {return undefined;}
220
+ return {
221
+ enumerable: false,
222
+ configurable: true,
223
+ writable: false,
224
+ value: undefined,
225
+ };
226
+ },
227
+ };
228
+
229
+ // ─── Resolution (await) ────────────────────────────────────────────────────
230
+
231
+ private async _resolve(internal: PyProxyInternal): Promise<unknown> {
232
+ try {
233
+ const addon = internal.addon;
234
+
235
+ if (internal.path.length === 0) {
236
+ // Resolve the object itself
237
+ const result = await addon.getObjectValue(internal.objectId as number);
238
+ return PyProxy._deserializeAddonResult(result, internal);
239
+ }
240
+
241
+ // Resolve attribute path
242
+ const result = await addon.getAttributePath(
243
+ internal.objectId as number,
244
+ internal.path
245
+ );
246
+ return PyProxy._deserializeAddonResult(result, internal);
247
+ } catch (err) {
248
+ if (err && typeof err === 'object' && 'pythonType' in err) {
249
+ throw translatePythonError({
250
+ type: (err as { pythonType: string }).pythonType,
251
+ message: (err as unknown as { message: string }).message,
252
+ traceback: (err as unknown as { pythonTraceback: string }).pythonTraceback || '',
253
+ });
254
+ }
255
+ throw err;
256
+ }
257
+ }
258
+
259
+ // ─── Function Call ─────────────────────────────────────────────────────────
260
+
261
+ private async _call(internal: PyProxyInternal, args: unknown[]): Promise<unknown> {
262
+ try {
263
+ const addon = internal.addon;
264
+
265
+ // Serialize arguments
266
+ const serializedArgs = await Promise.all(
267
+ args.map(arg => PyProxy._serializeArg(arg))
268
+ );
269
+
270
+ const callOptions: AddonCallOptions = {
271
+ args: serializedArgs,
272
+ kwargs: {},
273
+ timeout: internal.callTimeout,
274
+ };
275
+
276
+ // If last arg is a plain object (not PyProxy/Array), treat as kwargs
277
+ if (args.length > 0) {
278
+ const lastArg = args[args.length - 1];
279
+ if (
280
+ lastArg !== null &&
281
+ typeof lastArg === 'object' &&
282
+ !Array.isArray(lastArg) &&
283
+ !(lastArg instanceof PyProxy) &&
284
+ !(lastArg instanceof ArrayBuffer) &&
285
+ !ArrayBuffer.isView(lastArg) &&
286
+ !('__pyref__' in lastArg) &&
287
+ PyProxy._isPlainObject(lastArg)
288
+ ) {
289
+ // Treat as kwargs
290
+ callOptions.args = serializedArgs.slice(0, -1);
291
+ const kwargsEntries = await Promise.all(
292
+ Object.entries(lastArg as Record<string, unknown>).map(
293
+ async ([k, v]) => [k, await PyProxy._serializeArg(v)] as [string, SerializedValue]
294
+ )
295
+ );
296
+ callOptions.kwargs = Object.fromEntries(kwargsEntries);
297
+ }
298
+ }
299
+
300
+ const result = await addon.callFunction(
301
+ internal.objectId as number,
302
+ internal.path,
303
+ callOptions
304
+ );
305
+
306
+ return PyProxy._deserializeAddonResult(result, internal);
307
+ } catch (err) {
308
+ if (err && typeof err === 'object' && 'pythonType' in err) {
309
+ throw translatePythonError({
310
+ type: (err as { pythonType: string }).pythonType,
311
+ message: (err as unknown as { message: string }).message,
312
+ traceback: (err as unknown as { pythonTraceback: string }).pythonTraceback || '',
313
+ });
314
+ }
315
+ throw err;
316
+ }
317
+ }
318
+
319
+ // ─── Async Iterator (for await...of) ──────────────────────────────────────
320
+
321
+ private async *_makeAsyncIterator(internal: PyProxyInternal): AsyncIterableIterator<unknown> {
322
+ const addon = internal.addon;
323
+
324
+ // Create Python iterator
325
+ const iterResult = await addon.createIterator(
326
+ internal.objectId as number,
327
+ internal.path
328
+ );
329
+
330
+ const iteratorResult = iterResult as { iteratorId?: number; success?: boolean; error?: { type: string; message: string; traceback: string } };
331
+
332
+ if (!iteratorResult.success) {
333
+ const err = iteratorResult.error;
334
+ if (err) {
335
+ throw translatePythonError({
336
+ type: err.type,
337
+ message: err.message,
338
+ traceback: err.traceback || '',
339
+ });
340
+ }
341
+ throw new Error('Failed to create Python iterator');
342
+ }
343
+
344
+ // Parse iteratorId from resultJson
345
+ let iteratorId: number;
346
+ try {
347
+ const parsed = JSON.parse((iterResult as unknown as { resultJson: string }).resultJson || '{}');
348
+ iteratorId = parsed.iteratorId;
349
+ } catch {
350
+ throw new Error('Failed to parse iterator ID');
351
+ }
352
+
353
+ if (!iteratorId) {
354
+ throw new Error('Iterator ID not returned from Python');
355
+ }
356
+
357
+ try {
358
+ while (true) {
359
+ const next = await addon.iteratorNext(iteratorId);
360
+ const nextResult = next as { resultJson?: string; success?: boolean };
361
+
362
+ if (!nextResult.success) {
363
+ break;
364
+ }
365
+
366
+ let parsed: { done?: boolean; value?: unknown };
367
+ try {
368
+ parsed = JSON.parse(nextResult.resultJson || '{"done":true}');
369
+ } catch {
370
+ break;
371
+ }
372
+
373
+ if (parsed.done) {
374
+ break;
375
+ }
376
+
377
+ // Deserialize the value
378
+ const value = parsed.value;
379
+ if (value && typeof value === 'object' && '__pyref__' in (value as Record<string, unknown>)) {
380
+ const refId = (value as { __pyref__: number }).__pyref__;
381
+ yield PyProxy._createFromObjectId(refId as PyObjectId, [], addon, internal.callTimeout);
382
+ } else {
383
+ yield value;
384
+ }
385
+ }
386
+ } finally {
387
+ // Always destroy the iterator
388
+ await addon.destroyIterator(iteratorId).catch(() => {});
389
+ }
390
+ }
391
+
392
+ // ─── Set Attribute ────────────────────────────────────────────────────────
393
+
394
+ private async _setAttr(
395
+ internal: PyProxyInternal,
396
+ propName: string,
397
+ value: unknown
398
+ ): Promise<void> {
399
+ const serialized = await PyProxy._serializeArg(value);
400
+ await internal.addon.setAttribute(
401
+ internal.objectId as number,
402
+ propName,
403
+ serialized
404
+ );
405
+ // Invalidate cache
406
+ internal.childCache.delete(propName);
407
+ }
408
+
409
+ // ─── Static Helpers ────────────────────────────────────────────────────────
410
+
411
+ static _createFromObjectId(
412
+ objectId: PyObjectId,
413
+ path: AttributePath,
414
+ addon: NativeAddon,
415
+ callTimeout?: number
416
+ ): PyProxy {
417
+ return new PyProxy({
418
+ objectId,
419
+ path,
420
+ addon,
421
+ isResolved: false,
422
+ callTimeout,
423
+ childCache: new Map(),
424
+ });
425
+ }
426
+
427
+ static _deserializeAddonResult(
428
+ result: unknown,
429
+ internal: PyProxyInternal
430
+ ): unknown {
431
+ const r = result as {
432
+ success?: boolean;
433
+ resultJson?: string;
434
+ objectId?: number;
435
+ isObject?: boolean;
436
+ error?: { type: string; message: string; traceback: string };
437
+ };
438
+
439
+ if (!r.success) {
440
+ if (r.error) {
441
+ throw translatePythonError({
442
+ type: r.error.type,
443
+ message: r.error.message,
444
+ traceback: r.error.traceback || '',
445
+ });
446
+ }
447
+ throw new Error('Python call failed without error details');
448
+ }
449
+
450
+ if (r.isObject && r.objectId) {
451
+ // Result is a Python object — wrap in PyProxy
452
+ return PyProxy._createFromObjectId(
453
+ r.objectId as PyObjectId,
454
+ [],
455
+ internal.addon,
456
+ internal.callTimeout
457
+ );
458
+ }
459
+
460
+ // Deserialize JSON result
461
+ if (r.resultJson !== undefined && r.resultJson !== null) {
462
+ try {
463
+ const parsed = JSON.parse(r.resultJson);
464
+ if (parsed && typeof parsed === 'object' && '__pyref__' in parsed) {
465
+ return PyProxy._createFromObjectId(
466
+ parsed.__pyref__ as PyObjectId,
467
+ [],
468
+ internal.addon,
469
+ internal.callTimeout
470
+ );
471
+ }
472
+ return parsed;
473
+ } catch {
474
+ return r.resultJson;
475
+ }
476
+ }
477
+
478
+ return null;
479
+ }
480
+
481
+ static async _serializeArg(value: unknown): Promise<SerializedValue> {
482
+ // Already a SerializedValue
483
+ if (
484
+ value !== null &&
485
+ typeof value === 'object' &&
486
+ 'format' in (value as Record<string, unknown>)
487
+ ) {
488
+ return value as SerializedValue;
489
+ }
490
+
491
+ // PyProxy → pass as Python reference
492
+ if (value instanceof PyProxy) {
493
+ const internal = value[PYPROXY_INTERNAL];
494
+ // If proxy has a path, resolve it first to get the objectId
495
+ if (internal.path.length > 0) {
496
+ // We need to resolve it — for now, keep as reference
497
+ // Production: could pre-resolve path to objectId
498
+ }
499
+ return {
500
+ format: 6 as const, // PYTHON_REF
501
+ data: null,
502
+ metadata: { objectId: internal.objectId as number },
503
+ };
504
+ }
505
+
506
+ // null / undefined
507
+ if (value === null || value === undefined) {
508
+ return { format: 0 as const, data: 'null', metadata: {} };
509
+ }
510
+
511
+ // TypedArrays → numpy-compatible binary
512
+ if (
513
+ value instanceof Float32Array ||
514
+ value instanceof Float64Array ||
515
+ value instanceof Int8Array ||
516
+ value instanceof Int16Array ||
517
+ value instanceof Int32Array ||
518
+ value instanceof Uint8Array ||
519
+ value instanceof Uint16Array ||
520
+ value instanceof Uint32Array ||
521
+ value instanceof BigInt64Array ||
522
+ value instanceof BigUint64Array
523
+ ) {
524
+ const ta = value;
525
+ const dtype = PyProxy._getTypedArrayDtype(ta);
526
+ return {
527
+ format: 2 as const, // NUMPY_ARRAY
528
+ data: new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength),
529
+ metadata: {
530
+ dtype,
531
+ shape: [ta.length],
532
+ },
533
+ };
534
+ }
535
+
536
+ // ArrayBuffer
537
+ if (value instanceof ArrayBuffer) {
538
+ return {
539
+ format: 7 as const, // BYTES
540
+ data: new Uint8Array(value),
541
+ metadata: {},
542
+ };
543
+ }
544
+
545
+ // Primitive types
546
+ if (
547
+ typeof value === 'number' ||
548
+ typeof value === 'string' ||
549
+ typeof value === 'boolean'
550
+ ) {
551
+ return {
552
+ format: 0 as const, // JSON
553
+ data: JSON.stringify(value),
554
+ metadata: {},
555
+ };
556
+ }
557
+
558
+ // Array or object — use JSON
559
+ try {
560
+ return {
561
+ format: 0 as const, // JSON
562
+ data: JSON.stringify(value),
563
+ metadata: {},
564
+ };
565
+ } catch {
566
+ // Can't serialize — this shouldn't happen for plain objects
567
+ return { format: 8 as const, data: null, metadata: {} }; // NONE
568
+ }
569
+ }
570
+
571
+ static _getTypedArrayDtype(arr: ArrayBufferView): string {
572
+ if (arr instanceof Float32Array) {return 'float32';}
573
+ if (arr instanceof Float64Array) {return 'float64';}
574
+ if (arr instanceof Int8Array) {return 'int8';}
575
+ if (arr instanceof Int16Array) {return 'int16';}
576
+ if (arr instanceof Int32Array) {return 'int32';}
577
+ if (arr instanceof BigInt64Array) {return 'int64';}
578
+ if (arr instanceof Uint8Array) {return 'uint8';}
579
+ if (arr instanceof Uint16Array) {return 'uint16';}
580
+ if (arr instanceof Uint32Array) {return 'uint32';}
581
+ if (arr instanceof BigUint64Array) {return 'uint64';}
582
+ return 'uint8';
583
+ }
584
+
585
+ static _isPlainObject(obj: unknown): boolean {
586
+ if (!obj || typeof obj !== 'object') {return false;}
587
+ const proto = Object.getPrototypeOf(obj);
588
+ return proto === null || proto === Object.prototype;
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Check if a value is a PyProxy.
594
+ */
595
+ export function isPyProxy(value: unknown): value is PyProxy {
596
+ return (
597
+ value instanceof PyProxy ||
598
+ (
599
+ value !== null &&
600
+ typeof value === 'object' &&
601
+ PYPROXY_INTERNAL in (value as object)
602
+ )
603
+ );
604
+ }
605
+