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,379 @@
1
+ """
2
+ nodepyx — Python-side serializer
3
+ Converts Python objects into the nodepyx wire format consumed by the
4
+ TypeScript DataFrameBridge / NumpyBridge.
5
+
6
+ Wire format codes (must match NumpyBridge.ts NumpyDTypeCode enum):
7
+ 0 bool
8
+ 1 int8 2 int16 3 int32 4 int64
9
+ 5 uint8 6 uint16 7 uint32 8 uint64
10
+ 9 float32 10 float64
11
+ 11 complex64 12 complex128
12
+
13
+ DataFrame / Series are serialised as JSON (split orientation).
14
+ NumPy arrays use the custom binary layout described in NumpyBridge.ts.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import struct
21
+ import datetime
22
+ from typing import Any, Dict, List, Optional, Tuple, Union
23
+
24
+ # ── Optional heavy imports ────────────────────────────────────────────────────
25
+
26
+ try:
27
+ import numpy as np
28
+ HAS_NUMPY = True
29
+ except ImportError:
30
+ HAS_NUMPY = False
31
+
32
+ try:
33
+ import pandas as pd
34
+ HAS_PANDAS = True
35
+ except ImportError:
36
+ HAS_PANDAS = False
37
+
38
+ try:
39
+ import msgpack
40
+ HAS_MSGPACK = True
41
+ except ImportError:
42
+ HAS_MSGPACK = False
43
+
44
+ # ── dtype string → code ───────────────────────────────────────────────────────
45
+
46
+ _DTYPE_TO_CODE: Dict[str, int] = {
47
+ 'bool': 0,
48
+ 'int8': 1, 'int16': 2, 'int32': 3, 'int64': 4,
49
+ 'uint8': 5, 'uint16': 6, 'uint32': 7, 'uint64': 8,
50
+ 'float32': 9, 'float64': 10,
51
+ 'complex64': 11, 'complex128': 12,
52
+ }
53
+
54
+ _NUMPY_DTYPE_MAP: Dict[str, str] = {
55
+ 'bool': 'bool',
56
+ 'int8': 'int8', 'int16': 'int16', 'int32': 'int32', 'int64': 'int64',
57
+ 'uint8': 'uint8', 'uint16': 'uint16', 'uint32': 'uint32', 'uint64': 'uint64',
58
+ 'float32': 'float32', 'float64': 'float64',
59
+ 'complex64': 'complex64', 'complex128': 'complex128',
60
+ }
61
+
62
+ # ── SerializedFormat codes (match TypeScript SerializedFormat union) ──────────
63
+
64
+ FORMAT_JSON = 0
65
+ FORMAT_MSGPACK = 1
66
+ FORMAT_NUMPY = 2
67
+ FORMAT_DATAFRAME = 3
68
+ FORMAT_SERIES = 4
69
+ FORMAT_TORCH = 5
70
+ FORMAT_PYTHON_REF = 6
71
+ FORMAT_BYTES = 7
72
+ FORMAT_NONE = 8
73
+
74
+
75
+ # ─────────────────────────────────────────────────────────────────────────────
76
+ # Public API
77
+ # ─────────────────────────────────────────────────────────────────────────────
78
+
79
+ def serialize(value: Any, *, prefer_msgpack: bool = False) -> Dict[str, Any]:
80
+ """
81
+ Serialize *value* to a nodepyx wire-format dict:
82
+ { 'format': int, 'data': bytes|str|None, 'metadata': dict }
83
+ """
84
+ if value is None:
85
+ return _none()
86
+
87
+ if isinstance(value, bool):
88
+ return _json(str(value).lower())
89
+
90
+ if isinstance(value, int):
91
+ return _json(str(value))
92
+
93
+ if isinstance(value, float):
94
+ if value != value: # NaN
95
+ return _json('null')
96
+ return _json(repr(value))
97
+
98
+ if isinstance(value, complex):
99
+ return _json(json.dumps({'real': value.real, 'imag': value.imag}))
100
+
101
+ if isinstance(value, str):
102
+ return _json(json.dumps(value))
103
+
104
+ if isinstance(value, bytes):
105
+ return {'format': FORMAT_BYTES, 'data': value, 'metadata': {'length': len(value)}}
106
+
107
+ if HAS_NUMPY and isinstance(value, np.ndarray):
108
+ return _serialize_ndarray(value)
109
+
110
+ if HAS_NUMPY and isinstance(value, np.generic):
111
+ return serialize(value.item())
112
+
113
+ if HAS_PANDAS and isinstance(value, pd.DataFrame):
114
+ return _serialize_dataframe(value)
115
+
116
+ if HAS_PANDAS and isinstance(value, pd.Series):
117
+ return _serialize_series(value)
118
+
119
+ if isinstance(value, (list, tuple)):
120
+ return _serialize_sequence(value, prefer_msgpack=prefer_msgpack)
121
+
122
+ if isinstance(value, dict):
123
+ return _serialize_mapping(value, prefer_msgpack=prefer_msgpack)
124
+
125
+ if isinstance(value, (datetime.datetime, datetime.date)):
126
+ return _json(json.dumps(value.isoformat()))
127
+
128
+ if isinstance(value, set):
129
+ return _serialize_sequence(list(value), prefer_msgpack=prefer_msgpack)
130
+
131
+ # Fall through — mark as Python ref so Node side wraps in PyProxy
132
+ return _python_ref(id(value))
133
+
134
+
135
+ def deserialize(wire: Dict[str, Any]) -> Any:
136
+ """
137
+ Deserialize a wire-format dict back to a Python value.
138
+ Used when data flows Node → Python.
139
+ """
140
+ fmt = wire.get('format', FORMAT_NONE)
141
+ data = wire.get('data')
142
+ meta = wire.get('metadata') or {}
143
+
144
+ if fmt == FORMAT_NONE or data is None:
145
+ return None
146
+
147
+ if fmt == FORMAT_JSON:
148
+ return json.loads(data) if data else None
149
+
150
+ if fmt == FORMAT_MSGPACK:
151
+ if not HAS_MSGPACK:
152
+ raise RuntimeError('msgpack not installed; cannot deserialize MSGPACK format')
153
+ return msgpack.unpackb(data, raw=False)
154
+
155
+ if fmt == FORMAT_NUMPY:
156
+ return _deserialize_ndarray(data)
157
+
158
+ if fmt == FORMAT_DATAFRAME:
159
+ return _deserialize_dataframe(data)
160
+
161
+ if fmt == FORMAT_SERIES:
162
+ return _deserialize_series(data)
163
+
164
+ if fmt == FORMAT_BYTES:
165
+ return data if isinstance(data, (bytes, bytearray)) else data.encode()
166
+
167
+ return None
168
+
169
+
170
+ # ─────────────────────────────────────────────────────────────────────────────
171
+ # NumPy helpers
172
+ # ─────────────────────────────────────────────────────────────────────────────
173
+
174
+ def _serialize_ndarray(arr: 'np.ndarray') -> Dict[str, Any]:
175
+ """Serialize NumPy array using the binary layout in NumpyBridge.ts."""
176
+ dtype_str = _NUMPY_DTYPE_MAP.get(arr.dtype.name, 'float64')
177
+ dtype_code = _DTYPE_TO_CODE.get(dtype_str, 10)
178
+ item_size = arr.itemsize
179
+ ndim = arr.ndim
180
+ shape = list(arr.shape)
181
+ count = arr.size
182
+
183
+ # Ensure C-contiguous
184
+ if not arr.flags['C_CONTIGUOUS']:
185
+ arr = np.ascontiguousarray(arr)
186
+
187
+ # Build header: 4×uint32 + ndim×uint32
188
+ header = struct.pack(
189
+ f'<4I{ndim}I',
190
+ ndim, dtype_code, item_size, count,
191
+ *shape,
192
+ )
193
+ data = header + arr.tobytes()
194
+
195
+ return {
196
+ 'format': FORMAT_NUMPY,
197
+ 'data': data,
198
+ 'metadata': {
199
+ 'dtype': dtype_str,
200
+ 'shape': shape,
201
+ 'itemSize': item_size,
202
+ 'length': count,
203
+ },
204
+ }
205
+
206
+
207
+ def _deserialize_ndarray(data: bytes) -> Optional['np.ndarray']:
208
+ if not HAS_NUMPY:
209
+ return None
210
+ if len(data) < 16:
211
+ return None
212
+
213
+ ndim, dtype_code, item_size, count = struct.unpack_from('<4I', data, 0)
214
+ shape = struct.unpack_from(f'<{ndim}I', data, 16)
215
+ data_offset = 16 + ndim * 4
216
+
217
+ # Map code → numpy dtype
218
+ code_to_np = {
219
+ 0: 'bool', 1: 'int8', 2: 'int16', 3: 'int32', 4: 'int64',
220
+ 5: 'uint8', 6: 'uint16', 7: 'uint32', 8: 'uint64',
221
+ 9: 'float32', 10: 'float64', 11: 'complex64', 12: 'complex128',
222
+ }
223
+ np_dtype = code_to_np.get(dtype_code, 'float64')
224
+
225
+ raw = data[data_offset: data_offset + count * item_size]
226
+ arr = np.frombuffer(raw, dtype=np.dtype(np_dtype)).reshape(shape)
227
+ return arr.copy()
228
+
229
+
230
+ # ─────────────────────────────────────────────────────────────────────────────
231
+ # Pandas helpers
232
+ # ─────────────────────────────────────────────────────────────────────────────
233
+
234
+ def _serialize_dataframe(df: 'pd.DataFrame') -> Dict[str, Any]:
235
+ """Serialize DataFrame to split-orientation JSON wire format."""
236
+ columns = [str(c) for c in df.columns.tolist()]
237
+ index = df.index.tolist()
238
+ dtypes = {str(k): str(v) for k, v in df.dtypes.to_dict().items()}
239
+ shape = list(df.shape)
240
+
241
+ # Convert rows to lists of native Python types
242
+ data_rows: List[List[Any]] = []
243
+ for _, row in df.iterrows():
244
+ data_rows.append([_to_native(v) for v in row])
245
+
246
+ wire = {
247
+ 'columns': columns,
248
+ 'data': data_rows,
249
+ 'index': [_to_native(i) for i in index],
250
+ 'dtypes': dtypes,
251
+ 'shape': shape,
252
+ }
253
+
254
+ json_str = json.dumps(wire, default=_json_default)
255
+ return {
256
+ 'format': FORMAT_DATAFRAME,
257
+ 'data': json_str,
258
+ 'metadata': {'length': shape[0], 'columns': columns},
259
+ }
260
+
261
+
262
+ def _deserialize_dataframe(data: Union[str, bytes]) -> Optional['pd.DataFrame']:
263
+ if not HAS_PANDAS:
264
+ return None
265
+ text = data.decode() if isinstance(data, (bytes, bytearray)) else data
266
+ wire = json.loads(text)
267
+ columns = wire.get('columns', [])
268
+ rows = wire.get('data', [])
269
+ index = wire.get('index', None)
270
+ df = pd.DataFrame(rows, columns=columns, index=index)
271
+ return df
272
+
273
+
274
+ def _serialize_series(s: 'pd.Series') -> Dict[str, Any]:
275
+ wire = {
276
+ 'name': str(s.name) if s.name is not None else '',
277
+ 'data': [_to_native(v) for v in s.tolist()],
278
+ 'index': [_to_native(i) for i in s.index.tolist()],
279
+ 'dtype': str(s.dtype),
280
+ 'length': len(s),
281
+ }
282
+ json_str = json.dumps(wire, default=_json_default)
283
+ return {
284
+ 'format': FORMAT_SERIES,
285
+ 'data': json_str,
286
+ 'metadata': {'length': len(s)},
287
+ }
288
+
289
+
290
+ def _deserialize_series(data: Union[str, bytes]) -> Optional['pd.Series']:
291
+ if not HAS_PANDAS:
292
+ return None
293
+ text = data.decode() if isinstance(data, (bytes, bytearray)) else data
294
+ wire = json.loads(text)
295
+ s = pd.Series(wire.get('data', []), index=wire.get('index', None), name=wire.get('name'))
296
+ return s
297
+
298
+
299
+ # ─────────────────────────────────────────────────────────────────────────────
300
+ # Generic helpers
301
+ # ─────────────────────────────────────────────────────────────────────────────
302
+
303
+ def _serialize_sequence(seq: Union[list, tuple], *, prefer_msgpack: bool) -> Dict[str, Any]:
304
+ try:
305
+ native = [_to_native(v) for v in seq]
306
+ json_str = json.dumps(native, default=_json_default)
307
+ if len(json_str) <= 65_536 and not prefer_msgpack:
308
+ return _json(json_str)
309
+ if HAS_MSGPACK:
310
+ return {'format': FORMAT_MSGPACK, 'data': msgpack.packb(native, use_bin_type=True), 'metadata': {'length': len(seq)}}
311
+ return _json(json_str)
312
+ except Exception:
313
+ return _none()
314
+
315
+
316
+ def _serialize_mapping(mapping: dict, *, prefer_msgpack: bool) -> Dict[str, Any]:
317
+ try:
318
+ native = {str(k): _to_native(v) for k, v in mapping.items()}
319
+ json_str = json.dumps(native, default=_json_default)
320
+ if len(json_str) <= 65_536 and not prefer_msgpack:
321
+ return _json(json_str)
322
+ if HAS_MSGPACK:
323
+ return {'format': FORMAT_MSGPACK, 'data': msgpack.packb(native, use_bin_type=True), 'metadata': {}}
324
+ return _json(json_str)
325
+ except Exception:
326
+ return _none()
327
+
328
+
329
+ # ─────────────────────────────────────────────────────────────────────────────
330
+ # Utility
331
+ # ─────────────────────────────────────────────────────────────────────────────
332
+
333
+ def _json(text: str) -> Dict[str, Any]:
334
+ return {'format': FORMAT_JSON, 'data': text, 'metadata': {}}
335
+
336
+
337
+ def _none() -> Dict[str, Any]:
338
+ return {'format': FORMAT_NONE, 'data': None, 'metadata': {}}
339
+
340
+
341
+ def _python_ref(obj_id: int) -> Dict[str, Any]:
342
+ return {'format': FORMAT_PYTHON_REF, 'data': None, 'metadata': {'objectId': obj_id}}
343
+
344
+
345
+ def _to_native(value: Any) -> Any:
346
+ """Recursively convert NumPy/Pandas scalars to Python native types."""
347
+ if HAS_NUMPY:
348
+ if isinstance(value, np.integer):
349
+ return int(value)
350
+ if isinstance(value, np.floating):
351
+ return float(value)
352
+ if isinstance(value, np.bool_):
353
+ return bool(value)
354
+ if isinstance(value, np.complexfloating):
355
+ return {'real': float(value.real), 'imag': float(value.imag)}
356
+ if isinstance(value, np.ndarray):
357
+ return value.tolist()
358
+ if HAS_PANDAS:
359
+ if pd.isna(value):
360
+ return None
361
+ if isinstance(value, (datetime.datetime, datetime.date)):
362
+ return value.isoformat()
363
+ return value
364
+
365
+
366
+ def _json_default(obj: Any) -> Any:
367
+ """json.dumps default handler for non-serializable types."""
368
+ if HAS_NUMPY and isinstance(obj, np.integer):
369
+ return int(obj)
370
+ if HAS_NUMPY and isinstance(obj, np.floating):
371
+ return float(obj)
372
+ if HAS_NUMPY and isinstance(obj, np.ndarray):
373
+ return obj.tolist()
374
+ if isinstance(obj, (datetime.datetime, datetime.date)):
375
+ return obj.isoformat()
376
+ if isinstance(obj, bytes):
377
+ return obj.hex()
378
+ raise TypeError(f'Object of type {type(obj).__name__} is not JSON serializable')
379
+
@@ -0,0 +1,288 @@
1
+ """
2
+ nodepyx — type_inspector.py
3
+ Python-side module introspection utility.
4
+
5
+ Called by TypeGenerator via the embedded Python runtime:
6
+ result_json = nodepyx_runtime.inspect_module('pandas')
7
+
8
+ Returns a JSON-serializable dict matching RawModuleInspection in TypeGenerator.ts.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import inspect
14
+ import json
15
+ import importlib
16
+ import sys
17
+ import re
18
+ import types
19
+ import typing
20
+ from typing import Any, Dict, List, Optional, Tuple, get_type_hints
21
+
22
+ # ── helpers ──────────────────────────────────────────────────────────────────
23
+
24
+ _SKIP_NAMES = frozenset({
25
+ '__builtins__', '__cached__', '__loader__', '__spec__',
26
+ '__path__', '__package__', '__initializing__',
27
+ })
28
+
29
+ _SKIP_DUNDER = frozenset({
30
+ '__new__', '__del__', '__class__', '__dict__', '__weakref__',
31
+ '__module__', '__slots__', '__abstractmethods__',
32
+ })
33
+
34
+
35
+ def _get_annotation(annotation: Any) -> str:
36
+ """Convert an annotation object to a string."""
37
+ if annotation is inspect.Parameter.empty:
38
+ return 'Any'
39
+ if annotation is type(None):
40
+ return 'None'
41
+ if isinstance(annotation, str):
42
+ return annotation
43
+ if hasattr(annotation, '__name__'):
44
+ return annotation.__name__
45
+ if hasattr(annotation, '__origin__'):
46
+ # Generic alias (List[str], Dict[str, int], …)
47
+ return _stringify_generic(annotation)
48
+ return str(annotation).replace('typing.', '')
49
+
50
+
51
+ def _stringify_generic(tp: Any) -> str:
52
+ """Stringify a typing generic alias."""
53
+ origin = getattr(tp, '__origin__', None)
54
+ args = getattr(tp, '__args__', ())
55
+
56
+ if origin is None:
57
+ return str(tp)
58
+
59
+ origin_name = getattr(origin, '__name__', None) or str(origin)
60
+
61
+ # typing.Union[X, Y] → 'Union[X, Y]'
62
+ if origin is typing.Union:
63
+ parts = [_get_annotation(a) for a in args]
64
+ return f"Union[{', '.join(parts)}]"
65
+
66
+ # typing.Optional[X] is Union[X, None]
67
+ if len(args) == 2 and type(None) in args:
68
+ non_none = [a for a in args if a is not type(None)][0]
69
+ return f"Optional[{_get_annotation(non_none)}]"
70
+
71
+ if args:
72
+ args_str = ', '.join(_get_annotation(a) for a in args)
73
+ return f"{origin_name}[{args_str}]"
74
+
75
+ return origin_name
76
+
77
+
78
+ def _get_type_hints_safe(obj: Any) -> Dict[str, Any]:
79
+ try:
80
+ return get_type_hints(obj) or {}
81
+ except Exception:
82
+ return {}
83
+
84
+
85
+ def _param_kind_name(kind: inspect.Parameter.kind) -> str:
86
+ return {
87
+ inspect.Parameter.POSITIONAL_ONLY: 'positional_only',
88
+ inspect.Parameter.POSITIONAL_OR_KEYWORD: 'positional_or_keyword',
89
+ inspect.Parameter.VAR_POSITIONAL: 'var_positional',
90
+ inspect.Parameter.KEYWORD_ONLY: 'keyword_only',
91
+ inspect.Parameter.VAR_KEYWORD: 'var_keyword',
92
+ }.get(kind, 'positional_or_keyword')
93
+
94
+
95
+ # ── Inspect functions / methods ──────────────────────────────────────────────
96
+
97
+ def _inspect_callable(obj: Any, name: str) -> Dict[str, Any]:
98
+ result: Dict[str, Any] = {
99
+ 'name': name,
100
+ 'qual_name': getattr(obj, '__qualname__', name),
101
+ 'docstring': inspect.getdoc(obj) or '',
102
+ 'parameters': [],
103
+ 'return_type': 'Any',
104
+ 'is_method': inspect.ismethod(obj),
105
+ 'is_class_method': isinstance(
106
+ inspect.getattr_static(obj, '__func__', None) or obj,
107
+ classmethod),
108
+ 'is_static_method': isinstance(obj, staticmethod),
109
+ 'is_coroutine': inspect.iscoroutinefunction(obj),
110
+ 'is_generator': inspect.isgeneratorfunction(obj),
111
+ }
112
+
113
+ try:
114
+ sig = inspect.signature(obj)
115
+ hints = _get_type_hints_safe(obj)
116
+
117
+ params = []
118
+ for pname, param in sig.parameters.items():
119
+ annotation = hints.get(pname) or param.annotation
120
+ ann_str = _get_annotation(annotation)
121
+ has_def = param.default is not inspect.Parameter.empty
122
+ params.append({
123
+ 'name': pname,
124
+ 'type': ann_str,
125
+ 'annotation': ann_str,
126
+ 'optional': has_def or param.kind in (
127
+ inspect.Parameter.VAR_POSITIONAL,
128
+ inspect.Parameter.VAR_KEYWORD,
129
+ ),
130
+ 'has_default': has_def,
131
+ 'default': repr(param.default) if has_def else None,
132
+ 'kind': _param_kind_name(param.kind),
133
+ })
134
+
135
+ result['parameters'] = params
136
+
137
+ ret_ann = hints.get('return') or sig.return_annotation
138
+ result['return_type'] = _get_annotation(ret_ann)
139
+
140
+ except (ValueError, TypeError):
141
+ pass
142
+
143
+ return result
144
+
145
+
146
+ # ── Inspect classes ──────────────────────────────────────────────────────────
147
+
148
+ def _inspect_class(cls: type) -> Dict[str, Any]:
149
+ result: Dict[str, Any] = {
150
+ 'name': cls.__name__,
151
+ 'qual_name': getattr(cls, '__qualname__', cls.__name__),
152
+ 'docstring': inspect.getdoc(cls) or '',
153
+ 'bases': [b.__name__ for b in cls.__bases__ if b is not object],
154
+ 'members': [],
155
+ 'methods': [],
156
+ 'properties': [],
157
+ 'class_methods': [],
158
+ 'static_methods': [],
159
+ }
160
+
161
+ hints = _get_type_hints_safe(cls)
162
+
163
+ for mname in dir(cls):
164
+ if mname in _SKIP_DUNDER:
165
+ continue
166
+ if mname.startswith('__') and mname.endswith('__'):
167
+ if mname not in ('__init__', '__len__', '__str__', '__repr__',
168
+ '__iter__', '__getitem__', '__setitem__',
169
+ '__contains__', '__enter__', '__exit__'):
170
+ continue
171
+
172
+ try:
173
+ raw_attr = inspect.getattr_static(cls, mname)
174
+ except AttributeError:
175
+ continue
176
+
177
+ if isinstance(raw_attr, staticmethod):
178
+ inner = raw_attr.__func__
179
+ result['static_methods'].append(_inspect_callable(inner, mname))
180
+
181
+ elif isinstance(raw_attr, classmethod):
182
+ inner = raw_attr.__func__
183
+ r = _inspect_callable(inner, mname)
184
+ r['is_class_method'] = True
185
+ result['class_methods'].append(r)
186
+
187
+ elif isinstance(raw_attr, property):
188
+ ann = hints.get(mname) or raw_attr.fget and getattr(raw_attr.fget, '__annotations__', {}).get('return', 'Any')
189
+ result['properties'].append({
190
+ 'name': mname,
191
+ 'type': _get_annotation(ann) if ann else 'Any',
192
+ 'read_only': raw_attr.fset is None,
193
+ 'docstring': inspect.getdoc(raw_attr) or '',
194
+ })
195
+
196
+ elif callable(raw_attr) or inspect.isfunction(raw_attr):
197
+ result['methods'].append(_inspect_callable(raw_attr, mname))
198
+
199
+ else:
200
+ ann = hints.get(mname, 'Any')
201
+ result['members'].append({
202
+ 'name': mname,
203
+ 'type': _get_annotation(ann),
204
+ 'read_only': False,
205
+ 'docstring': '',
206
+ })
207
+
208
+ return result
209
+
210
+
211
+ # ── Main entry point ─────────────────────────────────────────────────────────
212
+
213
+ def inspect_module(module_name: str) -> Dict[str, Any]:
214
+ """
215
+ Inspect a Python module and return a dict matching RawModuleInspection.
216
+ """
217
+ result: Dict[str, Any] = {
218
+ 'name': module_name,
219
+ 'file': None,
220
+ 'docstring': '',
221
+ 'version': None,
222
+ 'functions': [],
223
+ 'classes': [],
224
+ 'constants': [],
225
+ 'submodules': [],
226
+ }
227
+
228
+ try:
229
+ module = importlib.import_module(module_name)
230
+ except ImportError as exc:
231
+ result['error'] = f'ImportError: {exc}'
232
+ return result
233
+ except Exception as exc:
234
+ result['error'] = f'{type(exc).__name__}: {exc}'
235
+ return result
236
+
237
+ result['file'] = getattr(module, '__file__', None)
238
+ result['docstring'] = inspect.getdoc(module) or ''
239
+ result['version'] = getattr(module, '__version__', None) or getattr(module, 'VERSION', None)
240
+
241
+ for name in dir(module):
242
+ if name in _SKIP_NAMES:
243
+ continue
244
+ if name.startswith('__'):
245
+ continue
246
+
247
+ try:
248
+ obj = getattr(module, name)
249
+ except AttributeError:
250
+ continue
251
+
252
+ # ── Sub-module ──────────────────────────────────────────────────
253
+ if isinstance(obj, types.ModuleType):
254
+ result['submodules'].append(name)
255
+
256
+ # ── Class ───────────────────────────────────────────────────────
257
+ elif inspect.isclass(obj):
258
+ result['classes'].append(_inspect_class(obj))
259
+
260
+ # ── Function / builtin ──────────────────────────────────────────
261
+ elif callable(obj) or inspect.isbuiltin(obj):
262
+ result['functions'].append(_inspect_callable(obj, name))
263
+
264
+ # ── Constant / value ────────────────────────────────────────────
265
+ else:
266
+ ann_type = type(obj).__name__
267
+ result['constants'].append({
268
+ 'name': name,
269
+ 'type': ann_type,
270
+ 'read_only': True,
271
+ 'docstring': '',
272
+ })
273
+
274
+ return result
275
+
276
+
277
+ def inspect_module_json(module_name: str) -> str:
278
+ """Return inspect_module result as a JSON string."""
279
+ return json.dumps(inspect_module(module_name), default=str)
280
+
281
+
282
+ # ── Direct execution for debugging ───────────────────────────────────────────
283
+
284
+ if __name__ == '__main__':
285
+ import sys as _sys
286
+ mod = _sys.argv[1] if len(_sys.argv) > 1 else 'os'
287
+ print(inspect_module_json(mod))
288
+