77import time
88import uuid
99from collections .abc import AsyncIterable , Iterable , Sequence
10- from dataclasses import dataclass
10+ from dataclasses import dataclass , field
1111from datetime import datetime
1212from enum import Enum
1313from typing import Any , Generic , Protocol , TypeVar , cast , overload
5050)
5151from durabletask .payload import helpers as payload_helpers
5252from durabletask .payload .store import PayloadStore
53+ from durabletask .serialization import DEFAULT_DATA_CONVERTER , DataConverter , JsonDataConverter
5354
5455TInput = TypeVar ('TInput' )
5556TOutput = TypeVar ('TOutput' )
@@ -82,6 +83,12 @@ class OrchestrationState:
8283 serialized_output : str | None
8384 serialized_custom_status : str | None
8485 failure_details : task .FailureDetails | None
86+ # Converter used by the typed accessors below. Defaults to the SDK's JSON
87+ # converter; the client populates it with its own converter so custom
88+ # serialization applies on the read side too. Excluded from equality/repr so
89+ # two states with equal payloads remain equal regardless of converter.
90+ _data_converter : DataConverter = field (
91+ default = DEFAULT_DATA_CONVERTER , compare = False , repr = False )
8592
8693 @overload
8794 def get_input (self , expected_type : type [T ]) -> T | None :
@@ -111,7 +118,7 @@ def get_input(self, expected_type: type | None = None) -> Any:
111118 """
112119 if self .serialized_input is None :
113120 return None
114- return shared . from_json (self .serialized_input , expected_type )
121+ return self . _data_converter . deserialize (self .serialized_input , expected_type )
115122
116123 @overload
117124 def get_output (self , expected_type : type [T ]) -> T | None :
@@ -141,7 +148,7 @@ def get_output(self, expected_type: type | None = None) -> Any:
141148 """
142149 if self .serialized_output is None :
143150 return None
144- return shared . from_json (self .serialized_output , expected_type )
151+ return self . _data_converter . deserialize (self .serialized_output , expected_type )
145152
146153 @overload
147154 def get_custom_status (self , expected_type : type [T ]) -> T | None :
@@ -171,7 +178,7 @@ def get_custom_status(self, expected_type: type | None = None) -> Any:
171178 """
172179 if self .serialized_custom_status is None :
173180 return None
174- return shared . from_json (self .serialized_custom_status , expected_type )
181+ return self . _data_converter . deserialize (self .serialized_custom_status , expected_type )
175182
176183 def raise_if_failed (self ):
177184 if self .failure_details is not None :
@@ -229,18 +236,22 @@ def failure_details(self):
229236 return self ._failure_details
230237
231238
232- def new_orchestration_state (instance_id : str , res : pb .GetInstanceResponse ) -> OrchestrationState | None :
239+ def new_orchestration_state (
240+ instance_id : str , res : pb .GetInstanceResponse ,
241+ data_converter : DataConverter | None = None ) -> OrchestrationState | None :
233242 if not res .exists :
234243 return None
235244
236245 state = res .orchestrationState
237246
238- new_state = parse_orchestration_state (state )
247+ new_state = parse_orchestration_state (state , data_converter )
239248 new_state .instance_id = instance_id # Override instance_id with the one from the request, to match old behavior
240249 return new_state
241250
242251
243- def parse_orchestration_state (state : pb .OrchestrationState ) -> OrchestrationState :
252+ def parse_orchestration_state (
253+ state : pb .OrchestrationState ,
254+ data_converter : DataConverter | None = None ) -> OrchestrationState :
244255 failure_details = None
245256 if state .failureDetails .errorMessage != '' or state .failureDetails .errorType != '' :
246257 failure_details = task .FailureDetails (
@@ -257,7 +268,8 @@ def parse_orchestration_state(state: pb.OrchestrationState) -> OrchestrationStat
257268 state .input .value if not helpers .is_empty (state .input ) else None ,
258269 state .output .value if not helpers .is_empty (state .output ) else None ,
259270 state .customStatus .value if not helpers .is_empty (state .customStatus ) else None ,
260- failure_details )
271+ failure_details ,
272+ data_converter if data_converter is not None else DEFAULT_DATA_CONVERTER )
261273
262274
263275# Grace period before a retired SDK-owned channel is force-closed. Long enough
@@ -400,9 +412,11 @@ def __init__(self, *,
400412 channel_options : GrpcChannelOptions | None = None ,
401413 resiliency_options : GrpcClientResiliencyOptions | None = None ,
402414 default_version : str | None = None ,
403- payload_store : PayloadStore | None = None ):
415+ payload_store : PayloadStore | None = None ,
416+ data_converter : DataConverter | None = None ):
404417
405418 self ._owns_channel = channel is None
419+ self ._data_converter = data_converter if data_converter is not None else JsonDataConverter ()
406420 self ._host_address = (
407421 host_address if host_address else shared .get_default_host_address ()
408422 )
@@ -605,7 +619,8 @@ def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInput, TOu
605619 req = build_schedule_new_orchestration_req (
606620 orchestrator , input = input , instance_id = instance_id , start_at = start_at ,
607621 reuse_id_policy = reuse_id_policy , tags = tags ,
608- version = version if version else self .default_version )
622+ version = version if version else self .default_version ,
623+ data_converter = self ._data_converter )
609624
610625 # Inject the active PRODUCER span context into the request so the sidecar
611626 # stores it in the executionStarted event and the worker can parent all
@@ -629,7 +644,7 @@ def get_orchestration_state(self, instance_id: str, *, fetch_payloads: bool = Tr
629644 # De-externalize any large-payload tokens in the response
630645 if self ._payload_store is not None and res .exists :
631646 payload_helpers .deexternalize_payloads (res , self ._payload_store )
632- return new_orchestration_state (req .instanceId , res )
647+ return new_orchestration_state (req .instanceId , res , self . _data_converter )
633648
634649 def get_orchestration_history (self ,
635650 instance_id : str , * ,
@@ -685,7 +700,7 @@ def get_all_orchestration_states(self,
685700 resp : pb .QueryInstancesResponse = self ._stub .QueryInstances (req )
686701 if self ._payload_store is not None :
687702 payload_helpers .deexternalize_payloads (resp , self ._payload_store )
688- states += [parse_orchestration_state (res ) for res in resp .orchestrationState ]
703+ states += [parse_orchestration_state (res , self . _data_converter ) for res in resp .orchestrationState ]
689704 if check_continuation_token (resp .continuationToken , _continuation_token , self ._logger ):
690705 _continuation_token = resp .continuationToken
691706 else :
@@ -705,7 +720,7 @@ def wait_for_orchestration_start(self, instance_id: str, *,
705720 )
706721 if self ._payload_store is not None and res .exists :
707722 payload_helpers .deexternalize_payloads (res , self ._payload_store )
708- return new_orchestration_state (req .instanceId , res )
723+ return new_orchestration_state (req .instanceId , res , self . _data_converter )
709724 except grpc .RpcError as rpc_error :
710725 if rpc_error .code () == grpc .StatusCode .DEADLINE_EXCEEDED : # type: ignore
711726 # Replace gRPC error with the built-in TimeoutError
@@ -725,7 +740,7 @@ def wait_for_orchestration_completion(self, instance_id: str, *,
725740 )
726741 if self ._payload_store is not None and res .exists :
727742 payload_helpers .deexternalize_payloads (res , self ._payload_store )
728- state = new_orchestration_state (req .instanceId , res )
743+ state = new_orchestration_state (req .instanceId , res , self . _data_converter )
729744 log_completion_state (self ._logger , instance_id , state )
730745 return state
731746 except grpc .RpcError as rpc_error :
@@ -737,7 +752,7 @@ def wait_for_orchestration_completion(self, instance_id: str, *,
737752 def raise_orchestration_event (self , instance_id : str , event_name : str , * ,
738753 data : Any | None = None ) -> None :
739754 with tracing .start_raise_event_span (event_name , instance_id ):
740- req = build_raise_event_req (instance_id , event_name , data )
755+ req = build_raise_event_req (instance_id , event_name , data , self . _data_converter )
741756 self ._logger .info (f"Raising event '{ event_name } ' for instance '{ instance_id } '." )
742757 if self ._payload_store is not None :
743758 payload_helpers .externalize_payloads (
@@ -748,7 +763,7 @@ def raise_orchestration_event(self, instance_id: str, event_name: str, *,
748763 def terminate_orchestration (self , instance_id : str , * ,
749764 output : Any | None = None ,
750765 recursive : bool = True ) -> None :
751- req = build_terminate_req (instance_id , output , recursive )
766+ req = build_terminate_req (instance_id , output , recursive , self . _data_converter )
752767
753768 self ._logger .info (f"Terminating instance '{ instance_id } '." )
754769 if self ._payload_store is not None :
@@ -811,7 +826,7 @@ def signal_entity(self,
811826 entity_instance_id : EntityInstanceId ,
812827 operation_name : str ,
813828 input : Any | None = None ) -> None :
814- req = build_signal_entity_req (entity_instance_id , operation_name , input )
829+ req = build_signal_entity_req (entity_instance_id , operation_name , input , self . _data_converter )
815830 self ._logger .info (f"Signaling entity '{ entity_instance_id } ' operation '{ operation_name } '." )
816831 if self ._payload_store is not None :
817832 payload_helpers .externalize_payloads (
@@ -896,9 +911,11 @@ def __init__(self, *,
896911 channel_options : GrpcChannelOptions | None = None ,
897912 resiliency_options : GrpcClientResiliencyOptions | None = None ,
898913 default_version : str | None = None ,
899- payload_store : PayloadStore | None = None ):
914+ payload_store : PayloadStore | None = None ,
915+ data_converter : DataConverter | None = None ):
900916
901917 self ._owns_channel = channel is None
918+ self ._data_converter = data_converter if data_converter is not None else JsonDataConverter ()
902919 self ._host_address = (
903920 host_address if host_address else shared .get_default_host_address ()
904921 )
@@ -1089,7 +1106,8 @@ async def schedule_new_orchestration(self, orchestrator: task.Orchestrator[TInpu
10891106 req = build_schedule_new_orchestration_req (
10901107 orchestrator , input = input , instance_id = instance_id , start_at = start_at ,
10911108 reuse_id_policy = reuse_id_policy , tags = tags ,
1092- version = version if version else self .default_version )
1109+ version = version if version else self .default_version ,
1110+ data_converter = self ._data_converter )
10931111
10941112 parent_trace_ctx = tracing .get_current_trace_context ()
10951113 if parent_trace_ctx is not None :
@@ -1110,7 +1128,7 @@ async def get_orchestration_state(self, instance_id: str, *,
11101128 res : pb .GetInstanceResponse = await self ._stub .GetInstance (req )
11111129 if self ._payload_store is not None and res .exists :
11121130 await payload_helpers .deexternalize_payloads_async (res , self ._payload_store )
1113- return new_orchestration_state (req .instanceId , res )
1131+ return new_orchestration_state (req .instanceId , res , self . _data_converter )
11141132
11151133 async def get_orchestration_history (self ,
11161134 instance_id : str , * ,
@@ -1166,7 +1184,7 @@ async def get_all_orchestration_states(self,
11661184 resp : pb .QueryInstancesResponse = await self ._stub .QueryInstances (req )
11671185 if self ._payload_store is not None :
11681186 await payload_helpers .deexternalize_payloads_async (resp , self ._payload_store )
1169- states += [parse_orchestration_state (res ) for res in resp .orchestrationState ]
1187+ states += [parse_orchestration_state (res , self . _data_converter ) for res in resp .orchestrationState ]
11701188 if check_continuation_token (resp .continuationToken , _continuation_token , self ._logger ):
11711189 _continuation_token = resp .continuationToken
11721190 else :
@@ -1186,7 +1204,7 @@ async def wait_for_orchestration_start(self, instance_id: str, *,
11861204 )
11871205 if self ._payload_store is not None and res .exists :
11881206 await payload_helpers .deexternalize_payloads_async (res , self ._payload_store )
1189- return new_orchestration_state (req .instanceId , res )
1207+ return new_orchestration_state (req .instanceId , res , self . _data_converter )
11901208 except grpc .aio .AioRpcError as rpc_error :
11911209 if rpc_error .code () == grpc .StatusCode .DEADLINE_EXCEEDED :
11921210 raise TimeoutError ("Timed-out waiting for the orchestration to start" )
@@ -1205,7 +1223,7 @@ async def wait_for_orchestration_completion(self, instance_id: str, *,
12051223 )
12061224 if self ._payload_store is not None and res .exists :
12071225 await payload_helpers .deexternalize_payloads_async (res , self ._payload_store )
1208- state = new_orchestration_state (req .instanceId , res )
1226+ state = new_orchestration_state (req .instanceId , res , self . _data_converter )
12091227 log_completion_state (self ._logger , instance_id , state )
12101228 return state
12111229 except grpc .aio .AioRpcError as rpc_error :
@@ -1217,7 +1235,7 @@ async def wait_for_orchestration_completion(self, instance_id: str, *,
12171235 async def raise_orchestration_event (self , instance_id : str , event_name : str , * ,
12181236 data : Any | None = None ) -> None :
12191237 with tracing .start_raise_event_span (event_name , instance_id ):
1220- req = build_raise_event_req (instance_id , event_name , data )
1238+ req = build_raise_event_req (instance_id , event_name , data , self . _data_converter )
12211239 self ._logger .info (f"Raising event '{ event_name } ' for instance '{ instance_id } '." )
12221240 if self ._payload_store is not None :
12231241 await payload_helpers .externalize_payloads_async (
@@ -1228,7 +1246,7 @@ async def raise_orchestration_event(self, instance_id: str, event_name: str, *,
12281246 async def terminate_orchestration (self , instance_id : str , * ,
12291247 output : Any | None = None ,
12301248 recursive : bool = True ) -> None :
1231- req = build_terminate_req (instance_id , output , recursive )
1249+ req = build_terminate_req (instance_id , output , recursive , self . _data_converter )
12321250
12331251 self ._logger .info (f"Terminating instance '{ instance_id } '." )
12341252 if self ._payload_store is not None :
@@ -1291,7 +1309,7 @@ async def signal_entity(self,
12911309 entity_instance_id : EntityInstanceId ,
12921310 operation_name : str ,
12931311 input : Any | None = None ) -> None :
1294- req = build_signal_entity_req (entity_instance_id , operation_name , input )
1312+ req = build_signal_entity_req (entity_instance_id , operation_name , input , self . _data_converter )
12951313 self ._logger .info (f"Signaling entity '{ entity_instance_id } ' operation '{ operation_name } '." )
12961314 if self ._payload_store is not None :
12971315 await payload_helpers .externalize_payloads_async (
0 commit comments