tywrap 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +42 -22
  2. package/SECURITY.md +38 -0
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/index.js +8 -0
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/core/annotation-parser.d.ts +2 -1
  7. package/dist/core/annotation-parser.d.ts.map +1 -1
  8. package/dist/core/annotation-parser.js +6 -3
  9. package/dist/core/annotation-parser.js.map +1 -1
  10. package/dist/core/generator.d.ts +23 -4
  11. package/dist/core/generator.d.ts.map +1 -1
  12. package/dist/core/generator.js +254 -170
  13. package/dist/core/generator.js.map +1 -1
  14. package/dist/core/mapper.d.ts +3 -2
  15. package/dist/core/mapper.d.ts.map +1 -1
  16. package/dist/core/mapper.js +5 -5
  17. package/dist/core/mapper.js.map +1 -1
  18. package/dist/index.d.ts +7 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/runtime/base-bridge.d.ts +3 -7
  23. package/dist/runtime/base-bridge.d.ts.map +1 -1
  24. package/dist/runtime/base-bridge.js +4 -17
  25. package/dist/runtime/base-bridge.js.map +1 -1
  26. package/dist/runtime/bounded-context.d.ts +3 -22
  27. package/dist/runtime/bounded-context.d.ts.map +1 -1
  28. package/dist/runtime/bounded-context.js +13 -53
  29. package/dist/runtime/bounded-context.js.map +1 -1
  30. package/dist/runtime/bridge-codec.d.ts +3 -3
  31. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  32. package/dist/runtime/bridge-codec.js +129 -62
  33. package/dist/runtime/bridge-codec.js.map +1 -1
  34. package/dist/runtime/errors.d.ts +16 -0
  35. package/dist/runtime/errors.d.ts.map +1 -1
  36. package/dist/runtime/errors.js +17 -0
  37. package/dist/runtime/errors.js.map +1 -1
  38. package/dist/runtime/http-transport.d.ts +1 -1
  39. package/dist/runtime/http-transport.d.ts.map +1 -1
  40. package/dist/runtime/http-transport.js +1 -1
  41. package/dist/runtime/http-transport.js.map +1 -1
  42. package/dist/runtime/index.d.ts +1 -0
  43. package/dist/runtime/index.d.ts.map +1 -1
  44. package/dist/runtime/index.js +1 -0
  45. package/dist/runtime/index.js.map +1 -1
  46. package/dist/runtime/node.d.ts +7 -36
  47. package/dist/runtime/node.d.ts.map +1 -1
  48. package/dist/runtime/node.js +2 -80
  49. package/dist/runtime/node.js.map +1 -1
  50. package/dist/runtime/pooled-transport.d.ts +120 -59
  51. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  52. package/dist/runtime/pooled-transport.js +345 -78
  53. package/dist/runtime/pooled-transport.js.map +1 -1
  54. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  55. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  56. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  57. package/dist/runtime/pyodide-transport.d.ts +1 -1
  58. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  59. package/dist/runtime/pyodide-transport.js +2 -3
  60. package/dist/runtime/pyodide-transport.js.map +1 -1
  61. package/dist/runtime/rpc-client.d.ts +7 -36
  62. package/dist/runtime/rpc-client.d.ts.map +1 -1
  63. package/dist/runtime/rpc-client.js +20 -102
  64. package/dist/runtime/rpc-client.js.map +1 -1
  65. package/dist/runtime/subprocess-transport.d.ts +30 -69
  66. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  67. package/dist/runtime/subprocess-transport.js +179 -236
  68. package/dist/runtime/subprocess-transport.js.map +1 -1
  69. package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
  70. package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
  71. package/dist/runtime/transport.d.ts +9 -19
  72. package/dist/runtime/transport.d.ts.map +1 -1
  73. package/dist/runtime/transport.js +1 -1
  74. package/dist/runtime/transport.js.map +1 -1
  75. package/dist/runtime/validators.d.ts +50 -0
  76. package/dist/runtime/validators.d.ts.map +1 -1
  77. package/dist/runtime/validators.js +154 -0
  78. package/dist/runtime/validators.js.map +1 -1
  79. package/dist/types/index.d.ts +14 -39
  80. package/dist/types/index.d.ts.map +1 -1
  81. package/dist/tywrap.d.ts +3 -2
  82. package/dist/tywrap.d.ts.map +1 -1
  83. package/dist/tywrap.js +140 -13
  84. package/dist/tywrap.js.map +1 -1
  85. package/dist/utils/cache.d.ts +3 -16
  86. package/dist/utils/cache.d.ts.map +1 -1
  87. package/dist/utils/codec.d.ts +17 -0
  88. package/dist/utils/codec.d.ts.map +1 -1
  89. package/dist/utils/codec.js +500 -85
  90. package/dist/utils/codec.js.map +1 -1
  91. package/dist/utils/ir-cache.d.ts +2 -1
  92. package/dist/utils/ir-cache.d.ts.map +1 -1
  93. package/dist/utils/runtime.d.ts +0 -29
  94. package/dist/utils/runtime.d.ts.map +1 -1
  95. package/dist/utils/runtime.js +16 -107
  96. package/dist/utils/runtime.js.map +1 -1
  97. package/dist/version.d.ts.map +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist/version.js.map +1 -1
  100. package/package.json +7 -6
  101. package/runtime/frame_codec.py +7 -1
  102. package/runtime/python_bridge.py +85 -125
  103. package/runtime/tywrap_bridge_core.py +515 -187
  104. package/src/config/index.ts +11 -0
  105. package/src/core/annotation-parser.ts +7 -4
  106. package/src/core/generator.ts +330 -205
  107. package/src/core/mapper.ts +8 -8
  108. package/src/index.ts +7 -4
  109. package/src/runtime/base-bridge.ts +5 -30
  110. package/src/runtime/bounded-context.ts +12 -67
  111. package/src/runtime/bridge-codec.ts +158 -74
  112. package/src/runtime/errors.ts +21 -0
  113. package/src/runtime/http-transport.ts +6 -1
  114. package/src/runtime/index.ts +6 -0
  115. package/src/runtime/node.ts +9 -120
  116. package/src/runtime/pooled-transport.ts +424 -90
  117. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  118. package/src/runtime/pyodide-transport.ts +7 -3
  119. package/src/runtime/rpc-client.ts +37 -139
  120. package/src/runtime/subprocess-transport.ts +210 -285
  121. package/src/runtime/timed-out-request-tracker.ts +1 -1
  122. package/src/runtime/transport.ts +15 -23
  123. package/src/runtime/validators.ts +212 -0
  124. package/src/types/index.ts +21 -60
  125. package/src/tywrap.ts +157 -22
  126. package/src/utils/cache.ts +3 -3
  127. package/src/utils/codec.ts +749 -112
  128. package/src/utils/ir-cache.ts +1 -1
  129. package/src/utils/runtime.ts +17 -128
  130. package/src/version.ts +1 -1
  131. package/dist/core/discovery.d.ts +0 -103
  132. package/dist/core/discovery.d.ts.map +0 -1
  133. package/dist/core/discovery.js +0 -380
  134. package/dist/core/discovery.js.map +0 -1
  135. package/dist/core/validation.d.ts +0 -102
  136. package/dist/core/validation.d.ts.map +0 -1
  137. package/dist/core/validation.js +0 -490
  138. package/dist/core/validation.js.map +0 -1
  139. package/dist/runtime/base.d.ts +0 -22
  140. package/dist/runtime/base.d.ts.map +0 -1
  141. package/dist/runtime/base.js +0 -23
  142. package/dist/runtime/base.js.map +0 -1
  143. package/dist/runtime/transport-pool.d.ts +0 -196
  144. package/dist/runtime/transport-pool.d.ts.map +0 -1
  145. package/dist/runtime/transport-pool.js +0 -418
  146. package/dist/runtime/transport-pool.js.map +0 -1
  147. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  148. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  149. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  150. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  151. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  152. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  153. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  154. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  155. package/runtime/safe_codec.py +0 -352
  156. package/src/core/discovery.ts +0 -477
  157. package/src/core/validation.ts +0 -729
  158. package/src/runtime/base.ts +0 -24
  159. package/src/runtime/transport-pool.ts +0 -538
@@ -1,352 +0,0 @@
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 BridgeCodec:
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 = BridgeCodec()
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
- # The wording is Python-version dependent (3.12+ appends ": nan";
153
- # 3.10/3.11 emit only "Out of range float values are not JSON
154
- # compliant"), so match that canonical phrase too for a stable message.
155
- error_msg = str(exc).lower()
156
- if (
157
- 'nan' in error_msg
158
- or 'infinity' in error_msg
159
- or 'inf' in error_msg
160
- or 'out of range float' in error_msg
161
- ):
162
- raise CodecError(
163
- 'Cannot serialize NaN - NaN/Infinity not allowed in JSON'
164
- ) from exc
165
- raise CodecError(f'JSON encoding failed: {exc}') from exc
166
- except TypeError as exc:
167
- raise CodecError(f'JSON encoding failed: {exc}') from exc
168
-
169
- # Check payload size
170
- payload_bytes = len(result.encode('utf-8'))
171
- if payload_bytes > self.max_payload_bytes:
172
- raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
173
-
174
- return result
175
-
176
- def decode(self, payload: str) -> Any:
177
- """
178
- Decode a JSON string to a Python value.
179
-
180
- Args:
181
- payload: The JSON string to decode.
182
-
183
- Returns:
184
- The decoded Python value.
185
-
186
- Raises:
187
- CodecError: If decoding fails due to:
188
- - Payload exceeds max_payload_bytes
189
- - Invalid JSON syntax
190
- """
191
- # Check payload size first
192
- payload_bytes = len(payload.encode('utf-8'))
193
- if payload_bytes > self.max_payload_bytes:
194
- raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
195
-
196
- try:
197
- return json.loads(payload)
198
- except json.JSONDecodeError as exc:
199
- raise CodecError(f'JSON decoding failed: {exc}') from exc
200
-
201
- def _default_encoder(self, obj: Any) -> Any:
202
- """
203
- Handle special Python types during JSON encoding.
204
-
205
- This method is called by json.dumps for objects that are not natively
206
- JSON serializable.
207
-
208
- Args:
209
- obj: The object to encode.
210
-
211
- Returns:
212
- A JSON-serializable representation of the object.
213
-
214
- Raises:
215
- TypeError: If the object cannot be serialized.
216
- CodecError: If the object contains NaN/Infinity when not allowed.
217
- """
218
- # Handle numpy/pandas scalars first (they need .item() extraction)
219
- if _is_numpy_scalar(obj):
220
- extracted = obj.item()
221
- # Check for NaN/Infinity in extracted value
222
- if not self.allow_nan and _is_nan_or_inf(extracted):
223
- raise CodecError(
224
- 'Cannot serialize NaN - NaN/Infinity not allowed in JSON'
225
- )
226
- return extracted
227
-
228
- if _is_pandas_scalar(obj):
229
- try:
230
- import pandas as pd
231
- except ImportError:
232
- pass
233
- else:
234
- # Handle NaT (Not a Time)
235
- if obj is pd.NaT or (hasattr(pd, 'isna') and pd.isna(obj)):
236
- return None
237
- # Pandas Timestamp -> ISO string
238
- if isinstance(obj, pd.Timestamp):
239
- return obj.isoformat()
240
- # Pandas Timedelta -> total seconds
241
- if isinstance(obj, pd.Timedelta):
242
- return obj.total_seconds()
243
-
244
- # datetime types
245
- if isinstance(obj, datetime):
246
- return obj.isoformat()
247
-
248
- if isinstance(obj, date):
249
- return obj.isoformat()
250
-
251
- if isinstance(obj, time):
252
- return obj.isoformat()
253
-
254
- # timedelta -> total seconds (consistent with python_bridge.py)
255
- if isinstance(obj, timedelta):
256
- return obj.total_seconds()
257
-
258
- # Decimal -> string (preserves precision)
259
- if isinstance(obj, Decimal):
260
- return str(obj)
261
-
262
- # UUID -> string
263
- if isinstance(obj, UUID):
264
- return str(obj)
265
-
266
- # Path -> string
267
- if isinstance(obj, (Path, PurePath)):
268
- return str(obj)
269
-
270
- # bytes/bytearray -> base64 with type marker
271
- if isinstance(obj, (bytes, bytearray)):
272
- return {
273
- '__type__': 'bytes',
274
- 'encoding': 'base64',
275
- 'data': base64.b64encode(obj).decode('ascii'),
276
- }
277
-
278
- # Pydantic models
279
- if _has_pydantic_model_dump(obj):
280
- try:
281
- return obj.model_dump(by_alias=True, mode='json')
282
- except TypeError:
283
- # Older Pydantic versions may not support mode='json'
284
- return obj.model_dump(by_alias=True)
285
-
286
- # Sets -> lists (common conversion)
287
- if isinstance(obj, (set, frozenset)):
288
- return list(obj)
289
-
290
- # Complex numbers (rejected by default as they contain floats)
291
- if isinstance(obj, complex):
292
- raise TypeError(
293
- f'Object of type {type(obj).__name__} is not JSON serializable'
294
- )
295
-
296
- # Fallback: raise TypeError with clear message
297
- raise TypeError(
298
- f'Object of type {type(obj).__name__} is not JSON serializable'
299
- )
300
-
301
-
302
- # Module-level convenience instance with default settings
303
- _default_codec: Optional[BridgeCodec] = None
304
-
305
-
306
- def get_default_codec() -> BridgeCodec:
307
- """
308
- Get or create the default BridgeCodec instance.
309
-
310
- Returns:
311
- The default BridgeCodec instance with standard settings.
312
- """
313
- global _default_codec
314
- if _default_codec is None:
315
- _default_codec = BridgeCodec()
316
- return _default_codec
317
-
318
-
319
- def encode(value: Any, *, allow_nan: bool = False) -> str:
320
- """
321
- Convenience function to encode a value using default settings.
322
-
323
- Args:
324
- value: The Python value to encode.
325
- allow_nan: If True, allow NaN/Infinity values.
326
-
327
- Returns:
328
- A JSON string representation of the value.
329
-
330
- Raises:
331
- CodecError: If encoding fails.
332
- """
333
- if allow_nan:
334
- codec = BridgeCodec(allow_nan=True)
335
- return codec.encode(value)
336
- return get_default_codec().encode(value)
337
-
338
-
339
- def decode(payload: str) -> Any:
340
- """
341
- Convenience function to decode a JSON string using default settings.
342
-
343
- Args:
344
- payload: The JSON string to decode.
345
-
346
- Returns:
347
- The decoded Python value.
348
-
349
- Raises:
350
- CodecError: If decoding fails.
351
- """
352
- return get_default_codec().decode(payload)