dex-python-sdk 0.0.2__cp311-abi3-win_amd64.whl

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 (121) hide show
  1. dex/__init__.py +146 -0
  2. dex/_grpc_errors.py +73 -0
  3. dex/_invocation_context.py +269 -0
  4. dex/_native.pyd +0 -0
  5. dex/_native.pyi +20 -0
  6. dex/_utils.py +18 -0
  7. dex/_value_hydrator.py +223 -0
  8. dex/_value_mapper.py +174 -0
  9. dex/_worker_dispatcher.py +451 -0
  10. dex/_worker_service.py +60 -0
  11. dex/attribute.py +87 -0
  12. dex/blob_cache.py +73 -0
  13. dex/channel.py +165 -0
  14. dex/client.py +715 -0
  15. dex/client_options.py +19 -0
  16. dex/codec.py +286 -0
  17. dex/command_request.py +120 -0
  18. dex/command_results.py +107 -0
  19. dex/communication.py +136 -0
  20. dex/communication_schema.py +54 -0
  21. dex/condition.py +74 -0
  22. dex/context.py +86 -0
  23. dex/data_attributes.py +70 -0
  24. dex/dexpb/__init__.py +1 -0
  25. dex/dexpb/dex_pb2.py +381 -0
  26. dex/dexpb/dex_pb2.pyi +1734 -0
  27. dex/dexpb/dex_pb2_grpc.py +1298 -0
  28. dex/errors.py +109 -0
  29. dex/flow.py +456 -0
  30. dex/flow_config.py +29 -0
  31. dex/flow_info.py +51 -0
  32. dex/flow_options.py +122 -0
  33. dex/object_encoder.py +799 -0
  34. dex/persistence.py +89 -0
  35. dex/persistence_options.py +12 -0
  36. dex/persistence_schema.py +51 -0
  37. dex/py.typed +1 -0
  38. dex/registry.py +204 -0
  39. dex/reset_workflow_type_and_options.py +67 -0
  40. dex/rpc.py +93 -0
  41. dex/runtime_errors.py +81 -0
  42. dex/search_attributes.py +184 -0
  43. dex/state_decision.py +153 -0
  44. dex/state_execution_locals.py +66 -0
  45. dex/state_movement.py +115 -0
  46. dex/state_schema.py +48 -0
  47. dex/step.py +194 -0
  48. dex/step_execution.py +42 -0
  49. dex/stop_workflow_options.py +18 -0
  50. dex/tests/__init__.py +80 -0
  51. dex/tests/dex-service-env/.env +7 -0
  52. dex/tests/dex-service-env/docker-compose-init.sh +44 -0
  53. dex/tests/dex-service-env/docker-compose.yml +97 -0
  54. dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
  55. dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +9 -0
  56. dex/tests/dex-service-env/dynamicconfig/docker.yaml +2 -0
  57. dex/tests/test_abnormal_exit_workflow.py +43 -0
  58. dex/tests/test_basic_workflow.py +70 -0
  59. dex/tests/test_conditional_complete.py +50 -0
  60. dex/tests/test_describe_workflow.py +40 -0
  61. dex/tests/test_empty_data_decodes_properly.py +74 -0
  62. dex/tests/test_internal_channel.py +28 -0
  63. dex/tests/test_internal_channel_with_no_prefix_channel.py +41 -0
  64. dex/tests/test_persistence_data_attributes.py +62 -0
  65. dex/tests/test_persistence_search_attributes.py +127 -0
  66. dex/tests/test_persistence_state_execution_locals.py +38 -0
  67. dex/tests/test_rpc.py +64 -0
  68. dex/tests/test_rpc_with_memo.py +195 -0
  69. dex/tests/test_rpc_with_memo_duplicate_java_tests.py +117 -0
  70. dex/tests/test_signal.py +51 -0
  71. dex/tests/test_skip_wait_until.py +76 -0
  72. dex/tests/test_state_failure_recovery.py +28 -0
  73. dex/tests/test_timer.py +35 -0
  74. dex/tests/test_wait_for_state_execution_completion.py +53 -0
  75. dex/tests/test_workflow_errors.py +87 -0
  76. dex/tests/test_workflow_state_options.py +118 -0
  77. dex/tests/test_workflow_state_options_override.py +44 -0
  78. dex/tests/worker_server.py +64 -0
  79. dex/tests/workflows/abnormal_exit_workflow.py +42 -0
  80. dex/tests/workflows/basic_workflow.py +62 -0
  81. dex/tests/workflows/conditional_complete_workflow.py +95 -0
  82. dex/tests/workflows/describe_workflow.py +46 -0
  83. dex/tests/workflows/empty_data_workflow.py +45 -0
  84. dex/tests/workflows/internal_channel_workflow.py +129 -0
  85. dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +100 -0
  86. dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +276 -0
  87. dex/tests/workflows/persistence_data_attributes_workflow.py +98 -0
  88. dex/tests/workflows/persistence_search_attributes_workflow.py +159 -0
  89. dex/tests/workflows/persistence_state_execution_local_workflow.py +63 -0
  90. dex/tests/workflows/recovery_workflow.py +82 -0
  91. dex/tests/workflows/rpc_memo_workflow.py +231 -0
  92. dex/tests/workflows/rpc_workflow.py +117 -0
  93. dex/tests/workflows/state_options_override_workflow.py +93 -0
  94. dex/tests/workflows/state_options_workflow.py +84 -0
  95. dex/tests/workflows/timer_workflow.py +46 -0
  96. dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +70 -0
  97. dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +71 -0
  98. dex/tests/workflows/wait_internal_channel_workflow.py +47 -0
  99. dex/tests/workflows/wait_signal_workflow.py +147 -0
  100. dex/timer.py +21 -0
  101. dex/type_store.py +99 -0
  102. dex/unregistered_client.py +585 -0
  103. dex/utils/__init__.py +3 -0
  104. dex/utils/dex_typing.py +25 -0
  105. dex/utils/persistence_utils.py +32 -0
  106. dex/wait.py +49 -0
  107. dex/worker.py +121 -0
  108. dex/worker_options.py +22 -0
  109. dex/worker_service.py +432 -0
  110. dex/workflow.py +79 -0
  111. dex/workflow_context.py +44 -0
  112. dex/workflow_info.py +16 -0
  113. dex/workflow_options.py +74 -0
  114. dex/workflow_state.py +123 -0
  115. dex/workflow_state_options.py +154 -0
  116. dex_python_sdk-0.0.2.dist-info/METADATA +202 -0
  117. dex_python_sdk-0.0.2.dist-info/RECORD +121 -0
  118. dex_python_sdk-0.0.2.dist-info/WHEEL +4 -0
  119. dex_python_sdk-0.0.2.dist-info/licenses/LEGACY_NOTICES.md +61 -0
  120. dex_python_sdk-0.0.2.dist-info/licenses/LICENSE +192 -0
  121. dex_python_sdk-0.0.2.dist-info/sboms/dex-blob-cache-python.cyclonedx.json +2406 -0
dex/object_encoder.py ADDED
@@ -0,0 +1,799 @@
1
+ # Legacy Materials in this file remain under their original licenses.
2
+ # See LEGACY_NOTICES.md.
3
+
4
+ # Modifications Copyright (c) 2026 Super Durable, Inc.
5
+ #
6
+ # Modifications after the Legacy Cutoff are licensed under the
7
+ # Super Durable Source License 1.0.
8
+ # Legacy Materials remain under their original licenses.
9
+ # See LICENSE and LEGACY_NOTICES.md.
10
+
11
+ """Base converter and implementations for data conversion.
12
+ Adapted from https://github.com/temporalio/sdk-python/blob/main/temporalio/converter.py
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import collections
18
+ import collections.abc
19
+ import dataclasses
20
+ import inspect
21
+ import json
22
+ import sys
23
+ import uuid
24
+ import warnings
25
+ from abc import ABC, abstractmethod
26
+ from dataclasses import dataclass
27
+ from enum import IntEnum
28
+ from typing import (
29
+ Any,
30
+ ClassVar,
31
+ Dict,
32
+ Mapping,
33
+ NewType,
34
+ Optional,
35
+ Sequence,
36
+ Tuple,
37
+ Type,
38
+ TypeVar,
39
+ Union,
40
+ get_type_hints,
41
+ )
42
+
43
+ from typing_extensions import Literal
44
+
45
+ from dex.dex_api.models import EncodedObject
46
+ from dex.dex_api.types import UNSET, Unset
47
+
48
+ # StrEnum is available in 3.11+
49
+ if sys.version_info >= (3, 11):
50
+ from enum import StrEnum
51
+
52
+ if sys.version_info >= (3, 10):
53
+ from types import UnionType
54
+
55
+ class PayloadConverter(ABC):
56
+ """Base payload converter to/from payload/value."""
57
+
58
+ @abstractmethod
59
+ def to_payload(
60
+ self,
61
+ value: Any,
62
+ ) -> Union[EncodedObject, Unset]:
63
+ """Encode values into payloads.
64
+
65
+ Args:
66
+ value: value to be converted
67
+
68
+ Returns:
69
+ A boolean to indicate if the payload was converted and the converted value
70
+ or Unset
71
+
72
+ Raises:
73
+ Exception: Any issue during conversion.
74
+ """
75
+ raise NotImplementedError
76
+
77
+ @abstractmethod
78
+ def from_payload(
79
+ self,
80
+ payload: EncodedObject,
81
+ type_hint: Optional[Type] = None,
82
+ ) -> Any:
83
+ """Decode payloads into values.
84
+
85
+ Args:
86
+ payload: Payload to convert to Python values.
87
+ type_hint: Type that are expected if any.
88
+
89
+ Returns:
90
+ payload value
91
+
92
+ Raises:
93
+ Exception: Any issue during conversion.
94
+ """
95
+ raise NotImplementedError
96
+
97
+ class EncodingPayloadConverter(ABC):
98
+ """Base converter for a **known encoding** for use in CompositePayloadConverter."""
99
+
100
+ @property
101
+ @abstractmethod
102
+ def encoding(self) -> Union[str, Unset]:
103
+ """Encoding for the payload this converter works with."""
104
+ raise NotImplementedError
105
+
106
+ @abstractmethod
107
+ def to_payload(self, value: Any) -> tuple[bool, Union[EncodedObject, Unset]]:
108
+ """Encode a single value to a payload or None.
109
+
110
+ Args:
111
+ value: Value to be converted.
112
+
113
+ Returns:
114
+ A boolean to indicate if the payload was converted and the converted value
115
+ or Unset
116
+
117
+ Raises:
118
+ TypeError: Value is not the expected type.
119
+ ValueError: Value is of the expected type but otherwise incorrect.
120
+ RuntimeError: General error during encoding.
121
+ """
122
+ raise NotImplementedError
123
+
124
+ @abstractmethod
125
+ def from_payload(
126
+ self,
127
+ payload: EncodedObject,
128
+ type_hint: Optional[Type] = None,
129
+ ) -> Any:
130
+ """Decode a single payload to a Python value or raise exception.
131
+
132
+ Args:
133
+ payload: Payload to convert to Python value.
134
+ type_hint: Type that is expected if any. This may not have a type if
135
+ there are no annotations on the target.
136
+
137
+ Return:
138
+ The decoded value from the payload. Since the encoding is checked by
139
+ the caller, this should raise an exception if the payload cannot be
140
+ converted.
141
+
142
+ Raises:
143
+ RuntimeError: General error during decoding.
144
+ """
145
+ raise NotImplementedError
146
+
147
+ class CompositePayloadConverter(PayloadConverter):
148
+ """Composite payload converter that delegates to a list of encoding payload converters.
149
+
150
+ Encoding/decoding are attempted on each payload converter successively until
151
+ it succeeds.
152
+
153
+ Attributes:
154
+ converters: List of payload converters to delegate to, in order.
155
+ """
156
+
157
+ converters: Mapping[Union[str, Unset], EncodingPayloadConverter]
158
+
159
+ def __init__(self, *converters: EncodingPayloadConverter) -> None:
160
+ """Initializes the data converter.
161
+
162
+ Args:
163
+ converters: Payload converters to delegate to, in order.
164
+ """
165
+ # Insertion order preserved here since Python 3.7
166
+ self.converters = {c.encoding: c for c in converters}
167
+
168
+ def to_payload(
169
+ self,
170
+ value: Any,
171
+ ) -> Union[EncodedObject, Unset]:
172
+ """Encode values trying each converter.
173
+
174
+ See base class. Always returns the same number of payloads as values.
175
+
176
+ Raises:
177
+ RuntimeError: No known converter
178
+ """
179
+ # We intentionally attempt these serially just in case a stateful
180
+ # converter may rely on the previous values
181
+ payload: Union[EncodedObject, Unset] = Unset()
182
+ is_encoded = False
183
+ for converter in self.converters.values():
184
+ is_encoded, payload = converter.to_payload(value)
185
+ if is_encoded:
186
+ break
187
+ if not is_encoded:
188
+ raise RuntimeError(
189
+ f"Value of type {type(value)} has no known converter",
190
+ )
191
+ return payload
192
+
193
+ def from_payload(
194
+ self,
195
+ payload: EncodedObject,
196
+ type_hint: Optional[Type] = None,
197
+ ) -> Any:
198
+ """Decode values trying each converter.
199
+
200
+ See base class. Always returns the same number of values as payloads.
201
+
202
+ Raises:
203
+ KeyError: Unknown payload encoding
204
+ RuntimeError: Error during decode
205
+ """
206
+ encoding = payload.encoding
207
+ assert isinstance(encoding, (str, Unset))
208
+ converter = self.converters.get(encoding)
209
+ if converter is None:
210
+ raise KeyError(f"Unknown payload encoding {encoding}")
211
+ try:
212
+ value = converter.from_payload(payload, type_hint)
213
+ except RuntimeError as err:
214
+ raise RuntimeError(
215
+ f"Payload with encoding {encoding} could not be converted",
216
+ ) from err
217
+ return value
218
+
219
+ class DefaultPayloadConverter(CompositePayloadConverter):
220
+ """Default payload converter compatible with other Temporal SDKs.
221
+
222
+ This handles None, bytes, all protobuf message types, and any type that
223
+ :py:func:`json.dump` accepts. A singleton instance of this is available at
224
+ :py:attr:`PayloadConverter.default`.
225
+ """
226
+
227
+ default_encoding_payload_converters: Tuple[EncodingPayloadConverter, ...]
228
+ """Default set of encoding payload converters the default payload converter
229
+ uses.
230
+ """
231
+
232
+ def __init__(self) -> None:
233
+ """Create a default payload converter."""
234
+ super().__init__(*DefaultPayloadConverter.default_encoding_payload_converters)
235
+
236
+ class UnsetPayloadConverter(EncodingPayloadConverter):
237
+ """Converter for 'unset' payloads supporting None values."""
238
+
239
+ @property
240
+ def encoding(self) -> Union[str, Unset]:
241
+ """See base class."""
242
+ return UNSET
243
+
244
+ def to_payload(self, value: Any) -> tuple[bool, Union[EncodedObject, Unset]]:
245
+ """See base class."""
246
+ if value is None:
247
+ return (True, UNSET)
248
+ return (False, UNSET)
249
+
250
+ def from_payload(
251
+ self,
252
+ payload: EncodedObject,
253
+ type_hint: Optional[Type] = None,
254
+ ) -> Any:
255
+ """See base class."""
256
+ if isinstance(payload.data, str) and len(payload.data) > 0:
257
+ raise RuntimeError("Expected empty data set for binary/null")
258
+ return None
259
+
260
+ class BinaryNullPayloadConverter(UnsetPayloadConverter):
261
+ """Converter for 'binary/null' payloads supporting None values."""
262
+
263
+ @property
264
+ def encoding(self) -> Union[str, Unset]:
265
+ """See base class."""
266
+ return "binary/null"
267
+
268
+ class BinaryPlainPayloadConverter(EncodingPayloadConverter):
269
+ """Converter for 'binary/plain' payloads supporting bytes values."""
270
+
271
+ @property
272
+ def encoding(self) -> Union[str, Unset]:
273
+ """See base class."""
274
+ return "binary/plain"
275
+
276
+ def to_payload(self, value: Any) -> tuple[bool, Union[EncodedObject, Unset]]:
277
+ """See base class."""
278
+ if isinstance(value, bytes):
279
+ return (
280
+ True,
281
+ EncodedObject(
282
+ encoding=self.encoding,
283
+ data=str(value),
284
+ ),
285
+ )
286
+ return (False, UNSET)
287
+
288
+ def from_payload(
289
+ self,
290
+ payload: EncodedObject,
291
+ type_hint: Optional[Type] = None,
292
+ ) -> Any:
293
+ """See base class."""
294
+ return payload.data
295
+
296
+ class AdvancedJSONEncoder(json.JSONEncoder):
297
+ """Advanced JSON encoder.
298
+
299
+ This encoder supports dataclasses, classes with dict() functions, and
300
+ all iterables as lists.
301
+ """
302
+
303
+ def default(self, o: Any) -> Any:
304
+ """Override JSON encoding default.
305
+
306
+ See :py:meth:`json.JSONEncoder.default`.
307
+ """
308
+ # Dataclass support
309
+ if dataclasses.is_dataclass(o) and not isinstance(o, type):
310
+ return dataclasses.asdict(o)
311
+ # Support for models with "dict" function like Pydantic
312
+ dict_fn = getattr(o, "dict", None)
313
+ if callable(dict_fn):
314
+ return dict_fn()
315
+ # Support for non-list iterables like set
316
+ if not isinstance(o, list) and isinstance(o, collections.abc.Iterable):
317
+ return list(o)
318
+ # Support for UUID
319
+ if isinstance(o, uuid.UUID):
320
+ return str(o)
321
+ return super().default(o)
322
+
323
+ class JSONPlainPayloadConverter(EncodingPayloadConverter):
324
+ """Converter for 'json/plain' payloads supporting common Python values.
325
+
326
+ For encoding, this supports all values that :py:func:`json.dump` supports
327
+ and by default adds extra encoding support for dataclasses, classes with
328
+ ``dict()`` methods, and all iterables.
329
+
330
+ For decoding, this uses type hints to attempt to rebuild the type from the
331
+ type hint.
332
+ """
333
+
334
+ _encoder: Optional[Type[json.JSONEncoder]]
335
+ _decoder: Optional[Type[json.JSONDecoder]]
336
+ _encoding: str
337
+
338
+ def __init__(
339
+ self,
340
+ *,
341
+ encoder: Optional[Type[json.JSONEncoder]] = AdvancedJSONEncoder,
342
+ decoder: Optional[Type[json.JSONDecoder]] = None,
343
+ encoding: str = "json/plain",
344
+ custom_type_converters: Sequence[JSONTypeConverter] = [],
345
+ ) -> None:
346
+ """Initialize a JSON data converter.
347
+
348
+ Args:
349
+ encoder: Custom encoder class object to use.
350
+ decoder: Custom decoder class object to use.
351
+ encoding: Encoding name to use.
352
+ custom_type_converters: Set of custom type converters that are used
353
+ when converting from a payload to type-hinted values.
354
+ """
355
+ super().__init__()
356
+ self._encoder = encoder
357
+ self._decoder = decoder
358
+ self._encoding = encoding
359
+ self._custom_type_converters = custom_type_converters
360
+
361
+ @property
362
+ def encoding(self) -> Union[str, Unset]:
363
+ """See base class."""
364
+ return self._encoding
365
+
366
+ def to_payload(self, value: Any) -> tuple[bool, Union[EncodedObject, Unset]]:
367
+ """See base class."""
368
+ # Check for pydantic then send warning
369
+ if hasattr(value, "parse_obj"):
370
+ warnings.warn(
371
+ "If you're using pydantic model, refer to "
372
+ "https://github.com/temporalio/examples/python/tree/main/pydantic_converter for better support",
373
+ )
374
+ # We let JSON conversion errors be thrown to caller
375
+ return (
376
+ True,
377
+ EncodedObject(
378
+ encoding=self.encoding,
379
+ data=json.dumps(
380
+ value,
381
+ cls=self._encoder,
382
+ separators=(",", ":"),
383
+ sort_keys=True,
384
+ ),
385
+ ),
386
+ )
387
+
388
+ def from_payload(
389
+ self,
390
+ payload: EncodedObject,
391
+ type_hint: Optional[Type] = None,
392
+ ) -> Any:
393
+ """See base class."""
394
+ try:
395
+ if isinstance(payload.data, str):
396
+ obj = json.loads(payload.data, cls=self._decoder)
397
+ if type_hint:
398
+ obj = value_to_type(type_hint, obj, self._custom_type_converters)
399
+ return obj
400
+ else:
401
+ return None
402
+ except json.JSONDecodeError as err:
403
+ raise RuntimeError("Failed parsing") from err
404
+
405
+ _JSONTypeConverterUnhandled = NewType("_JSONTypeConverterUnhandled", object)
406
+
407
+ class JSONTypeConverter(ABC):
408
+ """Converter for converting an object from Python :py:func:`json.loads`
409
+ result (e.g. scalar, list, or dict) to a known type.
410
+ """
411
+
412
+ Unhandled = _JSONTypeConverterUnhandled(object())
413
+ """Sentinel value that must be used as the result of
414
+ :py:meth:`to_typed_value` to say the given type is not handled by this
415
+ converter."""
416
+
417
+ @abstractmethod
418
+ def to_typed_value(
419
+ self,
420
+ hint: Type,
421
+ value: Any,
422
+ ) -> Union[Optional[Any], _JSONTypeConverterUnhandled]:
423
+ """Convert the given value to a type based on the given hint.
424
+
425
+ Args:
426
+ hint: Type hint to use to help in converting the value.
427
+ value: Value as returned by :py:func:`json.loads`. Usually a scalar,
428
+ list, or dict.
429
+
430
+ Returns:
431
+ The converted value or :py:attr:`Unhandled` if this converter does
432
+ not handle this situation.
433
+ """
434
+ raise NotImplementedError
435
+
436
+ class PayloadCodec(ABC):
437
+ """Codec for encoding/decoding to/from bytes.
438
+
439
+ Commonly used for compression or encryption.
440
+ """
441
+
442
+ @abstractmethod
443
+ def encode(
444
+ self,
445
+ payload: Union[EncodedObject, Unset],
446
+ ) -> EncodedObject:
447
+ """Encode the given payloads.
448
+
449
+ Args:
450
+ payload: Payloads to encode. This value should not be mutated.
451
+
452
+ Returns:
453
+ Encoded payloads. Note, this does not have to be the same number as
454
+ payloads given, but must be at least one and cannot be more than was
455
+ given.
456
+ """
457
+ raise NotImplementedError
458
+
459
+ @abstractmethod
460
+ def decode(
461
+ self,
462
+ payload: EncodedObject,
463
+ ) -> EncodedObject:
464
+ """Decode the given payloads.
465
+
466
+ Args:
467
+ payload: Payloads to decode. This value should not be mutated.
468
+
469
+ Returns:
470
+ Decoded payloads. Note, this does not have to be the same number as
471
+ payloads given, but must be at least one and cannot be more than was
472
+ given.
473
+ """
474
+ raise NotImplementedError
475
+
476
+ @dataclass(frozen=True)
477
+ class ObjectEncoder:
478
+ """Object Encoder for converting and encoding payloads to/from Python values.
479
+
480
+ This combines :py:class:`PayloadConverter` which converts values with
481
+ :py:class:`PayloadCodec` which encodes bytes.
482
+ """
483
+
484
+ payload_converter_class: Type[PayloadConverter] = DefaultPayloadConverter
485
+ """Class to instantiate for payload conversion."""
486
+
487
+ payload_codec: Optional[PayloadCodec] = None
488
+ """Optional codec for encoding payload bytes."""
489
+
490
+ payload_converter: PayloadConverter = dataclasses.field(init=False)
491
+ """Payload converter created from the :py:attr:`payload_converter_class`."""
492
+
493
+ default: ClassVar[ObjectEncoder]
494
+ """Singleton default data converter."""
495
+
496
+ def __post_init__(self) -> None: # noqa: D105
497
+ object.__setattr__(self, "payload_converter", self.payload_converter_class())
498
+
499
+ def encode(
500
+ self,
501
+ value: Any,
502
+ ) -> Union[EncodedObject, Unset]:
503
+ """Encode values into payloads.
504
+
505
+ First converts values to payload then encodes payload using codec.
506
+
507
+ Args:
508
+ value: Values to be converted and encoded.
509
+
510
+ Returns:
511
+ Converted and encoded payload.
512
+ """
513
+ payload = self.payload_converter.to_payload(value)
514
+ if self.payload_codec:
515
+ payload = self.payload_codec.encode(payload)
516
+ return payload
517
+
518
+ def decode(
519
+ self,
520
+ payload: Union[Optional[EncodedObject], Unset],
521
+ type_hint: Optional[Type] = None,
522
+ ) -> Any:
523
+ """Decode payloads into values.
524
+
525
+ First decodes payloads using codec then converts payloads to values.
526
+
527
+ Args:
528
+ type_hint: type to decode to
529
+ payload: Payload to be decoded and converted.
530
+
531
+ Returns:
532
+ Decoded and converted value.
533
+ """
534
+ if payload is None or isinstance(payload, Unset):
535
+ return None
536
+ if self.payload_codec:
537
+ payload = self.payload_codec.decode(payload)
538
+ return self.payload_converter.from_payload(payload, type_hint)
539
+
540
+ DefaultPayloadConverter.default_encoding_payload_converters = (
541
+ UnsetPayloadConverter(),
542
+ BinaryNullPayloadConverter(),
543
+ BinaryPlainPayloadConverter(),
544
+ JSONPlainPayloadConverter(),
545
+ )
546
+
547
+ ObjectEncoder.default = ObjectEncoder()
548
+
549
+ def value_to_type(
550
+ hint: Type,
551
+ value: Any,
552
+ custom_converters,
553
+ ) -> Any:
554
+ """Convert a given value to the given type hint.
555
+
556
+ This is used internally to convert a raw JSON loaded value to a specific
557
+ type hint.
558
+
559
+ Args:
560
+ hint: Type hint to convert the value to.
561
+ value: Raw value (e.g. primitive, dict, or list) to convert from.
562
+ custom_converters: Set of custom converters to try before doing default
563
+ conversion. Converters are tried in order and the first value that
564
+ is not :py:attr:`JSONTypeConverter.Unhandled` will be returned from
565
+ this function instead of doing default behavior.
566
+
567
+ Returns:
568
+ Converted value.
569
+
570
+ Raises:
571
+ TypeError: Unable to convert to the given hint.
572
+ """
573
+ # Try custom converters
574
+ if custom_converters is None:
575
+ custom_converters = []
576
+ for conv in custom_converters:
577
+ ret = conv.to_typed_value(hint, value)
578
+ if ret is not JSONTypeConverter.Unhandled:
579
+ return ret
580
+
581
+ # Any or primitives
582
+ if hint is Any:
583
+ return value
584
+ elif hint is int or hint is float:
585
+ if not isinstance(value, (int, float)):
586
+ raise TypeError(f"Expected value to be int|float, was {type(value)}")
587
+ return hint(value)
588
+ elif hint is bool:
589
+ if not isinstance(value, bool):
590
+ raise TypeError(f"Expected value to be bool, was {type(value)}")
591
+ return bool(value)
592
+ elif hint is str:
593
+ if not isinstance(value, str):
594
+ raise TypeError(f"Expected value to be str, was {type(value)}")
595
+ return str(value)
596
+ elif hint is bytes:
597
+ if not isinstance(value, (str, bytes, list)):
598
+ raise TypeError(f"Expected value to be bytes, was {type(value)}")
599
+ # In some other SDKs, this is serialized as a base64 string, but in
600
+ # Python this is a numeric array.
601
+ return bytes(value) # type: ignore
602
+ elif hint is type(None):
603
+ if value is not None:
604
+ raise TypeError(f"Expected None, got value of type {type(value)}")
605
+ return None
606
+
607
+ # NewType. Note we cannot simply check isinstance NewType here because it's
608
+ # only been a class since 3.10. Instead we'll just check for the presence
609
+ # of a supertype.
610
+ supertype = getattr(hint, "__supertype__", None)
611
+ if supertype:
612
+ return value_to_type(supertype, value, custom_converters)
613
+
614
+ # Load origin for other checks
615
+ origin = getattr(hint, "__origin__", hint)
616
+ type_args: Tuple = getattr(hint, "__args__", ())
617
+
618
+ # Literal
619
+ if origin is Literal:
620
+ if value not in type_args:
621
+ raise TypeError(f"Value {value} not in literal values {type_args}")
622
+ return value
623
+
624
+ is_union = origin is Union
625
+ if sys.version_info >= (3, 10):
626
+ is_union = is_union or isinstance(origin, UnionType)
627
+
628
+ # Union
629
+ if is_union:
630
+ # Try each one. Note, Optional is just a union w/ none.
631
+ for arg in type_args:
632
+ try:
633
+ return value_to_type(arg, value, custom_converters)
634
+ except Exception:
635
+ pass
636
+ raise TypeError(f"Failed converting to {hint} from {value}")
637
+
638
+ # Mapping
639
+ if inspect.isclass(origin) and issubclass(origin, collections.abc.Mapping):
640
+ if not isinstance(value, collections.abc.Mapping):
641
+ raise TypeError(f"Expected {hint}, value was {type(value)}")
642
+ ret_dict = {}
643
+ # If there are required or optional keys that means we are a TypedDict
644
+ # and therefore can extract per-key types
645
+ per_key_types: Optional[Dict[str, Type]] = None
646
+ if getattr(origin, "__required_keys__", None) or getattr(
647
+ origin,
648
+ "__optional_keys__",
649
+ None,
650
+ ):
651
+ per_key_types = get_type_hints(origin)
652
+ key_type = (
653
+ type_args[0]
654
+ if len(type_args) > 0
655
+ and type_args[0] is not Any
656
+ and not isinstance(type_args[0], TypeVar)
657
+ else None
658
+ )
659
+ value_type = (
660
+ type_args[1]
661
+ if len(type_args) > 1
662
+ and type_args[1] is not Any
663
+ and not isinstance(type_args[1], TypeVar)
664
+ else None
665
+ )
666
+ # Convert each key/value
667
+ for key, value in value.items():
668
+ if key_type:
669
+ try:
670
+ key = value_to_type(key_type, key, custom_converters)
671
+ except Exception as err:
672
+ raise TypeError(f"Failed converting key {key} on {hint}") from err
673
+ # If there are per-key types, use it instead of single type
674
+ this_value_type = value_type
675
+ if per_key_types:
676
+ # TODO(cretz): Strict mode would fail an unknown key
677
+ this_value_type = per_key_types.get(key)
678
+ if this_value_type:
679
+ try:
680
+ value = value_to_type(this_value_type, value, custom_converters)
681
+ except Exception as err:
682
+ raise TypeError(
683
+ f"Failed converting value for key {key} on {hint}",
684
+ ) from err
685
+ ret_dict[key] = value
686
+ # If there are per-key types, it's a typed dict and we want to attempt
687
+ # instantiation to get its validation
688
+ if per_key_types:
689
+ ret_dict = hint(**ret_dict)
690
+ return ret_dict
691
+
692
+ # Dataclass
693
+ if dataclasses.is_dataclass(hint):
694
+ if not isinstance(value, dict):
695
+ raise TypeError(
696
+ f"Cannot convert to dataclass {hint}, value is {type(value)} not dict",
697
+ )
698
+ # Obtain dataclass fields and check that all dict fields are there and
699
+ # that no required fields are missing. Unknown fields are silently
700
+ # ignored.
701
+ fields = dataclasses.fields(hint)
702
+ field_hints = get_type_hints(hint)
703
+ field_values = {}
704
+ for field in fields:
705
+ field_value = value.get(field.name, dataclasses.MISSING)
706
+ # We do not check whether field is required here. Rather, we let the
707
+ # attempted instantiation of the dataclass raise if a field is
708
+ # missing
709
+ if field_value is not dataclasses.MISSING:
710
+ try:
711
+ field_values[field.name] = value_to_type(
712
+ field_hints[field.name],
713
+ field_value,
714
+ custom_converters,
715
+ )
716
+ except Exception as err:
717
+ raise TypeError(
718
+ f"Failed converting field {field.name} on dataclass {hint}",
719
+ ) from err
720
+ # Simply instantiate the dataclass. This will fail as expected when
721
+ # missing required fields.
722
+ # TODO(cretz): Want way to convert snake case to camel case?
723
+ return hint(**field_values)
724
+
725
+ # If there is a @staticmethod or @classmethod parse_obj, we will use it.
726
+ # This covers Pydantic models.
727
+ parse_obj_attr = inspect.getattr_static(hint, "parse_obj", None)
728
+ if isinstance(parse_obj_attr, classmethod) or isinstance(
729
+ parse_obj_attr,
730
+ staticmethod,
731
+ ):
732
+ if not isinstance(value, dict):
733
+ raise TypeError(
734
+ f"Cannot convert to {hint}, value is {type(value)} not dict",
735
+ )
736
+ return getattr(hint, "parse_obj")(value)
737
+
738
+ # IntEnum
739
+ if inspect.isclass(hint) and issubclass(hint, IntEnum):
740
+ if not isinstance(value, int):
741
+ raise TypeError(
742
+ f"Cannot convert to enum {hint}, value not an integer, value is {type(value)}",
743
+ )
744
+ return hint(value)
745
+
746
+ # StrEnum, available in 3.11+
747
+ if sys.version_info >= (3, 11):
748
+ if inspect.isclass(hint) and issubclass(hint, StrEnum):
749
+ if not isinstance(value, str):
750
+ raise TypeError(
751
+ f"Cannot convert to enum {hint}, value not a string, value is {type(value)}",
752
+ )
753
+ return hint(value)
754
+
755
+ # UUID
756
+ if inspect.isclass(hint) and issubclass(hint, uuid.UUID):
757
+ return hint(value)
758
+
759
+ # Iterable. We intentionally put this last as it catches several others.
760
+ if inspect.isclass(origin) and issubclass(origin, collections.abc.Iterable):
761
+ if not isinstance(value, collections.abc.Iterable):
762
+ raise TypeError(f"Expected {hint}, value was {type(value)}")
763
+ ret_list = []
764
+ # If there is no type arg, just return value as is
765
+ if not type_args or (
766
+ len(type_args) == 1
767
+ and (isinstance(type_args[0], TypeVar) or type_args[0] is Ellipsis)
768
+ ):
769
+ ret_list = list(value)
770
+ else:
771
+ # Otherwise convert
772
+ for i, item in enumerate(value):
773
+ # Non-tuples use first type arg, tuples use arg set or one
774
+ # before ellipsis if that's set
775
+ if origin is not tuple:
776
+ arg_type = type_args[0]
777
+ elif len(type_args) > i and type_args[i] is not Ellipsis:
778
+ arg_type = type_args[i]
779
+ elif type_args[-1] is Ellipsis:
780
+ # Ellipsis means use the second to last one
781
+ arg_type = type_args[-2]
782
+ else:
783
+ raise TypeError(
784
+ f"Type {hint} only expecting {len(type_args)} values, got at least {i + 1}",
785
+ )
786
+ try:
787
+ ret_list.append(value_to_type(arg_type, item, custom_converters))
788
+ except Exception as err:
789
+ raise TypeError(f"Failed converting {hint} index {i}") from err
790
+ # If tuple, set, or deque convert back to that type
791
+ if origin is tuple:
792
+ return tuple(ret_list)
793
+ elif origin is set:
794
+ return set(ret_list)
795
+ elif origin is collections.deque:
796
+ return collections.deque(ret_list)
797
+ return ret_list
798
+
799
+ raise TypeError(f"Unserializable type during conversion: {hint}")