durabletask 0.0.0.dev38__tar.gz → 0.0.0.dev40__tar.gz

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 (35) hide show
  1. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/PKG-INFO +1 -1
  2. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/client.py +137 -32
  3. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/__init__.py +3 -1
  4. durabletask-0.0.0.dev40/durabletask/entities/entity_instance_id.py +84 -0
  5. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/entity_metadata.py +9 -5
  6. durabletask-0.0.0.dev40/durabletask/entities/entity_operation_failed_exception.py +15 -0
  7. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/helpers.py +7 -0
  8. durabletask-0.0.0.dev40/durabletask/internal/json_encode_output_exception.py +12 -0
  9. durabletask-0.0.0.dev40/durabletask/internal/orchestrator_service_pb2.py +270 -0
  10. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/orchestrator_service_pb2.pyi +41 -208
  11. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/orchestrator_service_pb2_grpc.py +8 -135
  12. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/proto_task_hub_sidecar_service_stub.py +0 -1
  13. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/worker.py +41 -19
  14. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/PKG-INFO +1 -1
  15. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/SOURCES.txt +2 -0
  16. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/pyproject.toml +1 -1
  17. durabletask-0.0.0.dev38/durabletask/entities/entity_instance_id.py +0 -42
  18. durabletask-0.0.0.dev38/durabletask/internal/orchestrator_service_pb2.py +0 -322
  19. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/LICENSE +0 -0
  20. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/README.md +0 -0
  21. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/__init__.py +0 -0
  22. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/durable_entity.py +0 -0
  23. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/entity_context.py +0 -0
  24. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/entities/entity_lock.py +0 -0
  25. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/entity_state_shim.py +0 -0
  26. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/exceptions.py +0 -0
  27. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/grpc_interceptor.py +0 -0
  28. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/orchestration_entity_context.py +0 -0
  29. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/internal/shared.py +0 -0
  30. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/py.typed +0 -0
  31. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask/task.py +0 -0
  32. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/dependency_links.txt +0 -0
  33. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/requires.txt +0 -0
  34. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/durabletask.egg-info/top_level.txt +0 -0
  35. {durabletask-0.0.0.dev38 → durabletask-0.0.0.dev40}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durabletask
3
- Version: 0.0.0.dev38
3
+ Version: 0.0.0.dev40
4
4
  Summary: A Durable Task Client SDK for Python
5
5
  License: MIT License
6
6
 
@@ -4,12 +4,12 @@
4
4
  import logging
5
5
  import uuid
6
6
  from dataclasses import dataclass
7
- from datetime import datetime, timezone
7
+ from datetime import datetime, timedelta, timezone
8
8
  from enum import Enum
9
- from typing import Any, Optional, Sequence, TypeVar, Union
9
+ from typing import Any, List, Optional, Sequence, TypeVar, Union
10
10
 
11
11
  import grpc
12
- from google.protobuf import wrappers_pb2
12
+ from google.protobuf import wrappers_pb2 as pb2
13
13
 
14
14
  from durabletask.entities import EntityInstanceId
15
15
  from durabletask.entities.entity_metadata import EntityMetadata
@@ -57,6 +57,12 @@ class OrchestrationState:
57
57
  self.failure_details)
58
58
 
59
59
 
60
+ class PurgeInstancesResult:
61
+ def __init__(self, deleted_instance_count: int, is_complete: bool):
62
+ self.deleted_instance_count = deleted_instance_count
63
+ self.is_complete = is_complete
64
+
65
+
60
66
  class OrchestrationFailedError(Exception):
61
67
  def __init__(self, message: str, failure_details: task.FailureDetails):
62
68
  super().__init__(message)
@@ -73,6 +79,12 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Op
73
79
 
74
80
  state = res.orchestrationState
75
81
 
82
+ new_state = parse_orchestration_state(state)
83
+ new_state.instance_id = instance_id # Override instance_id with the one from the request, to match old behavior
84
+ return new_state
85
+
86
+
87
+ def parse_orchestration_state(state: pb.OrchestrationState) -> OrchestrationState:
76
88
  failure_details = None
77
89
  if state.failureDetails.errorMessage != '' or state.failureDetails.errorType != '':
78
90
  failure_details = task.FailureDetails(
@@ -81,7 +93,7 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Op
81
93
  state.failureDetails.stackTrace.value if not helpers.is_empty(state.failureDetails.stackTrace) else None)
82
94
 
83
95
  return OrchestrationState(
84
- instance_id,
96
+ state.instanceId,
85
97
  state.name,
86
98
  OrchestrationStatus(state.orchestrationStatus),
87
99
  state.createdTimestamp.ToDatetime(),
@@ -93,7 +105,6 @@ def new_orchestration_state(instance_id: str, res: pb.GetInstanceResponse) -> Op
93
105
 
94
106
 
95
107
  class TaskHubGrpcClient:
96
-
97
108
  def __init__(self, *,
98
109
  host_address: Optional[str] = None,
99
110
  metadata: Optional[list[tuple[str, str]]] = None,
@@ -136,7 +147,7 @@ class TaskHubGrpcClient:
136
147
  req = pb.CreateInstanceRequest(
137
148
  name=name,
138
149
  instanceId=instance_id if instance_id else uuid.uuid4().hex,
139
- input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input is not None else None,
150
+ input=helpers.get_string_value(shared.to_json(input)),
140
151
  scheduledStartTimestamp=helpers.new_timestamp(start_at) if start_at else None,
141
152
  version=helpers.get_string_value(version if version else self.default_version),
142
153
  orchestrationIdReusePolicy=reuse_id_policy,
@@ -152,6 +163,54 @@ class TaskHubGrpcClient:
152
163
  res: pb.GetInstanceResponse = self._stub.GetInstance(req)
153
164
  return new_orchestration_state(req.instanceId, res)
154
165
 
166
+ def get_all_orchestration_states(self,
167
+ max_instance_count: Optional[int] = None,
168
+ fetch_inputs_and_outputs: bool = False) -> List[OrchestrationState]:
169
+ return self.get_orchestration_state_by(
170
+ created_time_from=None,
171
+ created_time_to=None,
172
+ runtime_status=None,
173
+ max_instance_count=max_instance_count,
174
+ fetch_inputs_and_outputs=fetch_inputs_and_outputs
175
+ )
176
+
177
+ def get_orchestration_state_by(self,
178
+ created_time_from: Optional[datetime] = None,
179
+ created_time_to: Optional[datetime] = None,
180
+ runtime_status: Optional[List[OrchestrationStatus]] = None,
181
+ max_instance_count: Optional[int] = None,
182
+ fetch_inputs_and_outputs: bool = False,
183
+ _continuation_token: Optional[pb2.StringValue] = None
184
+ ) -> List[OrchestrationState]:
185
+ if max_instance_count is None:
186
+ # DTS backend does not behave well with max_instance_count = None, so we set to max 32-bit signed value
187
+ max_instance_count = (1 << 31) - 1
188
+ req = pb.QueryInstancesRequest(
189
+ query=pb.InstanceQuery(
190
+ runtimeStatus=[status.value for status in runtime_status] if runtime_status else None,
191
+ createdTimeFrom=helpers.new_timestamp(created_time_from) if created_time_from else None,
192
+ createdTimeTo=helpers.new_timestamp(created_time_to) if created_time_to else None,
193
+ maxInstanceCount=max_instance_count,
194
+ fetchInputsAndOutputs=fetch_inputs_and_outputs,
195
+ continuationToken=_continuation_token
196
+ )
197
+ )
198
+ resp: pb.QueryInstancesResponse = self._stub.QueryInstances(req)
199
+ states = [parse_orchestration_state(res) for res in resp.orchestrationState]
200
+ # Check the value for continuationToken - none or "0" indicates that there are no more results.
201
+ if resp.continuationToken and resp.continuationToken.value and resp.continuationToken.value != "0":
202
+ self._logger.info(f"Received continuation token with value {resp.continuationToken.value}, fetching next list of instances...")
203
+ states += self.get_orchestration_state_by(
204
+ created_time_from,
205
+ created_time_to,
206
+ runtime_status,
207
+ max_instance_count,
208
+ fetch_inputs_and_outputs,
209
+ _continuation_token=resp.continuationToken
210
+ )
211
+ states = [state for state in states if state is not None] # Filter out any None values
212
+ return states
213
+
155
214
  def wait_for_orchestration_start(self, instance_id: str, *,
156
215
  fetch_payloads: bool = False,
157
216
  timeout: int = 60) -> Optional[OrchestrationState]:
@@ -199,7 +258,7 @@ class TaskHubGrpcClient:
199
258
  req = pb.RaiseEventRequest(
200
259
  instanceId=instance_id,
201
260
  name=event_name,
202
- input=wrappers_pb2.StringValue(value=shared.to_json(data)) if data else None)
261
+ input=helpers.get_string_value(shared.to_json(data)))
203
262
 
204
263
  self._logger.info(f"Raising event '{event_name}' for instance '{instance_id}'.")
205
264
  self._stub.RaiseEvent(req)
@@ -209,7 +268,7 @@ class TaskHubGrpcClient:
209
268
  recursive: bool = True):
210
269
  req = pb.TerminateRequest(
211
270
  instanceId=instance_id,
212
- output=wrappers_pb2.StringValue(value=shared.to_json(output)) if output else None,
271
+ output=helpers.get_string_value(shared.to_json(output)),
213
272
  recursive=recursive)
214
273
 
215
274
  self._logger.info(f"Terminating instance '{instance_id}'.")
@@ -225,30 +284,27 @@ class TaskHubGrpcClient:
225
284
  self._logger.info(f"Resuming instance '{instance_id}'.")
226
285
  self._stub.ResumeInstance(req)
227
286
 
228
- def restart_orchestration(self, instance_id: str, *,
229
- restart_with_new_instance_id: bool = False) -> str:
230
- """Restarts an existing orchestration instance.
231
-
232
- Args:
233
- instance_id: The ID of the orchestration instance to restart.
234
- restart_with_new_instance_id: If True, the restarted orchestration will use a new instance ID.
235
- If False (default), the restarted orchestration will reuse the same instance ID.
236
-
237
- Returns:
238
- The instance ID of the restarted orchestration.
239
- """
240
- req = pb.RestartInstanceRequest(
241
- instanceId=instance_id,
242
- restartWithNewInstanceId=restart_with_new_instance_id)
243
-
244
- self._logger.info(f"Restarting instance '{instance_id}'.")
245
- res: pb.RestartInstanceResponse = self._stub.RestartInstance(req)
246
- return res.instanceId
247
-
248
- def purge_orchestration(self, instance_id: str, recursive: bool = True):
287
+ def purge_orchestration(self, instance_id: str, recursive: bool = True) -> PurgeInstancesResult:
249
288
  req = pb.PurgeInstancesRequest(instanceId=instance_id, recursive=recursive)
250
289
  self._logger.info(f"Purging instance '{instance_id}'.")
251
- self._stub.PurgeInstances(req)
290
+ resp: pb.PurgeInstancesResponse = self._stub.PurgeInstances(req)
291
+ return PurgeInstancesResult(resp.deletedInstanceCount, resp.isComplete.value)
292
+
293
+ def purge_orchestrations_by(self,
294
+ created_time_from: Optional[datetime] = None,
295
+ created_time_to: Optional[datetime] = None,
296
+ runtime_status: Optional[List[OrchestrationStatus]] = None,
297
+ recursive: bool = False) -> PurgeInstancesResult:
298
+ resp: pb.PurgeInstancesResponse = self._stub.PurgeInstances(pb.PurgeInstancesRequest(
299
+ instanceId=None,
300
+ purgeInstanceFilter=pb.PurgeInstanceFilter(
301
+ createdTimeFrom=helpers.new_timestamp(created_time_from) if created_time_from else None,
302
+ createdTimeTo=helpers.new_timestamp(created_time_to) if created_time_to else None,
303
+ runtimeStatus=[status.value for status in runtime_status] if runtime_status else None
304
+ ),
305
+ recursive=recursive
306
+ ))
307
+ return PurgeInstancesResult(resp.deletedInstanceCount, resp.isComplete.value)
252
308
 
253
309
  def signal_entity(self,
254
310
  entity_instance_id: EntityInstanceId,
@@ -257,7 +313,7 @@ class TaskHubGrpcClient:
257
313
  req = pb.SignalEntityRequest(
258
314
  instanceId=str(entity_instance_id),
259
315
  name=operation_name,
260
- input=wrappers_pb2.StringValue(value=shared.to_json(input)) if input else None,
316
+ input=helpers.get_string_value(shared.to_json(input)),
261
317
  requestId=str(uuid.uuid4()),
262
318
  scheduledTime=None,
263
319
  parentTraceContext=None,
@@ -276,4 +332,53 @@ class TaskHubGrpcClient:
276
332
  if not res.exists:
277
333
  return None
278
334
 
279
- return EntityMetadata.from_entity_response(res, include_state)
335
+ return EntityMetadata.from_entity_metadata(res.entity, include_state)
336
+
337
+ def get_all_entities(self,
338
+ include_state: bool = True,
339
+ include_transient: bool = False,
340
+ page_size: Optional[int] = None) -> List[EntityMetadata]:
341
+ return self.get_entities_by(
342
+ instance_id_starts_with=None,
343
+ last_modified_from=None,
344
+ last_modified_to=None,
345
+ include_state=include_state,
346
+ include_transient=include_transient,
347
+ page_size=page_size
348
+ )
349
+
350
+ def get_entities_by(self,
351
+ instance_id_starts_with: Optional[str] = None,
352
+ last_modified_from: Optional[datetime] = None,
353
+ last_modified_to: Optional[datetime] = None,
354
+ include_state: bool = True,
355
+ include_transient: bool = False,
356
+ page_size: Optional[int] = None,
357
+ _continuation_token: Optional[pb2.StringValue] = None
358
+ ) -> List[EntityMetadata]:
359
+ self._logger.info(f"Getting entities")
360
+ query_request = pb.QueryEntitiesRequest(
361
+ query=pb.EntityQuery(
362
+ instanceIdStartsWith=helpers.get_string_value(instance_id_starts_with),
363
+ lastModifiedFrom=helpers.new_timestamp(last_modified_from) if last_modified_from else None,
364
+ lastModifiedTo=helpers.new_timestamp(last_modified_to) if last_modified_to else None,
365
+ includeState=include_state,
366
+ includeTransient=include_transient,
367
+ pageSize=helpers.get_int_value(page_size),
368
+ continuationToken=_continuation_token
369
+ )
370
+ )
371
+ resp: pb.QueryEntitiesResponse = self._stub.QueryEntities(query_request)
372
+ entities = [EntityMetadata.from_entity_metadata(entity, query_request.query.includeState) for entity in resp.entities]
373
+ if resp.continuationToken and resp.continuationToken.value != "0":
374
+ self._logger.info(f"Received continuation token with value {resp.continuationToken.value}, fetching next page of entities...")
375
+ entities += self.get_entities_by(
376
+ instance_id_starts_with=instance_id_starts_with,
377
+ last_modified_from=last_modified_from,
378
+ last_modified_to=last_modified_to,
379
+ include_state=include_state,
380
+ include_transient=include_transient,
381
+ page_size=page_size,
382
+ _continuation_token=resp.continuationToken
383
+ )
384
+ return entities
@@ -8,7 +8,9 @@ from durabletask.entities.durable_entity import DurableEntity
8
8
  from durabletask.entities.entity_lock import EntityLock
9
9
  from durabletask.entities.entity_context import EntityContext
10
10
  from durabletask.entities.entity_metadata import EntityMetadata
11
+ from durabletask.entities.entity_operation_failed_exception import EntityOperationFailedException
11
12
 
12
- __all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext", "EntityMetadata"]
13
+ __all__ = ["EntityInstanceId", "DurableEntity", "EntityLock", "EntityContext", "EntityMetadata",
14
+ "EntityOperationFailedException"]
13
15
 
14
16
  PACKAGE_NAME = "durabletask.entities"
@@ -0,0 +1,84 @@
1
+ class EntityInstanceId:
2
+ def __init__(self, entity: str, key: str):
3
+ EntityInstanceId.validate_entity_name(entity)
4
+ EntityInstanceId.validate_key(key)
5
+ self.entity = entity.lower()
6
+ self.key = key
7
+
8
+ def __str__(self) -> str:
9
+ return f"@{self.entity}@{self.key}"
10
+
11
+ def __eq__(self, other):
12
+ if not isinstance(other, EntityInstanceId):
13
+ return False
14
+ return self.entity == other.entity and self.key == other.key
15
+
16
+ def __lt__(self, other):
17
+ if not isinstance(other, EntityInstanceId):
18
+ return self < other
19
+ return str(self) < str(other)
20
+
21
+ @staticmethod
22
+ def parse(entity_id: str) -> "EntityInstanceId":
23
+ """Parse a string representation of an entity ID into an EntityInstanceId object.
24
+
25
+ Parameters
26
+ ----------
27
+ entity_id : str
28
+ The string representation of the entity ID, in the format '@entity@key'.
29
+
30
+ Returns
31
+ -------
32
+ EntityInstanceId
33
+ The parsed EntityInstanceId object.
34
+
35
+ Raises
36
+ ------
37
+ ValueError
38
+ If the input string is not in the correct format.
39
+ """
40
+ if not entity_id.startswith("@"):
41
+ raise ValueError("Entity ID must start with '@'.")
42
+ try:
43
+ _, entity, key = entity_id.split("@", 2)
44
+ except ValueError as ex:
45
+ raise ValueError(f"Invalid entity ID format: {entity_id}") from ex
46
+ return EntityInstanceId(entity=entity, key=key)
47
+
48
+ @staticmethod
49
+ def validate_entity_name(name: str) -> None:
50
+ """Validate that the entity name does not contain invalid characters.
51
+
52
+ Parameters
53
+ ----------
54
+ name : str
55
+ The entity name to validate.
56
+
57
+ Raises
58
+ ------
59
+ ValueError
60
+ If the name is not a valid entity name.
61
+ """
62
+ if not name:
63
+ raise ValueError("Entity name cannot be empty.")
64
+ if "@" in name:
65
+ raise ValueError("Entity name cannot contain '@' symbol.")
66
+
67
+ @staticmethod
68
+ def validate_key(key: str) -> None:
69
+ """Validate that the entity key does not contain invalid characters.
70
+
71
+ Parameters
72
+ ----------
73
+ key : str
74
+ The entity key to validate.
75
+
76
+ Raises
77
+ ------
78
+ ValueError
79
+ If the key is not a valid entity key.
80
+ """
81
+ if not key:
82
+ raise ValueError("Entity key cannot be empty.")
83
+ if "@" in key:
84
+ raise ValueError("Entity key cannot contain '@' symbol.")
@@ -44,18 +44,22 @@ class EntityMetadata:
44
44
 
45
45
  @staticmethod
46
46
  def from_entity_response(entity_response: pb.GetEntityResponse, includes_state: bool):
47
+ return EntityMetadata.from_entity_metadata(entity_response.entity, includes_state)
48
+
49
+ @staticmethod
50
+ def from_entity_metadata(entity: pb.EntityMetadata, includes_state: bool):
47
51
  try:
48
- entity_id = EntityInstanceId.parse(entity_response.entity.instanceId)
52
+ entity_id = EntityInstanceId.parse(entity.instanceId)
49
53
  except ValueError:
50
54
  raise ValueError("Invalid entity instance ID in entity response.")
51
55
  entity_state = None
52
56
  if includes_state:
53
- entity_state = entity_response.entity.serializedState.value
57
+ entity_state = entity.serializedState.value
54
58
  return EntityMetadata(
55
59
  id=entity_id,
56
- last_modified=entity_response.entity.lastModifiedTime.ToDatetime(timezone.utc),
57
- backlog_queue_size=entity_response.entity.backlogQueueSize,
58
- locked_by=entity_response.entity.lockedBy.value,
60
+ last_modified=entity.lastModifiedTime.ToDatetime(timezone.utc),
61
+ backlog_queue_size=entity.backlogQueueSize,
62
+ locked_by=entity.lockedBy.value,
59
63
  includes_state=includes_state,
60
64
  state=entity_state
61
65
  )
@@ -0,0 +1,15 @@
1
+ from durabletask.internal.orchestrator_service_pb2 import TaskFailureDetails
2
+ from durabletask.entities.entity_instance_id import EntityInstanceId
3
+
4
+
5
+ class EntityOperationFailedException(Exception):
6
+ """Exception raised when an operation on an Entity Function fails."""
7
+
8
+ def __init__(self, entity_instance_id: EntityInstanceId, operation_name: str, failure_details: TaskFailureDetails) -> None:
9
+ super().__init__()
10
+ self.entity_instance_id = entity_instance_id
11
+ self.operation_name = operation_name
12
+ self.failure_details = failure_details
13
+
14
+ def __str__(self) -> str:
15
+ return f"Operation '{self.operation_name}' on entity '{self.entity_instance_id}' failed with error: {self.failure_details.errorMessage}"
@@ -184,6 +184,13 @@ def get_string_value(val: Optional[str]) -> Optional[wrappers_pb2.StringValue]:
184
184
  return wrappers_pb2.StringValue(value=val)
185
185
 
186
186
 
187
+ def get_int_value(val: Optional[int]) -> Optional[wrappers_pb2.Int32Value]:
188
+ if val is None:
189
+ return None
190
+ else:
191
+ return wrappers_pb2.Int32Value(value=val)
192
+
193
+
187
194
  def get_string_value_or_empty(val: Optional[str]) -> wrappers_pb2.StringValue:
188
195
  if val is None:
189
196
  return wrappers_pb2.StringValue(value="")
@@ -0,0 +1,12 @@
1
+ from typing import Any
2
+
3
+
4
+ class JsonEncodeOutputException(Exception):
5
+ """Custom exception type used to indicate that an orchestration result could not be JSON-encoded."""
6
+
7
+ def __init__(self, problem_object: Any):
8
+ super().__init__()
9
+ self.problem_object = problem_object
10
+
11
+ def __str__(self) -> str:
12
+ return f"The orchestration result could not be encoded. Object details: {self.problem_object}"