tywrap 0.8.0 → 0.9.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 +22 -7
- package/SECURITY.md +39 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +2 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +6 -3
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/generator.d.ts +23 -4
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +243 -170
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +3 -2
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +5 -5
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +3 -7
- package/dist/runtime/base-bridge.d.ts.map +1 -1
- package/dist/runtime/base-bridge.js +4 -17
- package/dist/runtime/base-bridge.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +3 -22
- package/dist/runtime/bounded-context.d.ts.map +1 -1
- package/dist/runtime/bounded-context.js +13 -53
- package/dist/runtime/bounded-context.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +1 -1
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +78 -55
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/errors.d.ts +16 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +17 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-transport.d.ts +1 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +1 -1
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +7 -36
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +2 -80
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +120 -59
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +345 -78
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/pyodide-transport.d.ts +1 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +2 -3
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +7 -36
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +20 -102
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +30 -69
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +179 -236
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
- package/dist/runtime/transport.d.ts +9 -19
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +49 -0
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +152 -0
- package/dist/runtime/validators.js.map +1 -1
- package/dist/types/index.d.ts +14 -39
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +3 -2
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +140 -13
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +3 -16
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/codec.d.ts +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +13 -13
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +2 -1
- package/dist/utils/ir-cache.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts +0 -29
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +16 -107
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/runtime/frame_codec.py +7 -1
- package/runtime/python_bridge.py +85 -125
- package/runtime/tywrap_bridge_core.py +133 -150
- package/src/config/index.ts +11 -0
- package/src/core/annotation-parser.ts +7 -4
- package/src/core/generator.ts +315 -205
- package/src/core/mapper.ts +8 -8
- package/src/index.ts +7 -4
- package/src/runtime/base-bridge.ts +5 -30
- package/src/runtime/bounded-context.ts +12 -67
- package/src/runtime/bridge-codec.ts +94 -65
- package/src/runtime/errors.ts +21 -0
- package/src/runtime/http-transport.ts +6 -1
- package/src/runtime/index.ts +6 -0
- package/src/runtime/node.ts +9 -120
- package/src/runtime/pooled-transport.ts +424 -90
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +7 -3
- package/src/runtime/rpc-client.ts +37 -139
- package/src/runtime/subprocess-transport.ts +210 -285
- package/src/runtime/timed-out-request-tracker.ts +1 -1
- package/src/runtime/transport.ts +15 -23
- package/src/runtime/validators.ts +204 -0
- package/src/types/index.ts +21 -60
- package/src/tywrap.ts +157 -22
- package/src/utils/cache.ts +3 -3
- package/src/utils/codec.ts +21 -13
- package/src/utils/ir-cache.ts +1 -1
- package/src/utils/runtime.ts +17 -128
- package/src/version.ts +1 -1
- package/dist/core/discovery.d.ts +0 -103
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -380
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/validation.d.ts +0 -102
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/validation.js +0 -490
- package/dist/core/validation.js.map +0 -1
- package/dist/runtime/base.d.ts +0 -22
- package/dist/runtime/base.d.ts.map +0 -1
- package/dist/runtime/base.js +0 -23
- package/dist/runtime/base.js.map +0 -1
- package/dist/runtime/transport-pool.d.ts +0 -196
- package/dist/runtime/transport-pool.d.ts.map +0 -1
- package/dist/runtime/transport-pool.js +0 -418
- package/dist/runtime/transport-pool.js.map +0 -1
- package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/safe_codec.py +0 -352
- package/src/core/discovery.ts +0 -477
- package/src/core/validation.ts +0 -729
- package/src/runtime/base.ts +0 -24
- package/src/runtime/transport-pool.ts +0 -538
|
@@ -43,12 +43,11 @@ derived from TYWRAP_CODEC_FALLBACK=json so that "Node in json-fallback mode" and
|
|
|
43
43
|
import base64
|
|
44
44
|
import datetime as dt
|
|
45
45
|
import decimal
|
|
46
|
-
import functools
|
|
47
46
|
import importlib
|
|
48
47
|
import importlib.util
|
|
49
|
-
import inspect
|
|
50
48
|
import json
|
|
51
49
|
import math
|
|
50
|
+
import sys
|
|
52
51
|
import traceback
|
|
53
52
|
import uuid
|
|
54
53
|
from pathlib import Path, PurePath
|
|
@@ -64,10 +63,6 @@ class ProtocolError(Exception):
|
|
|
64
63
|
"""Raised for malformed requests (bad protocol/id/method/params)."""
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
class InstanceHandleError(ValueError):
|
|
68
|
-
"""Raised when an instance handle is unknown or no longer valid."""
|
|
69
|
-
|
|
70
|
-
|
|
71
66
|
class ImportNotAllowedError(PermissionError):
|
|
72
67
|
"""Raised when a requested module import is not on the active allowlist."""
|
|
73
68
|
|
|
@@ -96,8 +91,8 @@ class AttributeNotAllowedError(PermissionError):
|
|
|
96
91
|
# IMPORT / ATTRIBUTE ALLOWLIST (trust boundary enforcement)
|
|
97
92
|
# =============================================================================
|
|
98
93
|
#
|
|
99
|
-
# The bridge dispatches call
|
|
100
|
-
#
|
|
94
|
+
# The bridge dispatches call requests by importing the requested module and
|
|
95
|
+
# getattr-ing the requested function/class method. That is an
|
|
101
96
|
# arbitrary import+getattr+call surface, so two complementary guards live here.
|
|
102
97
|
# Both are PURE (no env reads) so the rules behave identically under the
|
|
103
98
|
# subprocess server and the in-WASM Pyodide server; the subprocess server derives
|
|
@@ -204,22 +199,6 @@ def resolve_allowed_attr_path(root, dotted_name, *, allow_private_attrs):
|
|
|
204
199
|
return obj
|
|
205
200
|
|
|
206
201
|
|
|
207
|
-
def is_accessor_attr(obj, attr_name):
|
|
208
|
-
"""
|
|
209
|
-
True when attr_name resolves to a @property or functools.cached_property on
|
|
210
|
-
obj's type — i.e. it is read by attribute access, not called.
|
|
211
|
-
|
|
212
|
-
Inspects type(obj)'s MRO via getattr_static (which never triggers the
|
|
213
|
-
descriptor protocol), NOT the instance dict. That matters for
|
|
214
|
-
cached_property: after the first read it stores its value in the instance
|
|
215
|
-
__dict__, so an instance-level static lookup would return the cached value
|
|
216
|
-
rather than the descriptor and misclassify it as a method on the next read.
|
|
217
|
-
Reading from the type keeps the classification stable across repeated reads.
|
|
218
|
-
"""
|
|
219
|
-
descriptor = inspect.getattr_static(type(obj), attr_name, None)
|
|
220
|
-
return isinstance(descriptor, (property, functools.cached_property))
|
|
221
|
-
|
|
222
|
-
|
|
223
202
|
class CodecError(Exception):
|
|
224
203
|
"""Raised when value encoding fails (e.g. NaN/Infinity not allowed)."""
|
|
225
204
|
|
|
@@ -269,22 +248,24 @@ def _deserialize_bytes_envelope(value):
|
|
|
269
248
|
return _NO_DESERIALIZE
|
|
270
249
|
|
|
271
250
|
|
|
272
|
-
def deserialize(value):
|
|
251
|
+
def deserialize(value, *, has_envelope_markers=True):
|
|
273
252
|
"""
|
|
274
253
|
Recursively deserialize request values into Python-native types.
|
|
275
254
|
|
|
276
255
|
Why: requests are JSON-only; we need a small set of explicit decoders
|
|
277
256
|
(currently bytes) to restore Python semantics at the boundary.
|
|
278
257
|
"""
|
|
258
|
+
if not has_envelope_markers:
|
|
259
|
+
return value
|
|
279
260
|
decoded = _deserialize_bytes_envelope(value)
|
|
280
261
|
if decoded is not _NO_DESERIALIZE:
|
|
281
262
|
return decoded
|
|
282
263
|
|
|
283
264
|
if isinstance(value, list):
|
|
284
|
-
return [deserialize(item) for item in value]
|
|
265
|
+
return [deserialize(item, has_envelope_markers=True) for item in value]
|
|
285
266
|
if isinstance(value, dict):
|
|
286
267
|
# Preserve dict shape while decoding nested values.
|
|
287
|
-
return {k: deserialize(v) for k, v in value.items()}
|
|
268
|
+
return {k: deserialize(v, has_envelope_markers=True) for k, v in value.items()}
|
|
288
269
|
return value
|
|
289
270
|
|
|
290
271
|
|
|
@@ -314,33 +295,33 @@ def module_available(module_name):
|
|
|
314
295
|
|
|
315
296
|
|
|
316
297
|
def is_numpy_array(obj):
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
298
|
+
"""Return whether obj is an ndarray without importing NumPy."""
|
|
299
|
+
np = sys.modules.get('numpy')
|
|
300
|
+
if np is None:
|
|
320
301
|
return False
|
|
321
302
|
return isinstance(obj, np.ndarray)
|
|
322
303
|
|
|
323
304
|
|
|
324
305
|
def is_pandas_dataframe(obj):
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
306
|
+
"""Return whether obj is a DataFrame without importing pandas."""
|
|
307
|
+
pd = sys.modules.get('pandas')
|
|
308
|
+
if pd is None:
|
|
328
309
|
return False
|
|
329
310
|
return isinstance(obj, pd.DataFrame)
|
|
330
311
|
|
|
331
312
|
|
|
332
313
|
def is_pandas_series(obj):
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
314
|
+
"""Return whether obj is a Series without importing pandas."""
|
|
315
|
+
pd = sys.modules.get('pandas')
|
|
316
|
+
if pd is None:
|
|
336
317
|
return False
|
|
337
318
|
return isinstance(obj, pd.Series)
|
|
338
319
|
|
|
339
320
|
|
|
340
321
|
def is_scipy_sparse(obj):
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
322
|
+
"""Return whether obj is sparse without importing SciPy."""
|
|
323
|
+
sp = sys.modules.get('scipy.sparse')
|
|
324
|
+
if sp is None:
|
|
344
325
|
return False
|
|
345
326
|
try:
|
|
346
327
|
return sp.issparse(obj)
|
|
@@ -349,9 +330,9 @@ def is_scipy_sparse(obj):
|
|
|
349
330
|
|
|
350
331
|
|
|
351
332
|
def is_torch_tensor(obj):
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
333
|
+
"""Return whether obj is a Tensor without importing PyTorch."""
|
|
334
|
+
torch = sys.modules.get('torch')
|
|
335
|
+
if torch is None:
|
|
355
336
|
return False
|
|
356
337
|
try:
|
|
357
338
|
return torch.is_tensor(obj)
|
|
@@ -360,11 +341,11 @@ def is_torch_tensor(obj):
|
|
|
360
341
|
|
|
361
342
|
|
|
362
343
|
def is_sklearn_estimator(obj):
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
"""Return whether obj is an estimator without importing scikit-learn."""
|
|
345
|
+
sklearn_base = sys.modules.get('sklearn.base')
|
|
346
|
+
if sklearn_base is None:
|
|
366
347
|
return False
|
|
367
|
-
return isinstance(obj, BaseEstimator)
|
|
348
|
+
return isinstance(obj, sklearn_base.BaseEstimator)
|
|
368
349
|
|
|
369
350
|
|
|
370
351
|
# =============================================================================
|
|
@@ -748,26 +729,45 @@ def serialize_stdlib(obj):
|
|
|
748
729
|
|
|
749
730
|
def serialize(obj, *, force_json_markers, torch_allow_copy=False):
|
|
750
731
|
"""
|
|
751
|
-
Top-level result serializer.
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
732
|
+
Top-level result serializer.
|
|
733
|
+
|
|
734
|
+
Scientific codecs are type-first and only inspect packages that the value can
|
|
735
|
+
belong to. A value from an optional package implies that package is already in
|
|
736
|
+
sys.modules, so these checks never cold-import the scientific stack. The
|
|
737
|
+
package dispatch deliberately precedes the JSON-native fast path: e.g. a
|
|
738
|
+
package-defined subclass of dict still receives its relevant codec check.
|
|
739
|
+
The remaining BridgeCodec value behaviors (numpy/pandas scalars, bytes, sets,
|
|
740
|
+
complex rejection, NaN/Infinity) are applied later during JSON encoding by
|
|
741
|
+
default_encoder.
|
|
756
742
|
"""
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
)
|
|
769
|
-
|
|
743
|
+
package = type(obj).__module__.split('.', 1)[0]
|
|
744
|
+
|
|
745
|
+
if package == 'numpy' and 'numpy' in sys.modules:
|
|
746
|
+
if is_numpy_array(obj):
|
|
747
|
+
return serialize_ndarray(obj, force_json_markers=force_json_markers)
|
|
748
|
+
elif package == 'pandas' and 'pandas' in sys.modules:
|
|
749
|
+
if is_pandas_dataframe(obj):
|
|
750
|
+
return serialize_dataframe(obj, force_json_markers=force_json_markers)
|
|
751
|
+
if is_pandas_series(obj):
|
|
752
|
+
return serialize_series(obj, force_json_markers=force_json_markers)
|
|
753
|
+
elif package == 'scipy' and 'scipy.sparse' in sys.modules:
|
|
754
|
+
if is_scipy_sparse(obj):
|
|
755
|
+
return serialize_sparse_matrix(obj)
|
|
756
|
+
elif package == 'torch' and 'torch' in sys.modules:
|
|
757
|
+
if is_torch_tensor(obj):
|
|
758
|
+
return serialize_torch_tensor(
|
|
759
|
+
obj, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy
|
|
760
|
+
)
|
|
761
|
+
elif 'sklearn.base' in sys.modules and is_sklearn_estimator(obj):
|
|
762
|
+
# No package gate here, unlike the branches above: subclassing
|
|
763
|
+
# BaseEstimator is sklearn's documented extension point, so user-defined
|
|
764
|
+
# estimators live outside the 'sklearn' package and must still get the
|
|
765
|
+
# estimator serializer (and its param-naming errors).
|
|
770
766
|
return serialize_sklearn_estimator(obj)
|
|
767
|
+
|
|
768
|
+
if isinstance(obj, (type(None), bool, int, float, str, dict, list, tuple)):
|
|
769
|
+
return obj
|
|
770
|
+
|
|
771
771
|
pydantic_value = serialize_pydantic(obj)
|
|
772
772
|
if pydantic_value is not _NO_PYDANTIC:
|
|
773
773
|
return pydantic_value
|
|
@@ -778,14 +778,12 @@ def serialize(obj, *, force_json_markers, torch_allow_copy=False):
|
|
|
778
778
|
|
|
779
779
|
|
|
780
780
|
# =============================================================================
|
|
781
|
-
# JSON
|
|
781
|
+
# JSON CODEC: value handling and size-limited encode/decode
|
|
782
782
|
# =============================================================================
|
|
783
783
|
#
|
|
784
|
-
# This
|
|
785
|
-
#
|
|
786
|
-
#
|
|
787
|
-
# core encoder exists so the Pyodide server gets identical value handling without
|
|
788
|
-
# depending on safe_codec.py. The conformance suite asserts these behaviors match.
|
|
784
|
+
# This is the single Python implementation used by both the subprocess bridge and
|
|
785
|
+
# the embedded Pyodide core. BridgeCodec adds payload-size enforcement around the
|
|
786
|
+
# shared encoder; encode_value is the unbounded form used by the in-memory bridge.
|
|
789
787
|
|
|
790
788
|
def _is_nan_or_inf(value):
|
|
791
789
|
if not isinstance(value, (int, float)):
|
|
@@ -910,6 +908,56 @@ def encode_value(value, *, allow_nan):
|
|
|
910
908
|
raise CodecError(f'JSON encoding failed: {exc}') from exc
|
|
911
909
|
|
|
912
910
|
|
|
911
|
+
class BridgeCodec:
|
|
912
|
+
"""Safe JSON codec with explicit value handling and payload-size limits."""
|
|
913
|
+
|
|
914
|
+
def __init__(self, allow_nan=False, max_payload_bytes=10 * 1024 * 1024):
|
|
915
|
+
self.allow_nan = allow_nan
|
|
916
|
+
self.max_payload_bytes = max_payload_bytes
|
|
917
|
+
self._encoder = make_default_encoder(allow_nan=allow_nan)
|
|
918
|
+
|
|
919
|
+
def encode(self, value):
|
|
920
|
+
result = encode_value(value, allow_nan=self.allow_nan)
|
|
921
|
+
if len(result.encode('utf-8')) > self.max_payload_bytes:
|
|
922
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
923
|
+
return result
|
|
924
|
+
|
|
925
|
+
def decode(self, payload):
|
|
926
|
+
if len(payload.encode('utf-8')) > self.max_payload_bytes:
|
|
927
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
928
|
+
try:
|
|
929
|
+
return json.loads(payload)
|
|
930
|
+
except json.JSONDecodeError as exc:
|
|
931
|
+
raise CodecError(f'JSON decoding failed: {exc}') from exc
|
|
932
|
+
|
|
933
|
+
def _default_encoder(self, obj):
|
|
934
|
+
"""Compatibility hook for callers that used the codec's JSON encoder."""
|
|
935
|
+
return self._encoder(obj)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
_default_codec = None
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def get_default_codec():
|
|
942
|
+
"""Return the lazily-created default BridgeCodec instance."""
|
|
943
|
+
global _default_codec
|
|
944
|
+
if _default_codec is None:
|
|
945
|
+
_default_codec = BridgeCodec()
|
|
946
|
+
return _default_codec
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def encode(value, *, allow_nan=False):
|
|
950
|
+
"""Encode a value with the default codec settings."""
|
|
951
|
+
if allow_nan:
|
|
952
|
+
return BridgeCodec(allow_nan=True).encode(value)
|
|
953
|
+
return get_default_codec().encode(value)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def decode(payload):
|
|
957
|
+
"""Decode a JSON payload with the default codec settings."""
|
|
958
|
+
return get_default_codec().decode(payload)
|
|
959
|
+
|
|
960
|
+
|
|
913
961
|
# =============================================================================
|
|
914
962
|
# REQUEST VALIDATION + HANDLERS + DISPATCH
|
|
915
963
|
# =============================================================================
|
|
@@ -949,11 +997,19 @@ def coerce_dict(value, key):
|
|
|
949
997
|
return value
|
|
950
998
|
|
|
951
999
|
|
|
952
|
-
def handle_call(
|
|
1000
|
+
def handle_call(
|
|
1001
|
+
params,
|
|
1002
|
+
*,
|
|
1003
|
+
force_json_markers,
|
|
1004
|
+
torch_allow_copy,
|
|
1005
|
+
allowed_modules,
|
|
1006
|
+
allow_private_attrs,
|
|
1007
|
+
has_envelope_markers,
|
|
1008
|
+
):
|
|
953
1009
|
module_name = require_str(params, 'module')
|
|
954
1010
|
function_name = require_str(params, 'functionName')
|
|
955
|
-
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
956
|
-
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'))
|
|
1011
|
+
args = deserialize(coerce_list(params.get('args'), 'args'), has_envelope_markers=has_envelope_markers)
|
|
1012
|
+
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'), has_envelope_markers=has_envelope_markers)
|
|
957
1013
|
mod = import_allowed_module(module_name, allowed_modules)
|
|
958
1014
|
# function_name may be dotted ('Class.method') for @classmethod/@staticmethod
|
|
959
1015
|
# calls, which the generated wrapper routes through call() rather than an
|
|
@@ -963,61 +1019,13 @@ def handle_call(params, *, force_json_markers, torch_allow_copy, allowed_modules
|
|
|
963
1019
|
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
|
|
964
1020
|
|
|
965
1021
|
|
|
966
|
-
def handle_instantiate(params, instances, *, allowed_modules, allow_private_attrs):
|
|
967
|
-
module_name = require_str(params, 'module')
|
|
968
|
-
class_name = require_str(params, 'className')
|
|
969
|
-
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
970
|
-
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'))
|
|
971
|
-
mod = import_allowed_module(module_name, allowed_modules)
|
|
972
|
-
cls = get_allowed_attr(mod, class_name, allow_private_attrs=allow_private_attrs)
|
|
973
|
-
obj = cls(*args, **kwargs)
|
|
974
|
-
handle_id = str(id(obj))
|
|
975
|
-
instances[handle_id] = obj
|
|
976
|
-
return handle_id
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
def handle_call_method(params, instances, *, force_json_markers, torch_allow_copy, allow_private_attrs):
|
|
980
|
-
handle_id = require_str(params, 'handle')
|
|
981
|
-
method_name = require_str(params, 'methodName')
|
|
982
|
-
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
983
|
-
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'))
|
|
984
|
-
if handle_id not in instances:
|
|
985
|
-
raise InstanceHandleError(f'Unknown instance handle: {handle_id}')
|
|
986
|
-
obj = instances[handle_id]
|
|
987
|
-
# A @property / functools.cached_property is read, not called: the generated
|
|
988
|
-
# `get prop()` accessor emits callMethod(handle, name, []). Classify before
|
|
989
|
-
# touching the value (so cached_property is detected on its first read) and
|
|
990
|
-
# return the attribute directly; everything else is a bound method to call.
|
|
991
|
-
if is_accessor_attr(obj, method_name):
|
|
992
|
-
# An accessor is read, never called: a generated `get prop()` always
|
|
993
|
-
# sends empty args. Reject a malformed request that supplies any so it
|
|
994
|
-
# fails loudly instead of silently dropping the arguments.
|
|
995
|
-
if args or kwargs:
|
|
996
|
-
raise ProtocolError(f'Accessor {method_name!r} does not accept arguments')
|
|
997
|
-
res = get_allowed_attr(obj, method_name, allow_private_attrs=allow_private_attrs)
|
|
998
|
-
else:
|
|
999
|
-
func = get_allowed_attr(obj, method_name, allow_private_attrs=allow_private_attrs)
|
|
1000
|
-
res = func(*args, **kwargs)
|
|
1001
|
-
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
def handle_dispose_instance(params, instances):
|
|
1005
|
-
handle_id = require_str(params, 'handle')
|
|
1006
|
-
if handle_id not in instances:
|
|
1007
|
-
return False
|
|
1008
|
-
del instances[handle_id]
|
|
1009
|
-
return True
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
1022
|
def build_meta(
|
|
1013
|
-
instances,
|
|
1014
1023
|
*,
|
|
1015
1024
|
bridge,
|
|
1016
1025
|
pid,
|
|
1017
1026
|
python_version,
|
|
1018
1027
|
codec_fallback,
|
|
1019
1028
|
arrow_available_override=None,
|
|
1020
|
-
transport_info=None,
|
|
1021
1029
|
):
|
|
1022
1030
|
"""
|
|
1023
1031
|
Build the bridge metadata payload.
|
|
@@ -1031,13 +1039,6 @@ def build_meta(
|
|
|
1031
1039
|
instead of probing pyarrow. The Pyodide server forces markers to JSON
|
|
1032
1040
|
unconditionally, so it advertises arrowAvailable=False regardless of whether
|
|
1033
1041
|
pyarrow happens to be importable in the WASM environment.
|
|
1034
|
-
|
|
1035
|
-
transport_info: optional chunked-transport negotiation block (BridgeInfo
|
|
1036
|
-
.transport). Core stays oblivious to framing policy -- it only echoes what
|
|
1037
|
-
the I/O layer tells it. The subprocess server passes a {'frameProtocol',
|
|
1038
|
-
'supportsChunking', 'maxFrameBytes'} dict when chunking is negotiated; the
|
|
1039
|
-
Pyodide server passes None (single-frame, in-memory). When None the block is
|
|
1040
|
-
omitted entirely (backward compatible: old bridges never emit it).
|
|
1041
1042
|
"""
|
|
1042
1043
|
arrow = arrow_available() if arrow_available_override is None else arrow_available_override
|
|
1043
1044
|
meta = {
|
|
@@ -1051,16 +1052,13 @@ def build_meta(
|
|
|
1051
1052
|
'scipyAvailable': module_available('scipy'),
|
|
1052
1053
|
'torchAvailable': module_available('torch'),
|
|
1053
1054
|
'sklearnAvailable': module_available('sklearn'),
|
|
1054
|
-
'instances':
|
|
1055
|
+
'instances': 0,
|
|
1055
1056
|
}
|
|
1056
|
-
if transport_info is not None:
|
|
1057
|
-
meta['transport'] = transport_info
|
|
1058
1057
|
return meta
|
|
1059
1058
|
|
|
1060
1059
|
|
|
1061
1060
|
def dispatch_request(
|
|
1062
1061
|
msg,
|
|
1063
|
-
instances,
|
|
1064
1062
|
*,
|
|
1065
1063
|
bridge,
|
|
1066
1064
|
pid,
|
|
@@ -1071,7 +1069,7 @@ def dispatch_request(
|
|
|
1071
1069
|
arrow_available_override=None,
|
|
1072
1070
|
allowed_modules=None,
|
|
1073
1071
|
allow_private_attrs=False,
|
|
1074
|
-
|
|
1072
|
+
has_envelope_markers=True,
|
|
1075
1073
|
):
|
|
1076
1074
|
"""
|
|
1077
1075
|
Validate and route a request, returning the fully-serialized response dict
|
|
@@ -1083,7 +1081,7 @@ def dispatch_request(
|
|
|
1083
1081
|
the final encode_value() call, which the caller performs.
|
|
1084
1082
|
|
|
1085
1083
|
allowed_modules: None (default) disables the import allowlist so existing
|
|
1086
|
-
behavior is preserved. Supplying a set restricts call
|
|
1084
|
+
behavior is preserved. Supplying a set restricts call imports to
|
|
1087
1085
|
those modules (plus the stdlib the bridge itself needs) and raises
|
|
1088
1086
|
ImportNotAllowedError otherwise. allow_private_attrs=False (default) blocks
|
|
1089
1087
|
getattr of underscore-prefixed names; True restores unrestricted access. See
|
|
@@ -1101,34 +1099,19 @@ def dispatch_request(
|
|
|
1101
1099
|
torch_allow_copy=torch_allow_copy,
|
|
1102
1100
|
allowed_modules=allowed_modules,
|
|
1103
1101
|
allow_private_attrs=allow_private_attrs,
|
|
1102
|
+
has_envelope_markers=has_envelope_markers,
|
|
1104
1103
|
)
|
|
1105
|
-
elif method == 'instantiate':
|
|
1106
|
-
result = handle_instantiate(
|
|
1107
|
-
params, instances, allowed_modules=allowed_modules, allow_private_attrs=allow_private_attrs
|
|
1108
|
-
)
|
|
1109
|
-
elif method == 'call_method':
|
|
1110
|
-
result = handle_call_method(
|
|
1111
|
-
params,
|
|
1112
|
-
instances,
|
|
1113
|
-
force_json_markers=force_json_markers,
|
|
1114
|
-
torch_allow_copy=torch_allow_copy,
|
|
1115
|
-
allow_private_attrs=allow_private_attrs,
|
|
1116
|
-
)
|
|
1117
|
-
elif method == 'dispose_instance':
|
|
1118
|
-
result = handle_dispose_instance(params, instances)
|
|
1119
1104
|
elif method == 'meta':
|
|
1120
1105
|
if python_version is None:
|
|
1121
1106
|
import sys
|
|
1122
1107
|
python_version = sys.version.split()[0]
|
|
1123
1108
|
codec_fallback = 'json' if force_json_markers else 'none'
|
|
1124
1109
|
result = build_meta(
|
|
1125
|
-
instances,
|
|
1126
1110
|
bridge=bridge,
|
|
1127
1111
|
pid=pid,
|
|
1128
1112
|
python_version=python_version,
|
|
1129
1113
|
codec_fallback=codec_fallback,
|
|
1130
1114
|
arrow_available_override=arrow_available_override,
|
|
1131
|
-
transport_info=transport_info,
|
|
1132
1115
|
)
|
|
1133
1116
|
else:
|
|
1134
1117
|
raise ProtocolError(f'Unknown method: {method}')
|
package/src/config/index.ts
CHANGED
|
@@ -118,6 +118,7 @@ function detectLegacyFields(config: TywrapConfig): void {
|
|
|
118
118
|
const ALLOWED_TOP_LEVEL = new Set([
|
|
119
119
|
'pythonModules',
|
|
120
120
|
'pythonImportPath',
|
|
121
|
+
'contractInput',
|
|
121
122
|
'output',
|
|
122
123
|
'runtime',
|
|
123
124
|
'performance',
|
|
@@ -129,6 +130,7 @@ const VALID_OUTPUT_FORMATS = ['esm', 'cjs', 'both'];
|
|
|
129
130
|
const VALID_COMPRESSION = ['auto', 'gzip', 'brotli', 'none'];
|
|
130
131
|
const VALID_TYPE_HINTS = ['strict', 'loose', 'ignore'];
|
|
131
132
|
const VALID_TYPE_PRESETS = new Set([
|
|
133
|
+
// Accepted as an explicit no-op in 0.9; ndarray shape typing depends on #268.
|
|
132
134
|
'numpy',
|
|
133
135
|
'pandas',
|
|
134
136
|
'pydantic',
|
|
@@ -276,6 +278,15 @@ function validateConfig(config: ResolvedTywrapConfig): void {
|
|
|
276
278
|
throw new Error('pythonImportPath must be an array of strings');
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
if (
|
|
282
|
+
config.contractInput !== undefined &&
|
|
283
|
+
typeof config.contractInput !== 'string' &&
|
|
284
|
+
(!isPlainObject(config.contractInput) ||
|
|
285
|
+
!Object.values(config.contractInput).every(value => typeof value === 'string'))
|
|
286
|
+
) {
|
|
287
|
+
throw new Error('contractInput must be a path string or a record of module paths');
|
|
288
|
+
}
|
|
289
|
+
|
|
279
290
|
validateOutput(config.output);
|
|
280
291
|
validateRuntime(config.runtime);
|
|
281
292
|
validatePerformance(config.performance);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PythonGenericParameter, PythonType } from '../types/index.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface AnnotationParserOptions {
|
|
4
4
|
onUnknownTypeName?: (name: string) => void;
|
|
5
5
|
knownTypeVarNames?: Iterable<string>;
|
|
6
6
|
typeParameters?: readonly PythonGenericParameter[];
|
|
@@ -407,8 +407,7 @@ export function parseAnnotationToPythonType(
|
|
|
407
407
|
const builtInClassMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
|
|
408
408
|
if (builtInClassMatch) {
|
|
409
409
|
const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
|
|
410
|
-
|
|
411
|
-
return mapSimpleName(name);
|
|
410
|
+
return mapSimpleName(inner);
|
|
412
411
|
}
|
|
413
412
|
|
|
414
413
|
if (raw.includes('|')) {
|
|
@@ -506,7 +505,11 @@ export function parseAnnotationToPythonType(
|
|
|
506
505
|
};
|
|
507
506
|
}
|
|
508
507
|
|
|
509
|
-
|
|
508
|
+
const qualified = splitQualifiedName(raw);
|
|
509
|
+
if (qualified.module) {
|
|
510
|
+
return { kind: 'custom', name: qualified.name, module: qualified.module };
|
|
511
|
+
}
|
|
512
|
+
return mapSimpleName(qualified.name);
|
|
510
513
|
};
|
|
511
514
|
|
|
512
515
|
return parse(annotation, 0);
|