nodepyx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +399 -0
- package/binding.gyp +73 -0
- package/dist/core/PyCallable.d.ts +65 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +109 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +228 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +55 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +172 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +65 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +483 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +105 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +438 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/CondaManager.d.ts +118 -0
- package/dist/env/CondaManager.d.ts.map +1 -0
- package/dist/env/CondaManager.js +401 -0
- package/dist/env/CondaManager.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +233 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +609 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +103 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +381 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/VenvManager.d.ts +117 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +331 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/PythonTypeMapper.d.ts +87 -0
- package/dist/types/PythonTypeMapper.d.ts.map +1 -0
- package/dist/types/PythonTypeMapper.js +449 -0
- package/dist/types/PythonTypeMapper.js.map +1 -0
- package/dist/types/StubCache.d.ts +109 -0
- package/dist/types/StubCache.d.ts.map +1 -0
- package/dist/types/StubCache.js +333 -0
- package/dist/types/StubCache.js.map +1 -0
- package/dist/types/TypeGenerator.d.ts +139 -0
- package/dist/types/TypeGenerator.d.ts.map +1 -0
- package/dist/types/TypeGenerator.js +372 -0
- package/dist/types/TypeGenerator.js.map +1 -0
- package/dist/types/addon.d.ts +114 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +175 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +44 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +177 -0
- package/python/error_handler.py +433 -0
- package/python/nodepyx_runtime.py +575 -0
- package/python/serializer.py +379 -0
- package/python/type_inspector.py +288 -0
- package/scripts/build-native.js +68 -0
- package/scripts/download-prebuilds.js +99 -0
- package/scripts/generate-stubs.js +405 -0
- package/scripts/install.js +260 -0
- package/src/core/PyCallable.ts +137 -0
- package/src/core/PyContext.ts +296 -0
- package/src/core/PyIterator.ts +294 -0
- package/src/core/PyModule.ts +194 -0
- package/src/core/PyProxy.ts +605 -0
- package/src/core/PyRuntime.ts +504 -0
- package/src/env/CondaManager.ts +451 -0
- package/src/env/PackageInstaller.ts +738 -0
- package/src/env/PythonDetector.ts +414 -0
- package/src/env/VenvManager.ts +396 -0
- package/src/index.ts +425 -0
- package/src/native/gil_guard.cpp +26 -0
- package/src/native/gil_guard.h +175 -0
- package/src/native/nodepyx_addon.cpp +886 -0
- package/src/native/python_bridge.cpp +790 -0
- package/src/native/python_bridge.h +257 -0
- package/src/native/thread_pool.cpp +336 -0
- package/src/native/thread_pool.h +175 -0
- package/src/native/type_converter.cpp +901 -0
- package/src/native/type_converter.h +272 -0
- package/src/nextjs/PyProvider.tsx +123 -0
- package/src/nextjs/index.ts +21 -0
- package/src/nextjs/usePython.ts +106 -0
- package/src/nextjs/withnodepyx.ts +88 -0
- package/src/plugins/Plugin.interface.ts +51 -0
- package/src/plugins/PluginManager.ts +155 -0
- package/src/plugins/builtin/NumpyPlugin.ts +36 -0
- package/src/plugins/builtin/PandasPlugin.ts +49 -0
- package/src/plugins/builtin/TorchPlugin.ts +56 -0
- package/src/plugins/index.ts +7 -0
- package/src/serialization/DataFrameBridge.ts +398 -0
- package/src/serialization/MsgPackSerializer.ts +220 -0
- package/src/serialization/NumpyBridge.ts +332 -0
- package/src/serialization/Serializer.ts +320 -0
- package/src/types/PythonTypeMapper.ts +495 -0
- package/src/types/StubCache.ts +340 -0
- package/src/types/TypeGenerator.ts +491 -0
- package/src/types/addon.ts +170 -0
- package/src/types/config.ts +226 -0
- package/src/types/index.ts +55 -0
- package/src/types/python.ts +309 -0
- package/src/types/stubs/numpy.d.ts +441 -0
- package/src/types/stubs/pandas.d.ts +575 -0
- package/src/types/stubs/sklearn.d.ts +728 -0
- package/src/types/stubs/torch.d.ts +694 -0
- package/src/utils/ErrorTranslator.ts +220 -0
- package/src/utils/Logger.ts +119 -0
- package/src/utils/MemoryMonitor.ts +175 -0
|
@@ -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
|
+
|