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