tywrap 0.1.1 → 0.1.2
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/README.md +23 -3
- package/dist/cli.js +70 -3
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +47 -11
- package/dist/config/index.js.map +1 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +6 -2
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +16 -4
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/bridge-core.d.ts +64 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -0
- package/dist/runtime/bridge-core.js +344 -0
- package/dist/runtime/bridge-core.js.map +1 -0
- package/dist/runtime/node.d.ts +5 -6
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +94 -190
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +4 -7
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +117 -194
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +20 -0
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -0
- package/dist/runtime/timed-out-request-tracker.js +48 -0
- package/dist/runtime/timed-out-request-tracker.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +17 -4
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +68 -35
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/codec.d.ts +22 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +245 -55
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +1 -0
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/python.d.ts.map +1 -1
- package/dist/utils/python.js.map +1 -1
- package/package.json +11 -2
- package/runtime/python_bridge.py +450 -69
- package/src/cli.ts +75 -3
- package/src/config/index.ts +58 -15
- package/src/core/generator.ts +6 -2
- package/src/core/mapper.ts +16 -4
- package/src/index.ts +2 -1
- package/src/runtime/bridge-core.ts +454 -0
- package/src/runtime/node.ts +116 -220
- package/src/runtime/optimized-node.ts +160 -243
- package/src/runtime/timed-out-request-tracker.ts +58 -0
- package/src/types/index.ts +3 -0
- package/src/tywrap.ts +91 -36
- package/src/utils/codec.ts +299 -82
- package/src/utils/logger.ts +1 -0
- package/src/utils/python.ts +0 -1
package/src/tywrap.ts
CHANGED
|
@@ -72,16 +72,50 @@ expectType<Record<string, unknown>>(mod);
|
|
|
72
72
|
await writeFile(filePath, content, 'utf-8');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export interface GenerateRunOptions {
|
|
76
|
+
/**
|
|
77
|
+
* If true, do not write files; instead compare generated output to what's on disk.
|
|
78
|
+
* Intended for CI to ensure wrappers are checked in and up to date.
|
|
79
|
+
*/
|
|
80
|
+
check?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface GenerateResult {
|
|
84
|
+
written: string[];
|
|
85
|
+
warnings: string[];
|
|
86
|
+
/**
|
|
87
|
+
* Only set when `GenerateRunOptions.check === true`.
|
|
88
|
+
* Lists files that are missing or differ from what would be generated.
|
|
89
|
+
*/
|
|
90
|
+
outOfDate?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeForComparison(text: string): string {
|
|
94
|
+
// Avoid false positives from CRLF vs LF (e.g. Windows checkouts)
|
|
95
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function safeReadFile(path: string): Promise<string | null> {
|
|
99
|
+
try {
|
|
100
|
+
return await fsUtils.readFile(path);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
/**
|
|
76
107
|
* Generate TypeScript wrappers for configured Python modules.
|
|
77
108
|
* Minimal MVP implementation: resolves module file, analyzes, generates TS, writes to output.dir
|
|
78
109
|
*/
|
|
79
110
|
export async function generate(
|
|
80
|
-
options: Partial<TywrapOptions
|
|
81
|
-
|
|
111
|
+
options: Partial<TywrapOptions>,
|
|
112
|
+
runOptions: GenerateRunOptions = {}
|
|
113
|
+
): Promise<GenerateResult> {
|
|
114
|
+
const checkMode = runOptions.check === true;
|
|
82
115
|
const resolvedOptions = createConfig(options);
|
|
83
116
|
const instance = await tywrap(resolvedOptions);
|
|
84
117
|
const written: string[] = [];
|
|
118
|
+
const outOfDate: string[] = [];
|
|
85
119
|
const warnings: string[] = [];
|
|
86
120
|
const outputDir = resolvedOptions.output.dir;
|
|
87
121
|
const caching = resolvedOptions.performance.caching;
|
|
@@ -92,11 +126,13 @@ export async function generate(
|
|
|
92
126
|
});
|
|
93
127
|
|
|
94
128
|
// Ensure directory exists (Node-only best-effort)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
if (!checkMode) {
|
|
130
|
+
try {
|
|
131
|
+
const modFs = await import('fs/promises');
|
|
132
|
+
await modFs.mkdir(outputDir, { recursive: true });
|
|
133
|
+
} catch {
|
|
134
|
+
// ignore in non-node or if already exists
|
|
135
|
+
}
|
|
100
136
|
}
|
|
101
137
|
|
|
102
138
|
const modules = resolvedOptions.pythonModules ?? {};
|
|
@@ -109,10 +145,6 @@ export async function generate(
|
|
|
109
145
|
let ir: unknown | null = null;
|
|
110
146
|
let irError: string | undefined;
|
|
111
147
|
if (caching && fsUtils.isAvailable()) {
|
|
112
|
-
try {
|
|
113
|
-
const modFs = await import('fs/promises');
|
|
114
|
-
await modFs.mkdir(cacheDir, { recursive: true });
|
|
115
|
-
} catch {}
|
|
116
148
|
try {
|
|
117
149
|
const cached = await fsUtils.readFile(pathUtils.join(cacheDir, cacheKey));
|
|
118
150
|
ir = JSON.parse(cached);
|
|
@@ -124,7 +156,7 @@ export async function generate(
|
|
|
124
156
|
const fetchResult = await fetchPythonIr(moduleKey, pythonPath);
|
|
125
157
|
ir = fetchResult.ir;
|
|
126
158
|
irError = fetchResult.error;
|
|
127
|
-
if (ir && caching && fsUtils.isAvailable()) {
|
|
159
|
+
if (ir && caching && fsUtils.isAvailable() && !checkMode) {
|
|
128
160
|
try {
|
|
129
161
|
await fsUtils.writeFile(pathUtils.join(cacheDir, cacheKey), JSON.stringify(ir));
|
|
130
162
|
} catch {}
|
|
@@ -141,26 +173,43 @@ export async function generate(
|
|
|
141
173
|
const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
|
|
142
174
|
const gen = instance.generator.generateModuleDefinition(moduleModel, annotatedJSDoc);
|
|
143
175
|
|
|
144
|
-
// Write file
|
|
145
176
|
const baseName = moduleModel.name || 'module';
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
const filesToEmit: Array<{ path: string; content: string }> = [
|
|
178
|
+
{ path: pathUtils.join(outputDir, `${baseName}.generated.ts`), content: gen.typescript },
|
|
179
|
+
];
|
|
149
180
|
|
|
150
181
|
// Optional .d.ts emission (header-only declarations mirroring exports)
|
|
151
182
|
if (resolvedOptions.output?.declaration) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
filesToEmit.push({
|
|
184
|
+
path: pathUtils.join(outputDir, `${baseName}.generated.d.ts`),
|
|
185
|
+
content: renderDts(gen.typescript),
|
|
186
|
+
});
|
|
156
187
|
}
|
|
157
188
|
|
|
158
189
|
// Optional source map emission (placeholder mapping for now)
|
|
159
190
|
if (resolvedOptions.output?.sourceMap) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
191
|
+
filesToEmit.push({
|
|
192
|
+
path: pathUtils.join(outputDir, `${baseName}.generated.ts.map`),
|
|
193
|
+
content: renderSourceMapPlaceholder(moduleModel.name),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (checkMode) {
|
|
198
|
+
for (const file of filesToEmit) {
|
|
199
|
+
const existing = await safeReadFile(file.path);
|
|
200
|
+
if (existing === null) {
|
|
201
|
+
outOfDate.push(file.path);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (normalizeForComparison(existing) !== normalizeForComparison(file.content)) {
|
|
205
|
+
outOfDate.push(file.path);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
for (const file of filesToEmit) {
|
|
210
|
+
await fsUtils.writeFile(file.path, file.content);
|
|
211
|
+
written.push(file.path);
|
|
212
|
+
}
|
|
164
213
|
}
|
|
165
214
|
|
|
166
215
|
// Emit warning summary of unknown typing constructs (best-effort)
|
|
@@ -174,22 +223,28 @@ export async function generate(
|
|
|
174
223
|
`Module ${baseName}: unknown typing constructs encountered: ${unkList}${entries.length > 25 ? '…' : ''}`
|
|
175
224
|
);
|
|
176
225
|
// Write JSON report
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
226
|
+
if (!checkMode) {
|
|
227
|
+
try {
|
|
228
|
+
const reportsDir = pathUtils.join('.tywrap', 'reports');
|
|
229
|
+
await fsUtils.writeFile(
|
|
230
|
+
pathUtils.join(reportsDir, `${baseName}.json`),
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
module: baseName,
|
|
233
|
+
unknowns: Object.fromEntries(entries),
|
|
234
|
+
generatedAt: new Date().toISOString(),
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
} catch {
|
|
238
|
+
// ignore
|
|
239
|
+
}
|
|
189
240
|
}
|
|
190
241
|
}
|
|
191
242
|
}
|
|
192
243
|
|
|
244
|
+
if (checkMode) {
|
|
245
|
+
return { written: [], warnings, outOfDate };
|
|
246
|
+
}
|
|
247
|
+
|
|
193
248
|
return { written, warnings };
|
|
194
249
|
}
|
|
195
250
|
|
package/src/utils/codec.ts
CHANGED
|
@@ -43,11 +43,19 @@ export interface SklearnEstimator {
|
|
|
43
43
|
export type CodecEnvelope =
|
|
44
44
|
| {
|
|
45
45
|
readonly __tywrap__: 'dataframe';
|
|
46
|
+
readonly codecVersion?: number;
|
|
46
47
|
readonly encoding: 'arrow';
|
|
47
48
|
readonly b64: string;
|
|
48
49
|
}
|
|
50
|
+
| {
|
|
51
|
+
readonly __tywrap__: 'dataframe';
|
|
52
|
+
readonly codecVersion?: number;
|
|
53
|
+
readonly encoding: 'json';
|
|
54
|
+
readonly data: unknown;
|
|
55
|
+
}
|
|
49
56
|
| {
|
|
50
57
|
readonly __tywrap__: 'series';
|
|
58
|
+
readonly codecVersion?: number;
|
|
51
59
|
readonly encoding: 'arrow' | 'json';
|
|
52
60
|
readonly b64?: string;
|
|
53
61
|
readonly data?: unknown;
|
|
@@ -55,6 +63,7 @@ export type CodecEnvelope =
|
|
|
55
63
|
}
|
|
56
64
|
| {
|
|
57
65
|
readonly __tywrap__: 'ndarray';
|
|
66
|
+
readonly codecVersion?: number;
|
|
58
67
|
readonly encoding: 'arrow' | 'json';
|
|
59
68
|
readonly b64?: string; // when encoding=arrow
|
|
60
69
|
readonly data?: unknown; // when encoding=json
|
|
@@ -62,6 +71,7 @@ export type CodecEnvelope =
|
|
|
62
71
|
}
|
|
63
72
|
| {
|
|
64
73
|
readonly __tywrap__: 'scipy.sparse';
|
|
74
|
+
readonly codecVersion?: number;
|
|
65
75
|
readonly encoding: 'json';
|
|
66
76
|
readonly format: 'csr' | 'csc' | 'coo';
|
|
67
77
|
readonly shape: readonly number[];
|
|
@@ -74,6 +84,7 @@ export type CodecEnvelope =
|
|
|
74
84
|
}
|
|
75
85
|
| {
|
|
76
86
|
readonly __tywrap__: 'torch.tensor';
|
|
87
|
+
readonly codecVersion?: number;
|
|
77
88
|
readonly encoding: 'ndarray';
|
|
78
89
|
readonly value: unknown;
|
|
79
90
|
readonly shape?: readonly number[];
|
|
@@ -82,6 +93,7 @@ export type CodecEnvelope =
|
|
|
82
93
|
}
|
|
83
94
|
| {
|
|
84
95
|
readonly __tywrap__: 'sklearn.estimator';
|
|
96
|
+
readonly codecVersion?: number;
|
|
85
97
|
readonly encoding: 'json';
|
|
86
98
|
readonly className: string;
|
|
87
99
|
readonly module: string;
|
|
@@ -116,10 +128,79 @@ export function hasArrowDecoder(): boolean {
|
|
|
116
128
|
return typeof arrowTableFrom === 'function';
|
|
117
129
|
}
|
|
118
130
|
|
|
131
|
+
type ArrowModuleLoader = () => unknown | Promise<unknown>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Detect Node.js runtime capabilities without hard dependencies.
|
|
135
|
+
*
|
|
136
|
+
* Why: keep browser/bundler builds safe while still enabling Node-only paths.
|
|
137
|
+
*/
|
|
138
|
+
function isNodeRuntime(): boolean {
|
|
139
|
+
return (
|
|
140
|
+
typeof process !== 'undefined' &&
|
|
141
|
+
typeof (process as { versions?: { node?: string } }).versions?.node === 'string'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate the Arrow module shape and register its IPC decoder.
|
|
147
|
+
*
|
|
148
|
+
* Why: centralize tableFromIPC checks so callers get consistent errors and can
|
|
149
|
+
* rely on a single registration path.
|
|
150
|
+
*/
|
|
151
|
+
function registerArrowDecoderFromModule(module: { tableFromIPC?: unknown }): void {
|
|
152
|
+
const tableFromIPC = module.tableFromIPC;
|
|
153
|
+
if (typeof tableFromIPC !== 'function') {
|
|
154
|
+
throw new Error('apache-arrow does not export tableFromIPC');
|
|
155
|
+
}
|
|
156
|
+
registerArrowDecoder((bytes: Uint8Array) => tableFromIPC(bytes));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Attempt to lazily register an Arrow decoder at runtime.
|
|
161
|
+
*
|
|
162
|
+
* Why: keep apache-arrow optional while letting NodeBridge (or callers) enable
|
|
163
|
+
* Arrow decoding when the module is present.
|
|
164
|
+
*/
|
|
165
|
+
export async function autoRegisterArrowDecoder(
|
|
166
|
+
options: { loader?: ArrowModuleLoader } = {}
|
|
167
|
+
): Promise<boolean> {
|
|
168
|
+
if (hasArrowDecoder()) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
const loader: ArrowModuleLoader | undefined =
|
|
172
|
+
options.loader ??
|
|
173
|
+
(isNodeRuntime()
|
|
174
|
+
? async (): Promise<unknown> => {
|
|
175
|
+
try {
|
|
176
|
+
const nodeModule = await import('node:module');
|
|
177
|
+
const require = nodeModule.createRequire(import.meta.url);
|
|
178
|
+
return require('apache-arrow') as unknown;
|
|
179
|
+
} catch {
|
|
180
|
+
return await import('apache-arrow');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
: undefined);
|
|
184
|
+
if (!loader) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const arrowModule = await loader();
|
|
189
|
+
registerArrowDecoderFromModule(arrowModule as { tableFromIPC?: unknown });
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
119
196
|
function isObject(value: unknown): value is { [k: string]: unknown } {
|
|
120
197
|
return typeof value === 'object' && value !== null;
|
|
121
198
|
}
|
|
122
199
|
|
|
200
|
+
function isNumberArray(value: unknown): value is number[] {
|
|
201
|
+
return Array.isArray(value) && value.every(item => typeof item === 'number');
|
|
202
|
+
}
|
|
203
|
+
|
|
123
204
|
function fromBase64(b64: string): Uint8Array {
|
|
124
205
|
if (typeof Buffer !== 'undefined') {
|
|
125
206
|
const buf = Buffer.from(b64, 'base64');
|
|
@@ -151,118 +232,254 @@ async function tryDecodeArrowTable(bytes: Uint8Array): Promise<ArrowTable | Uint
|
|
|
151
232
|
}
|
|
152
233
|
}
|
|
153
234
|
|
|
154
|
-
|
|
235
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
236
|
+
|
|
237
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
238
|
+
return (
|
|
239
|
+
typeof value === 'object' &&
|
|
240
|
+
value !== null &&
|
|
241
|
+
'then' in value &&
|
|
242
|
+
typeof (value as { then?: unknown }).then === 'function'
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Why: decoding needs to reject incompatible envelopes before we attempt to interpret payloads.
|
|
247
|
+
const CODEC_VERSION = 1;
|
|
248
|
+
|
|
249
|
+
function assertCodecVersion(envelope: { codecVersion?: unknown }, marker: string): void {
|
|
250
|
+
if (!('codecVersion' in envelope)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const version = envelope.codecVersion;
|
|
254
|
+
if (version === undefined) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (typeof version !== 'number' || !Number.isFinite(version)) {
|
|
258
|
+
throw new Error(`Invalid ${marker} envelope: codecVersion must be a number`);
|
|
259
|
+
}
|
|
260
|
+
if (version !== CODEC_VERSION) {
|
|
261
|
+
throw new Error(`Unsupported ${marker} envelope codecVersion: ${version}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function decodeEnvelopeCore<T>(
|
|
266
|
+
value: unknown,
|
|
267
|
+
decodeArrow: (bytes: Uint8Array) => MaybePromise<T>,
|
|
268
|
+
recurse: (value: unknown) => MaybePromise<T | unknown>
|
|
269
|
+
): MaybePromise<T | unknown> {
|
|
155
270
|
if (!isObject(value)) {
|
|
156
271
|
return value;
|
|
157
272
|
}
|
|
158
273
|
const marker = (value as { __tywrap__?: unknown }).__tywrap__;
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
(value as { encoding?: unknown }).encoding === 'arrow' &&
|
|
162
|
-
typeof (value as { b64?: unknown }).b64 === 'string'
|
|
163
|
-
) {
|
|
164
|
-
const bytes = fromBase64(String((value as { b64: string }).b64));
|
|
165
|
-
return decodeArrow(bytes);
|
|
274
|
+
if (typeof marker !== 'string') {
|
|
275
|
+
return value as unknown;
|
|
166
276
|
}
|
|
277
|
+
|
|
167
278
|
if (
|
|
168
|
-
marker === 'dataframe'
|
|
169
|
-
|
|
170
|
-
'
|
|
279
|
+
marker === 'dataframe' ||
|
|
280
|
+
marker === 'series' ||
|
|
281
|
+
marker === 'ndarray' ||
|
|
282
|
+
marker === 'scipy.sparse' ||
|
|
283
|
+
marker === 'torch.tensor' ||
|
|
284
|
+
marker === 'sklearn.estimator'
|
|
171
285
|
) {
|
|
172
|
-
|
|
286
|
+
assertCodecVersion(value as { codecVersion?: unknown }, marker);
|
|
173
287
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
(value as { encoding?: unknown }).encoding
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
288
|
+
|
|
289
|
+
if (marker === 'dataframe') {
|
|
290
|
+
const encoding = (value as { encoding?: unknown }).encoding;
|
|
291
|
+
if (encoding === 'arrow') {
|
|
292
|
+
const b64 = (value as { b64?: unknown }).b64;
|
|
293
|
+
if (typeof b64 !== 'string') {
|
|
294
|
+
throw new Error('Invalid dataframe envelope: missing b64');
|
|
295
|
+
}
|
|
296
|
+
const bytes = fromBase64(b64);
|
|
297
|
+
return decodeArrow(bytes);
|
|
298
|
+
}
|
|
299
|
+
if (encoding === 'json') {
|
|
300
|
+
if (!('data' in (value as object))) {
|
|
301
|
+
throw new Error('Invalid dataframe envelope: missing data');
|
|
302
|
+
}
|
|
303
|
+
return (value as { data: unknown }).data;
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`Invalid dataframe envelope: unsupported encoding ${String(encoding)}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (marker === 'series') {
|
|
309
|
+
const encoding = (value as { encoding?: unknown }).encoding;
|
|
310
|
+
if (encoding === 'arrow') {
|
|
311
|
+
const b64 = (value as { b64?: unknown }).b64;
|
|
312
|
+
if (typeof b64 !== 'string') {
|
|
313
|
+
throw new Error('Invalid series envelope: missing b64');
|
|
314
|
+
}
|
|
315
|
+
const bytes = fromBase64(b64);
|
|
316
|
+
return decodeArrow(bytes);
|
|
317
|
+
}
|
|
318
|
+
if (encoding === 'json') {
|
|
319
|
+
if (!('data' in (value as object))) {
|
|
320
|
+
throw new Error('Invalid series envelope: missing data');
|
|
321
|
+
}
|
|
322
|
+
return (value as { data: unknown }).data;
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`Invalid series envelope: unsupported encoding ${String(encoding)}`);
|
|
180
325
|
}
|
|
326
|
+
|
|
181
327
|
if (marker === 'ndarray') {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
328
|
+
const encoding = (value as { encoding?: unknown }).encoding;
|
|
329
|
+
if (encoding === 'arrow') {
|
|
330
|
+
const b64 = (value as { b64?: unknown }).b64;
|
|
331
|
+
if (typeof b64 !== 'string') {
|
|
332
|
+
throw new Error('Invalid ndarray envelope: missing b64');
|
|
333
|
+
}
|
|
334
|
+
const bytes = fromBase64(b64);
|
|
187
335
|
return decodeArrow(bytes);
|
|
188
336
|
}
|
|
189
|
-
if (
|
|
337
|
+
if (encoding === 'json') {
|
|
338
|
+
if (!('data' in (value as object))) {
|
|
339
|
+
throw new Error('Invalid ndarray envelope: missing data');
|
|
340
|
+
}
|
|
190
341
|
return (value as { data: unknown }).data;
|
|
191
342
|
}
|
|
343
|
+
throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
|
|
192
344
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
(value as { encoding?: unknown }).encoding
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
345
|
+
|
|
346
|
+
if (marker === 'scipy.sparse') {
|
|
347
|
+
const encoding = (value as { encoding?: unknown }).encoding;
|
|
348
|
+
if (encoding !== 'json') {
|
|
349
|
+
throw new Error(`Invalid scipy.sparse envelope: unsupported encoding ${String(encoding)}`);
|
|
350
|
+
}
|
|
351
|
+
const format = (value as { format?: unknown }).format;
|
|
352
|
+
if (format !== 'csr' && format !== 'csc' && format !== 'coo') {
|
|
353
|
+
throw new Error(`Invalid scipy.sparse envelope: unsupported format ${String(format)}`);
|
|
354
|
+
}
|
|
355
|
+
const shape = (value as { shape?: unknown }).shape;
|
|
356
|
+
if (
|
|
357
|
+
!Array.isArray(shape) ||
|
|
358
|
+
shape.length !== 2 ||
|
|
359
|
+
typeof shape[0] !== 'number' ||
|
|
360
|
+
typeof shape[1] !== 'number'
|
|
361
|
+
) {
|
|
362
|
+
throw new Error('Invalid scipy.sparse envelope: shape must be a 2-item number[]');
|
|
363
|
+
}
|
|
364
|
+
const data = (value as { data?: unknown }).data;
|
|
365
|
+
if (!Array.isArray(data)) {
|
|
366
|
+
throw new Error('Invalid scipy.sparse envelope: data must be an array');
|
|
367
|
+
}
|
|
368
|
+
const dtypeValue = (value as { dtype?: unknown }).dtype;
|
|
369
|
+
const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
|
|
370
|
+
|
|
371
|
+
if (format === 'coo') {
|
|
372
|
+
const row = (value as { row?: unknown }).row;
|
|
373
|
+
const col = (value as { col?: unknown }).col;
|
|
374
|
+
if (!Array.isArray(row) || !Array.isArray(col)) {
|
|
375
|
+
throw new Error('Invalid scipy.sparse envelope: coo requires row and col arrays');
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
format,
|
|
379
|
+
shape,
|
|
380
|
+
data,
|
|
381
|
+
row,
|
|
382
|
+
col,
|
|
383
|
+
dtype,
|
|
384
|
+
} satisfies SparseMatrix;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const indices = (value as { indices?: unknown }).indices;
|
|
388
|
+
const indptr = (value as { indptr?: unknown }).indptr;
|
|
389
|
+
if (!Array.isArray(indices) || !Array.isArray(indptr)) {
|
|
390
|
+
throw new Error('Invalid scipy.sparse envelope: csr/csc requires indices and indptr arrays');
|
|
391
|
+
}
|
|
210
392
|
return {
|
|
211
|
-
format
|
|
212
|
-
shape
|
|
213
|
-
data
|
|
214
|
-
indices
|
|
215
|
-
indptr
|
|
216
|
-
|
|
217
|
-
col: sparse.col,
|
|
218
|
-
dtype: sparse.dtype,
|
|
393
|
+
format,
|
|
394
|
+
shape,
|
|
395
|
+
data,
|
|
396
|
+
indices,
|
|
397
|
+
indptr,
|
|
398
|
+
dtype,
|
|
219
399
|
} satisfies SparseMatrix;
|
|
220
400
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const decoded = decodeEnvelope(torchValue.value, decodeArrow);
|
|
230
|
-
return {
|
|
231
|
-
data: decoded,
|
|
232
|
-
shape: torchValue.shape,
|
|
233
|
-
dtype: torchValue.dtype,
|
|
234
|
-
device: torchValue.device,
|
|
235
|
-
} satisfies TorchTensor;
|
|
401
|
+
|
|
402
|
+
if (marker === 'torch.tensor') {
|
|
403
|
+
const encoding = (value as { encoding?: unknown }).encoding;
|
|
404
|
+
if (encoding !== 'ndarray') {
|
|
405
|
+
throw new Error(`Invalid torch.tensor envelope: unsupported encoding ${String(encoding)}`);
|
|
406
|
+
}
|
|
407
|
+
if (!('value' in (value as object))) {
|
|
408
|
+
throw new Error('Invalid torch.tensor envelope: missing value');
|
|
236
409
|
}
|
|
410
|
+
const nested = (value as { value: unknown }).value;
|
|
411
|
+
if (!isObject(nested) || (nested as { __tywrap__?: unknown }).__tywrap__ !== 'ndarray') {
|
|
412
|
+
throw new Error('Invalid torch.tensor envelope: value must be an ndarray envelope');
|
|
413
|
+
}
|
|
414
|
+
const decoded = recurse(nested);
|
|
415
|
+
const shapeValue = (value as { shape?: unknown }).shape;
|
|
416
|
+
const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
|
|
417
|
+
const dtypeValue = (value as { dtype?: unknown }).dtype;
|
|
418
|
+
const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
|
|
419
|
+
const deviceValue = (value as { device?: unknown }).device;
|
|
420
|
+
const device = typeof deviceValue === 'string' ? deviceValue : undefined;
|
|
421
|
+
|
|
422
|
+
if (isPromiseLike(decoded)) {
|
|
423
|
+
return decoded.then(data => ({ data, shape, dtype, device })) as Promise<T | unknown>;
|
|
424
|
+
}
|
|
425
|
+
return { data: decoded, shape, dtype, device } satisfies TorchTensor;
|
|
237
426
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
(value as { encoding?: unknown }).encoding
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
427
|
+
|
|
428
|
+
if (marker === 'sklearn.estimator') {
|
|
429
|
+
const encoding = (value as { encoding?: unknown }).encoding;
|
|
430
|
+
if (encoding !== 'json') {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Invalid sklearn.estimator envelope: unsupported encoding ${String(encoding)}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const className = (value as { className?: unknown }).className;
|
|
436
|
+
const module = (value as { module?: unknown }).module;
|
|
437
|
+
const params = (value as { params?: unknown }).params;
|
|
438
|
+
if (typeof className !== 'string' || typeof module !== 'string' || !isObject(params)) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
'Invalid sklearn.estimator envelope: expected className/module strings + params object'
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const versionValue = (value as { version?: unknown }).version;
|
|
444
|
+
if (versionValue !== undefined && typeof versionValue !== 'string') {
|
|
445
|
+
throw new Error('Invalid sklearn.estimator envelope: version must be a string when provided');
|
|
446
|
+
}
|
|
447
|
+
const version = typeof versionValue === 'string' ? versionValue : undefined;
|
|
251
448
|
return {
|
|
252
|
-
className
|
|
253
|
-
module
|
|
254
|
-
version
|
|
255
|
-
params
|
|
449
|
+
className,
|
|
450
|
+
module,
|
|
451
|
+
version,
|
|
452
|
+
params,
|
|
256
453
|
} satisfies SklearnEstimator;
|
|
257
454
|
}
|
|
455
|
+
|
|
258
456
|
return value as unknown;
|
|
259
457
|
}
|
|
260
458
|
|
|
459
|
+
function decodeEnvelope<T>(value: unknown, decodeArrow: (bytes: Uint8Array) => T): T | unknown {
|
|
460
|
+
const recurse: (value: unknown) => MaybePromise<T | unknown> = v =>
|
|
461
|
+
decodeEnvelopeCore(v, decodeArrow, recurse);
|
|
462
|
+
const decoded = decodeEnvelopeCore(value, decodeArrow, recurse);
|
|
463
|
+
if (isPromiseLike(decoded)) {
|
|
464
|
+
throw new Error('Unexpected Promise return from decodeValue; use decodeValueAsync instead.');
|
|
465
|
+
}
|
|
466
|
+
return decoded;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function decodeEnvelopeAsync<T>(
|
|
470
|
+
value: unknown,
|
|
471
|
+
decodeArrow: (bytes: Uint8Array) => Promise<T>
|
|
472
|
+
): Promise<T | unknown> {
|
|
473
|
+
const recurse: (value: unknown) => MaybePromise<T | unknown> = v =>
|
|
474
|
+
decodeEnvelopeCore(v, decodeArrow, recurse);
|
|
475
|
+
return await decodeEnvelopeCore(value, decodeArrow, recurse);
|
|
476
|
+
}
|
|
477
|
+
|
|
261
478
|
/**
|
|
262
479
|
* Decode values produced by the Python bridge.
|
|
263
480
|
*/
|
|
264
481
|
export async function decodeValueAsync(value: unknown): Promise<DecodedValue> {
|
|
265
|
-
return await
|
|
482
|
+
return await decodeEnvelopeAsync(value, tryDecodeArrowTable);
|
|
266
483
|
}
|
|
267
484
|
|
|
268
485
|
/**
|
package/src/utils/logger.ts
CHANGED
|
@@ -134,6 +134,7 @@ class LoggerImpl implements Logger {
|
|
|
134
134
|
if (!this.enabled) {
|
|
135
135
|
return false;
|
|
136
136
|
}
|
|
137
|
+
// eslint-disable-next-line security/detect-object-injection -- level is constrained to LogLevel
|
|
137
138
|
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.level];
|
|
138
139
|
}
|
|
139
140
|
|