@@ -47,6 +47,7 @@ event loop. This means task management, sleep, cancellation, etc have all been d
47
47
- [ Usage] ( #usage )
48
48
- [ Client] ( #client )
49
49
- [ Data Conversion] ( #data-conversion )
50
+ - [ Custom Type Data Conversion] ( #custom-type-data-conversion )
50
51
- [ Workers] ( #workers )
51
52
- [ Workflows] ( #workflows )
52
53
- [ Definition] ( #definition )
@@ -268,21 +269,118 @@ The default data converter supports converting multiple types including:
268
269
* Iterables including ones JSON dump may not support by default, e.g. ` set `
269
270
* Any class with a ` dict() ` method and a static ` parse_obj() ` method, e.g.
270
271
[ Pydantic models] ( https://pydantic-docs.helpmanual.io/usage/models )
271
- * Note, this doesn't mean every Pydantic field can be converted, only fields which the data converter supports
272
+ * The default data converter is deprecated for Pydantic models and will warn if used since not all fields work.
273
+ See [ this sample] ( https://github.com/temporalio/samples-python/tree/main/pydantic_converter ) for the recommended
274
+ approach.
272
275
* [ IntEnum, StrEnum] ( https://docs.python.org/3/library/enum.html ) based enumerates
273
276
* [ UUID] ( https://docs.python.org/3/library/uuid.html )
274
277
275
278
This notably doesn't include any ` date ` , ` time ` , or ` datetime ` objects as they may not work across SDKs.
276
279
280
+ Users are strongly encouraged to use a single ` dataclass ` for parameter and return types so fields with defaults can be
281
+ easily added without breaking compatibility.
282
+
277
283
Classes with generics may not have the generics properly resolved. The current implementation, similar to Pydantic, does
278
284
not have generic type resolution. Users should use concrete types.
279
285
286
+ ##### Custom Type Data Conversion
287
+
280
288
For converting from JSON, the workflow/activity type hint is taken into account to convert to the proper type. Care has
281
289
been taken to support all common typings including ` Optional ` , ` Union ` , all forms of iterables and mappings, ` NewType ` ,
282
290
etc in addition to the regular JSON values mentioned before.
283
291
284
- Users are strongly encouraged to use a single ` dataclass ` for parameter and return types so fields with defaults can be
285
- easily added without breaking compatibility.
292
+ Data converters contain a reference to a payload converter class that is used to convert to/from payloads/values. This
293
+ is a class and not an instance because it is instantiated on every workflow run inside the sandbox. The payload
294
+ converter is usually a ` CompositePayloadConverter ` which contains a multiple ` EncodingPayloadConverter ` s it uses to try
295
+ to serialize/deserialize payloads. Upon serialization, each ` EncodingPayloadConverter ` is tried until one succeeds. The
296
+ ` EncodingPayloadConverter ` provides an "encoding" string serialized onto the payload so that, upon deserialization, the
297
+ specific ` EncodingPayloadConverter ` for the given "encoding" is used.
298
+
299
+ The default data converter uses the ` DefaultPayloadConverter ` which is simply a ` CompositePayloadConverter ` with a known
300
+ set of default ` EncodingPayloadConverter ` s. To implement a custom encoding for a custom type, a new
301
+ ` EncodingPayloadConverter ` can be created for the new type. For example, to support ` IPv4Address ` types:
302
+
303
+ ``` python
304
+ class IPv4AddressEncodingPayloadConverter (EncodingPayloadConverter ):
305
+ @ property
306
+ def encoding (self ) -> str :
307
+ return " text/ipv4-address"
308
+
309
+ def to_payload (self , value : Any) -> Optional[Payload]:
310
+ if isinstance (value, ipaddress.IPv4Address):
311
+ return Payload(
312
+ metadata = {" encoding" : self .encoding.encode()},
313
+ data = str (value).encode(),
314
+ )
315
+ else :
316
+ return None
317
+
318
+ def from_payload (self , payload : Payload, type_hint : Optional[Type] = None ) -> Any:
319
+ assert not type_hint or type_hint is ipaddress.IPv4Address
320
+ return ipaddress.IPv4Address(payload.data.decode())
321
+
322
+ class IPv4AddressPayloadConverter (CompositePayloadConverter ):
323
+ def __init__ (self ) -> None :
324
+ # Just add ours as first before the defaults
325
+ super ().__init__ (
326
+ IPv4AddressEncodingPayloadConverter(),
327
+ * DefaultPayloadConverter.default_encoding_payload_converters,
328
+ )
329
+
330
+ my_data_converter = dataclasses.replace(
331
+ DataConverter.default,
332
+ payload_converter_class = IPv4AddressPayloadConverter,
333
+ )
334
+ ```
335
+
336
+ Imports are left off for brevity.
337
+
338
+ This is good for many custom types. However, sometimes you want to override the behavior of the just the existing JSON
339
+ encoding payload converter to support a new type. It is already the last encoding data converter in the list, so it's
340
+ the fall-through behavior for any otherwise unknown type. Customizing the existing JSON converter has the benefit of
341
+ making the type work in lists, unions, etc.
342
+
343
+ The ` JSONPlainPayloadConverter ` uses the Python [ json] ( https://docs.python.org/3/library/json.html ) library with an
344
+ advanced JSON encoder by default and a custom value conversion method to turn ` json.load ` ed values to their type hints.
345
+ The conversion can be customized for serialization with a custom ` json.JSONEncoder ` and deserialization with a custom
346
+ ` JSONTypeConverter ` . For example, to support ` IPv4Address ` types in existing JSON conversion:
347
+
348
+ ``` python
349
+ class IPv4AddressJSONEncoder (AdvancedJSONEncoder ):
350
+ def default (self , o : Any) -> Any:
351
+ if isinstance (o, ipaddress.IPv4Address):
352
+ return str (o)
353
+ return super ().default(o)
354
+ class IPv4AddressJSONTypeConverter (JSONTypeConverter ):
355
+ def to_typed_value (
356
+ self , hint : Type, value : Any
357
+ ) -> Union[Optional[Any], _JSONTypeConverterUnhandled]:
358
+ if issubclass (hint, ipaddress.IPv4Address):
359
+ return ipaddress.IPv4Address(value)
360
+ return JSONTypeConverter.Unhandled
361
+
362
+ class IPv4AddressPayloadConverter (CompositePayloadConverter ):
363
+ def __init__ (self ) -> None :
364
+ # Replace default JSON plain with our own that has our encoder and type
365
+ # converter
366
+ json_converter = JSONPlainPayloadConverter(
367
+ encoder = IPv4AddressJSONEncoder,
368
+ custom_type_converters = [IPv4AddressJSONTypeConverter()],
369
+ )
370
+ super ().__init__ (
371
+ * [
372
+ c if not isinstance (c, JSONPlainPayloadConverter) else json_converter
373
+ for c in DefaultPayloadConverter.default_encoding_payload_converters
374
+ ]
375
+ )
376
+
377
+ my_data_converter = dataclasses.replace(
378
+ DataConverter.default,
379
+ payload_converter_class = IPv4AddressPayloadConverter,
380
+ )
381
+ ```
382
+
383
+ Now ` IPv4Address ` can be used in type hints including collections, optionals, etc.
286
384
287
385
### Workers
288
386
0 commit comments