tywrap 0.2.0 → 0.3.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/README.md +65 -31
- package/dist/cli.js +49 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +43 -7
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +47 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +184 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +344 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +48 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +60 -73
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +97 -130
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +392 -521
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +1 -1
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +1 -1
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +135 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +177 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +209 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +729 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +154 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +459 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +103 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +519 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +191 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +120 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +229 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +208 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +419 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/types/index.d.ts +51 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +227 -328
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +114 -1
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +40 -18
- package/runtime/python_bridge.py +108 -15
- package/runtime/safe_codec.py +344 -0
- package/src/cli.ts +61 -2
- package/src/config/index.ts +63 -17
- package/src/core/analyzer.ts +52 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/index.ts +78 -6
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +483 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +245 -0
- package/src/runtime/http.ts +76 -115
- package/src/runtime/node.ts +563 -676
- package/src/runtime/optimized-node.ts +4 -1
- package/src/runtime/pooled-transport.ts +263 -0
- package/src/runtime/process-io.ts +930 -0
- package/src/runtime/pyodide-io.ts +551 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +651 -0
- package/src/runtime/transport.ts +278 -0
- package/src/runtime/validators.ts +252 -0
- package/src/runtime/worker-pool.ts +559 -0
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -0
- package/src/tywrap.ts +279 -358
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +122 -1
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe JSON codec with explicit edge case handling.
|
|
3
|
+
|
|
4
|
+
Provides bidirectional validation and serialization for the JS<->Python bridge.
|
|
5
|
+
This module handles special Python types and enforces payload size limits to
|
|
6
|
+
ensure predictable behavior at the language boundary.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
from datetime import date, datetime, time, timedelta
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
from pathlib import Path, PurePath
|
|
15
|
+
from typing import Any, Optional, Union
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CodecError(Exception):
|
|
20
|
+
"""Raised when encoding/decoding fails."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_nan_or_inf(value: Any) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Check if a numeric value is NaN or Infinity.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
value: The value to check.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if the value is NaN, positive infinity, or negative infinity.
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(value, (int, float)):
|
|
36
|
+
return False
|
|
37
|
+
try:
|
|
38
|
+
return math.isnan(value) or math.isinf(value)
|
|
39
|
+
except (TypeError, ValueError):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_numpy_scalar(obj: Any) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Detect numpy scalar types when NumPy is installed.
|
|
46
|
+
|
|
47
|
+
Why: numpy scalars have an `.item()` method to extract Python primitives,
|
|
48
|
+
which is needed for JSON serialization.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
import numpy as np
|
|
52
|
+
except ImportError:
|
|
53
|
+
return False
|
|
54
|
+
return isinstance(obj, (np.generic, np.ndarray)) and obj.ndim == 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_pandas_scalar(obj: Any) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Detect pandas scalar types (Timestamp, Timedelta, etc.).
|
|
60
|
+
|
|
61
|
+
Why: pandas wraps numpy scalars with additional metadata; extracting the
|
|
62
|
+
underlying value ensures clean JSON serialization.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
import pandas as pd
|
|
66
|
+
except ImportError:
|
|
67
|
+
return False
|
|
68
|
+
# Check for pandas Timestamp, Timedelta, NaT, etc.
|
|
69
|
+
return isinstance(
|
|
70
|
+
obj,
|
|
71
|
+
(
|
|
72
|
+
pd.Timestamp,
|
|
73
|
+
pd.Timedelta,
|
|
74
|
+
type(pd.NaT),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _has_pydantic_model_dump(obj: Any) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Check if object is a Pydantic v2 model with model_dump method.
|
|
82
|
+
|
|
83
|
+
Why: Pydantic models should be serialized via their built-in mechanism
|
|
84
|
+
to respect field aliases and serialization modes.
|
|
85
|
+
"""
|
|
86
|
+
model_dump = getattr(obj, 'model_dump', None)
|
|
87
|
+
return callable(model_dump)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SafeCodec:
|
|
91
|
+
"""
|
|
92
|
+
Safe JSON codec with explicit edge case handling.
|
|
93
|
+
|
|
94
|
+
This codec provides:
|
|
95
|
+
- Rejection of NaN/Infinity by default (configurable)
|
|
96
|
+
- Payload size limits to prevent memory exhaustion
|
|
97
|
+
- Automatic handling of common Python types (datetime, Decimal, UUID, etc.)
|
|
98
|
+
- Support for numpy/pandas scalars
|
|
99
|
+
- Pydantic model serialization
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
allow_nan: If False (default), reject NaN/Infinity values.
|
|
103
|
+
max_payload_bytes: Maximum payload size in bytes (default 10MB).
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> codec = SafeCodec()
|
|
107
|
+
>>> codec.encode({"key": "value"})
|
|
108
|
+
'{"key": "value"}'
|
|
109
|
+
>>> codec.decode('{"key": "value"}')
|
|
110
|
+
{'key': 'value'}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
allow_nan: bool = False,
|
|
116
|
+
max_payload_bytes: int = 10 * 1024 * 1024,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Initialize the codec with configuration.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
allow_nan: If False (default), reject NaN/Infinity values.
|
|
123
|
+
max_payload_bytes: Maximum payload size in bytes (default 10MB).
|
|
124
|
+
"""
|
|
125
|
+
self.allow_nan = allow_nan
|
|
126
|
+
self.max_payload_bytes = max_payload_bytes
|
|
127
|
+
|
|
128
|
+
def encode(self, value: Any) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Encode a Python value to a JSON string.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
value: The Python value to encode.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
A JSON string representation of the value.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
CodecError: If encoding fails due to:
|
|
140
|
+
- NaN/Infinity values when allow_nan is False
|
|
141
|
+
- Payload exceeds max_payload_bytes
|
|
142
|
+
- Value contains non-serializable types
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
result = json.dumps(
|
|
146
|
+
value,
|
|
147
|
+
default=self._default_encoder,
|
|
148
|
+
allow_nan=self.allow_nan,
|
|
149
|
+
)
|
|
150
|
+
except ValueError as exc:
|
|
151
|
+
# json.dumps raises ValueError for NaN/Infinity when allow_nan=False
|
|
152
|
+
error_msg = str(exc).lower()
|
|
153
|
+
if 'nan' in error_msg or 'infinity' in error_msg or 'inf' in error_msg:
|
|
154
|
+
raise CodecError(
|
|
155
|
+
'Cannot serialize NaN - NaN/Infinity not allowed in JSON'
|
|
156
|
+
) from exc
|
|
157
|
+
raise CodecError(f'JSON encoding failed: {exc}') from exc
|
|
158
|
+
except TypeError as exc:
|
|
159
|
+
raise CodecError(f'JSON encoding failed: {exc}') from exc
|
|
160
|
+
|
|
161
|
+
# Check payload size
|
|
162
|
+
payload_bytes = len(result.encode('utf-8'))
|
|
163
|
+
if payload_bytes > self.max_payload_bytes:
|
|
164
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
def decode(self, payload: str) -> Any:
|
|
169
|
+
"""
|
|
170
|
+
Decode a JSON string to a Python value.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
payload: The JSON string to decode.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The decoded Python value.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
CodecError: If decoding fails due to:
|
|
180
|
+
- Payload exceeds max_payload_bytes
|
|
181
|
+
- Invalid JSON syntax
|
|
182
|
+
"""
|
|
183
|
+
# Check payload size first
|
|
184
|
+
payload_bytes = len(payload.encode('utf-8'))
|
|
185
|
+
if payload_bytes > self.max_payload_bytes:
|
|
186
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
return json.loads(payload)
|
|
190
|
+
except json.JSONDecodeError as exc:
|
|
191
|
+
raise CodecError(f'JSON decoding failed: {exc}') from exc
|
|
192
|
+
|
|
193
|
+
def _default_encoder(self, obj: Any) -> Any:
|
|
194
|
+
"""
|
|
195
|
+
Handle special Python types during JSON encoding.
|
|
196
|
+
|
|
197
|
+
This method is called by json.dumps for objects that are not natively
|
|
198
|
+
JSON serializable.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
obj: The object to encode.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
A JSON-serializable representation of the object.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
TypeError: If the object cannot be serialized.
|
|
208
|
+
CodecError: If the object contains NaN/Infinity when not allowed.
|
|
209
|
+
"""
|
|
210
|
+
# Handle numpy/pandas scalars first (they need .item() extraction)
|
|
211
|
+
if _is_numpy_scalar(obj):
|
|
212
|
+
extracted = obj.item()
|
|
213
|
+
# Check for NaN/Infinity in extracted value
|
|
214
|
+
if not self.allow_nan and _is_nan_or_inf(extracted):
|
|
215
|
+
raise CodecError(
|
|
216
|
+
'Cannot serialize NaN - NaN/Infinity not allowed in JSON'
|
|
217
|
+
)
|
|
218
|
+
return extracted
|
|
219
|
+
|
|
220
|
+
if _is_pandas_scalar(obj):
|
|
221
|
+
try:
|
|
222
|
+
import pandas as pd
|
|
223
|
+
except ImportError:
|
|
224
|
+
pass
|
|
225
|
+
else:
|
|
226
|
+
# Handle NaT (Not a Time)
|
|
227
|
+
if obj is pd.NaT or (hasattr(pd, 'isna') and pd.isna(obj)):
|
|
228
|
+
return None
|
|
229
|
+
# Pandas Timestamp -> ISO string
|
|
230
|
+
if isinstance(obj, pd.Timestamp):
|
|
231
|
+
return obj.isoformat()
|
|
232
|
+
# Pandas Timedelta -> total seconds
|
|
233
|
+
if isinstance(obj, pd.Timedelta):
|
|
234
|
+
return obj.total_seconds()
|
|
235
|
+
|
|
236
|
+
# datetime types
|
|
237
|
+
if isinstance(obj, datetime):
|
|
238
|
+
return obj.isoformat()
|
|
239
|
+
|
|
240
|
+
if isinstance(obj, date):
|
|
241
|
+
return obj.isoformat()
|
|
242
|
+
|
|
243
|
+
if isinstance(obj, time):
|
|
244
|
+
return obj.isoformat()
|
|
245
|
+
|
|
246
|
+
# timedelta -> total seconds (consistent with python_bridge.py)
|
|
247
|
+
if isinstance(obj, timedelta):
|
|
248
|
+
return obj.total_seconds()
|
|
249
|
+
|
|
250
|
+
# Decimal -> string (preserves precision)
|
|
251
|
+
if isinstance(obj, Decimal):
|
|
252
|
+
return str(obj)
|
|
253
|
+
|
|
254
|
+
# UUID -> string
|
|
255
|
+
if isinstance(obj, UUID):
|
|
256
|
+
return str(obj)
|
|
257
|
+
|
|
258
|
+
# Path -> string
|
|
259
|
+
if isinstance(obj, (Path, PurePath)):
|
|
260
|
+
return str(obj)
|
|
261
|
+
|
|
262
|
+
# bytes/bytearray -> base64 with type marker
|
|
263
|
+
if isinstance(obj, (bytes, bytearray)):
|
|
264
|
+
return {
|
|
265
|
+
'__type__': 'bytes',
|
|
266
|
+
'encoding': 'base64',
|
|
267
|
+
'data': base64.b64encode(obj).decode('ascii'),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Pydantic models
|
|
271
|
+
if _has_pydantic_model_dump(obj):
|
|
272
|
+
try:
|
|
273
|
+
return obj.model_dump(by_alias=True, mode='json')
|
|
274
|
+
except TypeError:
|
|
275
|
+
# Older Pydantic versions may not support mode='json'
|
|
276
|
+
return obj.model_dump(by_alias=True)
|
|
277
|
+
|
|
278
|
+
# Sets -> lists (common conversion)
|
|
279
|
+
if isinstance(obj, (set, frozenset)):
|
|
280
|
+
return list(obj)
|
|
281
|
+
|
|
282
|
+
# Complex numbers (rejected by default as they contain floats)
|
|
283
|
+
if isinstance(obj, complex):
|
|
284
|
+
raise TypeError(
|
|
285
|
+
f'Object of type {type(obj).__name__} is not JSON serializable'
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Fallback: raise TypeError with clear message
|
|
289
|
+
raise TypeError(
|
|
290
|
+
f'Object of type {type(obj).__name__} is not JSON serializable'
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# Module-level convenience instance with default settings
|
|
295
|
+
_default_codec: Optional[SafeCodec] = None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_default_codec() -> SafeCodec:
|
|
299
|
+
"""
|
|
300
|
+
Get or create the default SafeCodec instance.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
The default SafeCodec instance with standard settings.
|
|
304
|
+
"""
|
|
305
|
+
global _default_codec
|
|
306
|
+
if _default_codec is None:
|
|
307
|
+
_default_codec = SafeCodec()
|
|
308
|
+
return _default_codec
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def encode(value: Any, *, allow_nan: bool = False) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Convenience function to encode a value using default settings.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
value: The Python value to encode.
|
|
317
|
+
allow_nan: If True, allow NaN/Infinity values.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
A JSON string representation of the value.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
CodecError: If encoding fails.
|
|
324
|
+
"""
|
|
325
|
+
if allow_nan:
|
|
326
|
+
codec = SafeCodec(allow_nan=True)
|
|
327
|
+
return codec.encode(value)
|
|
328
|
+
return get_default_codec().encode(value)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def decode(payload: str) -> Any:
|
|
332
|
+
"""
|
|
333
|
+
Convenience function to decode a JSON string using default settings.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
payload: The JSON string to decode.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The decoded Python value.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
CodecError: If decoding fails.
|
|
343
|
+
"""
|
|
344
|
+
return get_default_codec().decode(payload)
|
package/src/cli.ts
CHANGED
|
@@ -192,6 +192,11 @@ async function main(): Promise<void> {
|
|
|
192
192
|
type: 'boolean',
|
|
193
193
|
describe: 'Enable debug logging',
|
|
194
194
|
})
|
|
195
|
+
.option('verbose', {
|
|
196
|
+
alias: 'v',
|
|
197
|
+
type: 'boolean',
|
|
198
|
+
describe: 'Alias for --debug',
|
|
199
|
+
})
|
|
195
200
|
.option('fail-on-warn', {
|
|
196
201
|
type: 'boolean',
|
|
197
202
|
default: false,
|
|
@@ -215,6 +220,7 @@ async function main(): Promise<void> {
|
|
|
215
220
|
sourceMap?: boolean;
|
|
216
221
|
useCache?: boolean;
|
|
217
222
|
debug?: boolean;
|
|
223
|
+
verbose?: boolean;
|
|
218
224
|
failOnWarn: boolean;
|
|
219
225
|
check: boolean;
|
|
220
226
|
}>
|
|
@@ -253,7 +259,9 @@ async function main(): Promise<void> {
|
|
|
253
259
|
};
|
|
254
260
|
}
|
|
255
261
|
|
|
256
|
-
if (
|
|
262
|
+
if (argv.verbose === true) {
|
|
263
|
+
overrides.debug = true;
|
|
264
|
+
} else if (typeof argv.debug === 'boolean') {
|
|
257
265
|
overrides.debug = argv.debug;
|
|
258
266
|
}
|
|
259
267
|
|
|
@@ -279,11 +287,27 @@ async function main(): Promise<void> {
|
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
const res = await generate(options, { check: argv.check });
|
|
290
|
+
const warnings = res.warnings ?? [];
|
|
291
|
+
const moduleCount = Object.keys(options.pythonModules ?? {}).length;
|
|
292
|
+
|
|
293
|
+
const emitWarnings = (): void => {
|
|
294
|
+
if (warnings.length === 0) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
process.stderr.write(`Warnings (count ${warnings.length}):\n`);
|
|
298
|
+
for (const w of warnings) {
|
|
299
|
+
process.stderr.write(`- ${w}\n`);
|
|
300
|
+
}
|
|
301
|
+
process.stderr.write('\n');
|
|
302
|
+
};
|
|
303
|
+
|
|
282
304
|
if (argv.check) {
|
|
283
305
|
const outOfDate = res.outOfDate ?? [];
|
|
284
306
|
if (outOfDate.length === 0) {
|
|
307
|
+
emitWarnings();
|
|
285
308
|
process.stdout.write('Generated wrappers are up to date.\n');
|
|
286
309
|
} else {
|
|
310
|
+
emitWarnings();
|
|
287
311
|
process.stderr.write('Generated wrappers are out of date:\n');
|
|
288
312
|
for (const file of outOfDate) {
|
|
289
313
|
process.stderr.write(`- ${file}\n`);
|
|
@@ -294,7 +318,42 @@ async function main(): Promise<void> {
|
|
|
294
318
|
}
|
|
295
319
|
}
|
|
296
320
|
} else {
|
|
297
|
-
|
|
321
|
+
emitWarnings();
|
|
322
|
+
|
|
323
|
+
const written = res.written ?? [];
|
|
324
|
+
if (written.length === 0) {
|
|
325
|
+
process.stderr.write(
|
|
326
|
+
`No files were generated. This usually means Python failed to import one or more modules.\n`
|
|
327
|
+
);
|
|
328
|
+
const modulesConfigured = Object.keys(options.pythonModules ?? {});
|
|
329
|
+
if (modulesConfigured.length > 0) {
|
|
330
|
+
process.stderr.write(`Modules: ${modulesConfigured.join(', ')}\n`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const pythonImportPath = options.pythonImportPath ?? [];
|
|
334
|
+
if (pythonImportPath.length > 0) {
|
|
335
|
+
process.stderr.write(`pythonImportPath: ${pythonImportPath.join(', ')}\n`);
|
|
336
|
+
} else {
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
`Tip: set "pythonImportPath" in your tywrap config to include your local module directories.\n`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const pyPath = process.env.PYTHONPATH;
|
|
343
|
+
if (typeof pyPath === 'string' && pyPath.trim().length > 0) {
|
|
344
|
+
process.stderr.write(`PYTHONPATH: ${pyPath}\n`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
process.stderr.write(`Tip: re-run with --debug for more context.\n`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
process.stdout.write(
|
|
352
|
+
`Generated: ${written.length} files from ${moduleCount} modules (warnings: ${warnings.length}).\n`
|
|
353
|
+
);
|
|
354
|
+
for (const file of written) {
|
|
355
|
+
process.stdout.write(`- ${file}\n`);
|
|
356
|
+
}
|
|
298
357
|
}
|
|
299
358
|
if (argv.failOnWarn && res.warnings.length > 0) {
|
|
300
359
|
log.error(
|
package/src/config/index.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
|
-
import {
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { readFile, rm, writeFile, mkdtemp, symlink } from 'node:fs/promises';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
13
|
import { dirname, extname, resolve } from 'node:path';
|
|
13
14
|
import { pathToFileURL } from 'node:url';
|
|
@@ -20,11 +21,6 @@ import type {
|
|
|
20
21
|
} from '../types/index.js';
|
|
21
22
|
import { getDefaultPythonPath } from '../utils/python.js';
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* Public configuration type. Currently identical to {@link TywrapOptions}.
|
|
25
|
-
*/
|
|
26
|
-
export type TywrapConfig = TywrapOptions;
|
|
27
|
-
|
|
28
24
|
type DeepPartial<T> = {
|
|
29
25
|
[K in keyof T]?: T[K] extends Array<infer U>
|
|
30
26
|
? Array<U>
|
|
@@ -33,11 +29,22 @@ type DeepPartial<T> = {
|
|
|
33
29
|
: T[K];
|
|
34
30
|
};
|
|
35
31
|
|
|
32
|
+
/**
|
|
33
|
+
* User-authored configuration shape accepted by defineConfig() and config files.
|
|
34
|
+
*/
|
|
35
|
+
export type TywrapConfig = DeepPartial<TywrapOptions>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fully resolved configuration shape returned by createConfig() and resolveConfig().
|
|
39
|
+
*/
|
|
40
|
+
export type ResolvedTywrapConfig = TywrapOptions;
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* Default configuration values used when options are not supplied.
|
|
38
44
|
*/
|
|
39
|
-
const DEFAULT_CONFIG:
|
|
45
|
+
const DEFAULT_CONFIG: ResolvedTywrapConfig = {
|
|
40
46
|
pythonModules: {},
|
|
47
|
+
pythonImportPath: [],
|
|
41
48
|
output: { dir: './generated', format: 'esm', declaration: false, sourceMap: false },
|
|
42
49
|
runtime: { node: { pythonPath: getDefaultPythonPath(), timeout: 30000 } },
|
|
43
50
|
performance: { caching: false, batching: false, compression: 'none' },
|
|
@@ -85,9 +92,10 @@ function safeExists(path: string): boolean {
|
|
|
85
92
|
/**
|
|
86
93
|
* Validate configuration values and throw user-friendly errors when invalid.
|
|
87
94
|
*/
|
|
88
|
-
function validateConfig(config:
|
|
95
|
+
function validateConfig(config: ResolvedTywrapConfig): void {
|
|
89
96
|
const allowedTopLevel = new Set([
|
|
90
97
|
'pythonModules',
|
|
98
|
+
'pythonImportPath',
|
|
91
99
|
'output',
|
|
92
100
|
'runtime',
|
|
93
101
|
'performance',
|
|
@@ -105,6 +113,15 @@ function validateConfig(config: TywrapConfig): void {
|
|
|
105
113
|
if (typeof out.dir !== 'string') {
|
|
106
114
|
throw new Error('output.dir must be a string');
|
|
107
115
|
}
|
|
116
|
+
|
|
117
|
+
if (config.pythonImportPath !== undefined) {
|
|
118
|
+
if (
|
|
119
|
+
!Array.isArray(config.pythonImportPath) ||
|
|
120
|
+
config.pythonImportPath.some(p => typeof p !== 'string')
|
|
121
|
+
) {
|
|
122
|
+
throw new Error('pythonImportPath must be an array of strings');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
108
125
|
if (!['esm', 'cjs', 'both'].includes(out.format)) {
|
|
109
126
|
throw new Error('output.format must be one of "esm", "cjs" or "both"');
|
|
110
127
|
}
|
|
@@ -183,7 +200,7 @@ function validateConfig(config: TywrapConfig): void {
|
|
|
183
200
|
*
|
|
184
201
|
* @param overrides CLI or programmatic overrides
|
|
185
202
|
*/
|
|
186
|
-
export function createConfig(overrides: DeepPartial<TywrapOptions> = {}):
|
|
203
|
+
export function createConfig(overrides: DeepPartial<TywrapOptions> = {}): ResolvedTywrapConfig {
|
|
187
204
|
const merged = merge(DEFAULT_CONFIG, overrides);
|
|
188
205
|
validateConfig(merged);
|
|
189
206
|
return merged;
|
|
@@ -200,7 +217,9 @@ export interface ResolveConfigOptions {
|
|
|
200
217
|
* Resolve configuration by loading a config file (JSON/JS/TS) and merging
|
|
201
218
|
* defaults with any overrides.
|
|
202
219
|
*/
|
|
203
|
-
export async function resolveConfig(
|
|
220
|
+
export async function resolveConfig(
|
|
221
|
+
options: ResolveConfigOptions = {}
|
|
222
|
+
): Promise<ResolvedTywrapConfig> {
|
|
204
223
|
const cwd = options.cwd ?? process.cwd();
|
|
205
224
|
const overrides = options.overrides ?? {};
|
|
206
225
|
const configFile = options.configFile ? resolve(cwd, options.configFile) : undefined;
|
|
@@ -272,6 +291,20 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
|
|
|
272
291
|
fileName: resolved,
|
|
273
292
|
});
|
|
274
293
|
|
|
294
|
+
let transpiledOutput = output.outputText;
|
|
295
|
+
if (!emitCommonJs) {
|
|
296
|
+
try {
|
|
297
|
+
// Preserve support for `import { defineConfig } from 'tywrap'` when ESM config
|
|
298
|
+
// is evaluated from an OS temp directory outside the package scope.
|
|
299
|
+
const tywrapEntryHref = await import.meta.resolve('tywrap');
|
|
300
|
+
transpiledOutput = transpiledOutput
|
|
301
|
+
.replaceAll("'tywrap'", `'${tywrapEntryHref}'`)
|
|
302
|
+
.replaceAll('\"tywrap\"', `\"${tywrapEntryHref}\"`);
|
|
303
|
+
} catch {
|
|
304
|
+
// Best-effort: leave source as-is when tywrap cannot be resolved.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
275
308
|
if (emitCommonJs) {
|
|
276
309
|
// Why: `.cts` is explicitly CommonJS. We evaluate the transpiled output in-memory using
|
|
277
310
|
// Node's Module internals (`Module._compile` / `_nodeModulePaths`) to avoid writing an extra
|
|
@@ -309,18 +342,31 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
|
|
|
309
342
|
return ensureConfigObject(loaded ?? {}, resolved);
|
|
310
343
|
}
|
|
311
344
|
|
|
312
|
-
// Why: Node can't import ESM from a string without a custom loader. We write
|
|
313
|
-
// output to a temporary `.mjs` file
|
|
314
|
-
//
|
|
315
|
-
const
|
|
345
|
+
// Why: Node can't import ESM from a string without a custom loader. We write transpiled
|
|
346
|
+
// output to a temporary `.mjs` file under the OS temp directory (not next to user config),
|
|
347
|
+
// then clean up both file and temporary directory after loading.
|
|
348
|
+
const tempDir = await mkdtemp(resolve(tmpdir(), 'tywrap-config-'));
|
|
349
|
+
const tmpPath = resolve(tempDir, `.tywrap.config.${randomUUID()}.mjs`);
|
|
316
350
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
351
|
+
const localNodeModules = resolve(process.cwd(), 'node_modules');
|
|
352
|
+
if (safeExists(localNodeModules)) {
|
|
353
|
+
const tempNodeModules = resolve(tempDir, 'node_modules');
|
|
354
|
+
try {
|
|
355
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
356
|
+
await symlink(localNodeModules, tempNodeModules, 'dir');
|
|
357
|
+
} catch {
|
|
358
|
+
// Best-effort only; regular resolution may still succeed without a symlink.
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
|
|
363
|
+
await writeFile(tmpPath, transpiledOutput, 'utf-8');
|
|
319
364
|
const mod = (await import(pathToFileURL(tmpPath).href)) as Record<string, unknown>;
|
|
320
365
|
const loaded = mod.default ?? mod;
|
|
321
366
|
return ensureConfigObject(loaded ?? {}, resolved);
|
|
322
367
|
} finally {
|
|
323
368
|
await rm(tmpPath, { force: true });
|
|
369
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
324
370
|
}
|
|
325
371
|
}
|
|
326
372
|
|
|
@@ -335,6 +381,6 @@ async function safeReadFileAsync(path: string): Promise<string> {
|
|
|
335
381
|
/**
|
|
336
382
|
* Type helper for authoring tywrap configs with full type inference.
|
|
337
383
|
*/
|
|
338
|
-
export function defineConfig(config:
|
|
384
|
+
export function defineConfig<T extends TywrapConfig>(config: T): T {
|
|
339
385
|
return config;
|
|
340
386
|
}
|