aws-durable-execution-sdk-python-testing 1.0.0__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 (52) hide show
  1. aws_durable_execution_sdk_python_testing/__about__.py +4 -0
  2. aws_durable_execution_sdk_python_testing/__init__.py +20 -0
  3. aws_durable_execution_sdk_python_testing/checkpoint/__init__.py +1 -0
  4. aws_durable_execution_sdk_python_testing/checkpoint/processor.py +101 -0
  5. aws_durable_execution_sdk_python_testing/checkpoint/processors/__init__.py +1 -0
  6. aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +199 -0
  7. aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +89 -0
  8. aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +59 -0
  9. aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +52 -0
  10. aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +124 -0
  11. aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +95 -0
  12. aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +104 -0
  13. aws_durable_execution_sdk_python_testing/checkpoint/validators/__init__.py +1 -0
  14. aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +242 -0
  15. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/__init__.py +1 -0
  16. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/callback.py +45 -0
  17. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/context.py +73 -0
  18. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/execution.py +47 -0
  19. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +56 -0
  20. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/step.py +106 -0
  21. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/wait.py +54 -0
  22. aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +66 -0
  23. aws_durable_execution_sdk_python_testing/cli.py +501 -0
  24. aws_durable_execution_sdk_python_testing/client.py +50 -0
  25. aws_durable_execution_sdk_python_testing/exceptions.py +288 -0
  26. aws_durable_execution_sdk_python_testing/execution.py +440 -0
  27. aws_durable_execution_sdk_python_testing/executor.py +1214 -0
  28. aws_durable_execution_sdk_python_testing/invoker.py +327 -0
  29. aws_durable_execution_sdk_python_testing/model.py +3267 -0
  30. aws_durable_execution_sdk_python_testing/observer.py +144 -0
  31. aws_durable_execution_sdk_python_testing/py.typed +1 -0
  32. aws_durable_execution_sdk_python_testing/runner.py +1185 -0
  33. aws_durable_execution_sdk_python_testing/scheduler.py +246 -0
  34. aws_durable_execution_sdk_python_testing/stores/__init__.py +1 -0
  35. aws_durable_execution_sdk_python_testing/stores/base.py +147 -0
  36. aws_durable_execution_sdk_python_testing/stores/filesystem.py +104 -0
  37. aws_durable_execution_sdk_python_testing/stores/memory.py +38 -0
  38. aws_durable_execution_sdk_python_testing/stores/sqlite.py +283 -0
  39. aws_durable_execution_sdk_python_testing/token.py +49 -0
  40. aws_durable_execution_sdk_python_testing/web/__init__.py +1 -0
  41. aws_durable_execution_sdk_python_testing/web/errors.py +8 -0
  42. aws_durable_execution_sdk_python_testing/web/handlers.py +807 -0
  43. aws_durable_execution_sdk_python_testing/web/models.py +266 -0
  44. aws_durable_execution_sdk_python_testing/web/routes.py +684 -0
  45. aws_durable_execution_sdk_python_testing/web/serialization.py +245 -0
  46. aws_durable_execution_sdk_python_testing/web/server.py +243 -0
  47. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/METADATA +221 -0
  48. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/RECORD +52 -0
  49. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/WHEEL +4 -0
  50. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/entry_points.txt +2 -0
  51. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/LICENSE +175 -0
  52. aws_durable_execution_sdk_python_testing-1.0.0.dist-info/licenses/NOTICE +1 -0
@@ -0,0 +1,3267 @@
1
+ """Model classes for the web API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ from dataclasses import dataclass, replace
7
+ from enum import Enum
8
+ from typing import Any
9
+ import json
10
+
11
+ from dateutil.tz import UTC
12
+
13
+ from aws_durable_execution_sdk_python.execution import DurableExecutionInvocationOutput
14
+
15
+ # Import existing types from the main SDK - REUSE EVERYTHING POSSIBLE
16
+ from aws_durable_execution_sdk_python.lambda_service import (
17
+ CallbackDetails,
18
+ CallbackOptions,
19
+ ChainedInvokeDetails,
20
+ ChainedInvokeOptions,
21
+ ContextDetails,
22
+ ContextOptions,
23
+ ErrorObject,
24
+ ExecutionDetails,
25
+ Operation,
26
+ OperationAction,
27
+ OperationStatus,
28
+ OperationSubType,
29
+ OperationType,
30
+ OperationUpdate,
31
+ StepDetails,
32
+ StepOptions,
33
+ WaitDetails,
34
+ WaitOptions,
35
+ )
36
+ from aws_durable_execution_sdk_python.types import (
37
+ LambdaContext as LambdaContextProtocol,
38
+ )
39
+
40
+ from aws_durable_execution_sdk_python_testing.exceptions import (
41
+ InvalidParameterValueException,
42
+ )
43
+
44
+
45
+ class EventType(Enum):
46
+ """Event types for durable execution events."""
47
+
48
+ EXECUTION_STARTED = "ExecutionStarted"
49
+ EXECUTION_SUCCEEDED = "ExecutionSucceeded"
50
+ EXECUTION_FAILED = "ExecutionFailed"
51
+ EXECUTION_TIMED_OUT = "ExecutionTimedOut"
52
+ EXECUTION_STOPPED = "ExecutionStopped"
53
+ CONTEXT_STARTED = "ContextStarted"
54
+ CONTEXT_SUCCEEDED = "ContextSucceeded"
55
+ CONTEXT_FAILED = "ContextFailed"
56
+ WAIT_STARTED = "WaitStarted"
57
+ WAIT_SUCCEEDED = "WaitSucceeded"
58
+ WAIT_CANCELLED = "WaitCancelled"
59
+ STEP_STARTED = "StepStarted"
60
+ STEP_SUCCEEDED = "StepSucceeded"
61
+ STEP_FAILED = "StepFailed"
62
+ CHAINED_INVOKE_STARTED = "ChainedInvokeStarted"
63
+ CHAINED_INVOKE_SUCCEEDED = "ChainedInvokeSucceeded"
64
+ CHAINED_INVOKE_FAILED = "ChainedInvokeFailed"
65
+ CHAINED_INVOKE_TIMED_OUT = "ChainedInvokeTimedOut"
66
+ CHAINED_INVOKE_STOPPED = "ChainedInvokeStopped"
67
+ CALLBACK_STARTED = "CallbackStarted"
68
+ CALLBACK_SUCCEEDED = "CallbackSucceeded"
69
+ CALLBACK_FAILED = "CallbackFailed"
70
+ CALLBACK_TIMED_OUT = "CallbackTimedOut"
71
+ INVOCATION_COMPLETED = "InvocationCompleted"
72
+
73
+
74
+ TERMINAL_STATUSES: set[OperationStatus] = {
75
+ OperationStatus.SUCCEEDED,
76
+ OperationStatus.FAILED,
77
+ OperationStatus.TIMED_OUT,
78
+ OperationStatus.STOPPED,
79
+ OperationStatus.CANCELLED,
80
+ }
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class LambdaContext(LambdaContextProtocol):
85
+ """Lambda context for testing."""
86
+
87
+ aws_request_id: str
88
+ log_group_name: str | None = None
89
+ log_stream_name: str | None = None
90
+ function_name: str | None = None
91
+ memory_limit_in_mb: str | None = None
92
+ function_version: str | None = None
93
+ invoked_function_arn: str | None = None
94
+ tenant_id: str | None = None
95
+ client_context: dict | None = None
96
+ identity: dict | None = None
97
+
98
+ def get_remaining_time_in_millis(self) -> int:
99
+ return 900000 # 15 minutes default
100
+
101
+ def log(self, msg) -> None:
102
+ pass # No-op for testing
103
+
104
+
105
+ # region web_api_models
106
+ # Web API specific models (not in Smithy but needed for web interface)
107
+ @dataclass(frozen=True)
108
+ class StartDurableExecutionInput:
109
+ """Input for starting a durable execution via web API."""
110
+
111
+ account_id: str
112
+ function_name: str
113
+ function_qualifier: str
114
+ execution_name: str
115
+ execution_timeout_seconds: int
116
+ execution_retention_period_days: int
117
+ invocation_id: str | None = None
118
+ trace_fields: dict | None = None
119
+ tenant_id: str | None = None
120
+ input: str | None = None
121
+ lambda_endpoint: str | None = None # Endpoint for this specific execution
122
+
123
+ @classmethod
124
+ def from_dict(cls, data: dict) -> StartDurableExecutionInput:
125
+ # Validate required fields and raise AWS-compliant exceptions
126
+ required_fields = [
127
+ "AccountId",
128
+ "FunctionName",
129
+ "FunctionQualifier",
130
+ "ExecutionName",
131
+ "ExecutionTimeoutSeconds",
132
+ "ExecutionRetentionPeriodDays",
133
+ ]
134
+
135
+ for field in required_fields:
136
+ if field not in data:
137
+ msg: str = f"Missing required field: {field}"
138
+ raise InvalidParameterValueException(msg)
139
+
140
+ return cls(
141
+ account_id=data["AccountId"],
142
+ function_name=data["FunctionName"],
143
+ function_qualifier=data["FunctionQualifier"],
144
+ execution_name=data["ExecutionName"],
145
+ execution_timeout_seconds=data["ExecutionTimeoutSeconds"],
146
+ execution_retention_period_days=data["ExecutionRetentionPeriodDays"],
147
+ invocation_id=data.get("InvocationId"),
148
+ trace_fields=data.get("TraceFields"),
149
+ tenant_id=data.get("TenantId"),
150
+ input=data.get("Input"),
151
+ lambda_endpoint=data.get("LambdaEndpoint", None),
152
+ )
153
+
154
+ def to_dict(self) -> dict[str, Any]:
155
+ result = {
156
+ "AccountId": self.account_id,
157
+ "FunctionName": self.function_name,
158
+ "FunctionQualifier": self.function_qualifier,
159
+ "ExecutionName": self.execution_name,
160
+ "ExecutionTimeoutSeconds": self.execution_timeout_seconds,
161
+ "ExecutionRetentionPeriodDays": self.execution_retention_period_days,
162
+ }
163
+ if self.invocation_id is not None:
164
+ result["InvocationId"] = self.invocation_id
165
+ if self.trace_fields is not None:
166
+ result["TraceFields"] = self.trace_fields
167
+ if self.tenant_id is not None:
168
+ result["TenantId"] = self.tenant_id
169
+ if self.input is not None:
170
+ result["Input"] = self.input
171
+ if self.lambda_endpoint is not None:
172
+ result["LambdaEndpoint"] = self.lambda_endpoint
173
+ return result
174
+
175
+ def get_normalized_input(self):
176
+ """
177
+ Normalize input string to be JSON deserializable.
178
+ Avoid double coding json input.
179
+ """
180
+ # Try to parse once
181
+ try:
182
+ _ = json.loads(self.input)
183
+ return self.input
184
+ except (json.JSONDecodeError, TypeError):
185
+ # Not valid JSON, treat as plain string and encode it
186
+ return json.dumps(self.input)
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class StartDurableExecutionOutput:
191
+ """Output from starting a durable execution via web API."""
192
+
193
+ execution_arn: str | None = None
194
+
195
+ @classmethod
196
+ def from_dict(cls, data: dict) -> StartDurableExecutionOutput:
197
+ return cls(execution_arn=data.get("ExecutionArn"))
198
+
199
+ def to_dict(self) -> dict[str, Any]:
200
+ result = {}
201
+ if self.execution_arn is not None:
202
+ result["ExecutionArn"] = self.execution_arn
203
+ return result
204
+
205
+
206
+ # endregion web_api_models
207
+
208
+
209
+ # region smithy_api_models
210
+ # Smithy-based API models
211
+ @dataclass(frozen=True)
212
+ class GetDurableExecutionRequest:
213
+ """Request to get durable execution details."""
214
+
215
+ durable_execution_arn: str
216
+
217
+ @classmethod
218
+ def from_dict(cls, data: dict) -> GetDurableExecutionRequest:
219
+ return cls(durable_execution_arn=data["DurableExecutionArn"])
220
+
221
+ def to_dict(self) -> dict[str, Any]:
222
+ return {"DurableExecutionArn": self.durable_execution_arn}
223
+
224
+
225
+ @dataclass(frozen=True)
226
+ class GetDurableExecutionResponse:
227
+ """Response containing durable execution details."""
228
+
229
+ durable_execution_arn: str
230
+ durable_execution_name: str
231
+ function_arn: str
232
+ status: str
233
+ start_timestamp: datetime.datetime
234
+ input_payload: str | None = None
235
+ result: str | None = None
236
+ error: ErrorObject | None = None
237
+ end_timestamp: datetime.datetime | None = None
238
+ version: str | None = None
239
+
240
+ @classmethod
241
+ def from_dict(cls, data: dict) -> GetDurableExecutionResponse:
242
+ error = None
243
+ if error_data := data.get("Error"):
244
+ error = ErrorObject.from_dict(error_data)
245
+
246
+ return cls(
247
+ durable_execution_arn=data["DurableExecutionArn"],
248
+ durable_execution_name=data["DurableExecutionName"],
249
+ function_arn=data["FunctionArn"],
250
+ status=data["Status"],
251
+ start_timestamp=data["StartTimestamp"],
252
+ input_payload=data.get("InputPayload"),
253
+ result=data.get("Result"),
254
+ error=error,
255
+ end_timestamp=data.get("EndTimestamp"),
256
+ version=data.get("Version"),
257
+ )
258
+
259
+ def to_dict(self) -> dict[str, Any]:
260
+ result: dict[str, Any] = {
261
+ "DurableExecutionArn": self.durable_execution_arn,
262
+ "DurableExecutionName": self.durable_execution_name,
263
+ "FunctionArn": self.function_arn,
264
+ "Status": self.status,
265
+ "StartTimestamp": self.start_timestamp,
266
+ }
267
+ if self.input_payload is not None:
268
+ result["InputPayload"] = self.input_payload
269
+ if self.result is not None:
270
+ result["Result"] = self.result
271
+ if self.error is not None:
272
+ result["Error"] = self.error.to_dict()
273
+ if self.end_timestamp is not None:
274
+ result["EndTimestamp"] = self.end_timestamp
275
+ if self.end_timestamp is not None:
276
+ result["EndTimestamp"] = self.end_timestamp
277
+ if self.version is not None:
278
+ result["Version"] = self.version
279
+ return result
280
+
281
+
282
+ @dataclass(frozen=True)
283
+ class Execution:
284
+ """Execution summary structure from Smithy model."""
285
+
286
+ durable_execution_arn: str
287
+ durable_execution_name: str
288
+ function_arn: str
289
+ status: str
290
+ start_timestamp: datetime.datetime
291
+ end_timestamp: datetime.datetime | None = None
292
+
293
+ @classmethod
294
+ def from_dict(cls, data: dict) -> Execution:
295
+ return cls(
296
+ durable_execution_arn=data["DurableExecutionArn"],
297
+ durable_execution_name=data["DurableExecutionName"],
298
+ function_arn=data.get(
299
+ "FunctionArn", ""
300
+ ), # Make optional for backward compatibility
301
+ status=data["Status"],
302
+ start_timestamp=data["StartTimestamp"],
303
+ end_timestamp=data.get("EndTimestamp"),
304
+ )
305
+
306
+ def to_dict(self) -> dict[str, Any]:
307
+ result = {
308
+ "DurableExecutionArn": self.durable_execution_arn,
309
+ "DurableExecutionName": self.durable_execution_name,
310
+ "Status": self.status,
311
+ "StartTimestamp": self.start_timestamp,
312
+ }
313
+ if self.function_arn: # Only include if not empty
314
+ result["FunctionArn"] = self.function_arn
315
+ if self.end_timestamp is not None:
316
+ result["EndTimestamp"] = self.end_timestamp
317
+ return result
318
+
319
+ @classmethod
320
+ def from_execution(cls, execution, status: str) -> Execution:
321
+ """Create ExecutionSummary from Execution object."""
322
+
323
+ execution_op = execution.get_operation_execution_started()
324
+ return cls(
325
+ durable_execution_arn=execution.durable_execution_arn,
326
+ durable_execution_name=execution.start_input.execution_name,
327
+ function_arn=f"arn:aws:lambda:us-east-1:123456789012:function:{execution.start_input.function_name}",
328
+ status=status,
329
+ start_timestamp=execution_op.start_timestamp
330
+ if execution_op.start_timestamp
331
+ else datetime.datetime.now(datetime.UTC),
332
+ end_timestamp=execution_op.end_timestamp
333
+ if execution_op.end_timestamp
334
+ else None,
335
+ )
336
+
337
+
338
+ @dataclass(frozen=True)
339
+ class ListDurableExecutionsRequest:
340
+ """Request to list durable executions."""
341
+
342
+ function_name: str | None = None
343
+ function_version: str | None = None
344
+ durable_execution_name: str | None = None
345
+ status_filter: list[str] | None = None
346
+ started_after: str | None = None
347
+ started_before: str | None = None
348
+ marker: str | None = None
349
+ max_items: int = 0
350
+ reverse_order: bool | None = None
351
+
352
+ @classmethod
353
+ def from_dict(cls, data: dict) -> ListDurableExecutionsRequest:
354
+ # Handle query parameters that may be lists
355
+ function_name = data.get("FunctionName")
356
+ if isinstance(function_name, list):
357
+ function_name = function_name[0] if function_name else None
358
+
359
+ function_version = data.get("FunctionVersion")
360
+ if isinstance(function_version, list):
361
+ function_version = function_version[0] if function_version else None
362
+
363
+ durable_execution_name = data.get("DurableExecutionName")
364
+ if isinstance(durable_execution_name, list):
365
+ durable_execution_name = (
366
+ durable_execution_name[0] if durable_execution_name else None
367
+ )
368
+
369
+ status_filter = data.get("StatusFilter")
370
+ if isinstance(status_filter, list):
371
+ status_filter = status_filter if status_filter else None
372
+ elif status_filter:
373
+ status_filter = [status_filter]
374
+
375
+ started_after = data.get("StartedAfter")
376
+ if isinstance(started_after, list):
377
+ started_after = started_after[0] if started_after else None
378
+
379
+ started_before = data.get("StartedBefore")
380
+ if isinstance(started_before, list):
381
+ started_before = started_before[0] if started_before else None
382
+
383
+ marker = data.get("Marker")
384
+ if isinstance(marker, list):
385
+ marker = marker[0] if marker else None
386
+
387
+ max_items = data.get("MaxItems", 0)
388
+ if isinstance(max_items, list):
389
+ max_items = int(max_items[0]) if max_items else 0
390
+
391
+ reverse_order = data.get("ReverseOrder")
392
+ if isinstance(reverse_order, list):
393
+ reverse_order = (
394
+ reverse_order[0].lower() in ("true", "1", "yes")
395
+ if reverse_order
396
+ else None
397
+ )
398
+ elif isinstance(reverse_order, str):
399
+ reverse_order = reverse_order.lower() in ("true", "1", "yes")
400
+
401
+ return cls(
402
+ function_name=function_name,
403
+ function_version=function_version,
404
+ durable_execution_name=durable_execution_name,
405
+ status_filter=status_filter,
406
+ started_after=started_after,
407
+ started_before=started_before,
408
+ marker=marker,
409
+ max_items=max_items,
410
+ reverse_order=reverse_order,
411
+ )
412
+
413
+ def to_dict(self) -> dict[str, Any]:
414
+ result: dict[str, Any] = {}
415
+ if self.function_name is not None:
416
+ result["FunctionName"] = self.function_name
417
+ if self.function_version is not None:
418
+ result["FunctionVersion"] = self.function_version
419
+ if self.durable_execution_name is not None:
420
+ result["DurableExecutionName"] = self.durable_execution_name
421
+ if self.status_filter is not None:
422
+ result["StatusFilter"] = self.status_filter
423
+ if self.started_after is not None:
424
+ result["StartedAfter"] = self.started_after
425
+ if self.started_before is not None:
426
+ result["StartedBefore"] = self.started_before
427
+ if self.marker is not None:
428
+ result["Marker"] = self.marker
429
+ if self.max_items is not None:
430
+ result["MaxItems"] = self.max_items
431
+ if self.reverse_order is not None:
432
+ result["ReverseOrder"] = self.reverse_order
433
+ return result
434
+
435
+
436
+ @dataclass(frozen=True)
437
+ class ListDurableExecutionsResponse:
438
+ """Response containing list of durable executions."""
439
+
440
+ durable_executions: list[Execution]
441
+ next_marker: str | None = None
442
+
443
+ @classmethod
444
+ def from_dict(cls, data: dict) -> ListDurableExecutionsResponse:
445
+ executions = [
446
+ Execution.from_dict(exec_data)
447
+ for exec_data in data.get("DurableExecutions", [])
448
+ ]
449
+ return cls(
450
+ durable_executions=executions,
451
+ next_marker=data.get("NextMarker"),
452
+ )
453
+
454
+ def to_dict(self) -> dict[str, Any]:
455
+ result: dict[str, Any] = {
456
+ "DurableExecutions": [exe.to_dict() for exe in self.durable_executions]
457
+ }
458
+ if self.next_marker is not None:
459
+ result["NextMarker"] = self.next_marker
460
+ return result
461
+
462
+
463
+ @dataclass(frozen=True)
464
+ class StopDurableExecutionRequest:
465
+ """Request to stop a durable execution."""
466
+
467
+ durable_execution_arn: str
468
+ error: ErrorObject | None = None
469
+
470
+ @classmethod
471
+ def from_dict(cls, data: dict) -> StopDurableExecutionRequest:
472
+ error = None
473
+ if error_data := data.get("Error"):
474
+ error = ErrorObject.from_dict(error_data)
475
+
476
+ return cls(
477
+ durable_execution_arn=data["DurableExecutionArn"],
478
+ error=error,
479
+ )
480
+
481
+ def to_dict(self) -> dict[str, Any]:
482
+ result: dict[str, Any] = {"DurableExecutionArn": self.durable_execution_arn}
483
+ if self.error is not None:
484
+ result["Error"] = self.error.to_dict()
485
+ return result
486
+
487
+
488
+ @dataclass(frozen=True)
489
+ class StopDurableExecutionResponse:
490
+ """Response from stopping a durable execution."""
491
+
492
+ stop_timestamp: datetime.datetime
493
+
494
+ @classmethod
495
+ def from_dict(cls, data: dict) -> StopDurableExecutionResponse:
496
+ return cls(stop_timestamp=data["StopTimestamp"])
497
+
498
+ def to_dict(self) -> dict[str, Any]:
499
+ return {"StopTimestamp": self.stop_timestamp}
500
+
501
+
502
+ @dataclass(frozen=True)
503
+ class GetDurableExecutionStateRequest:
504
+ """Request to get durable execution state."""
505
+
506
+ durable_execution_arn: str
507
+ checkpoint_token: str
508
+ marker: str | None = None
509
+ max_items: int = 0
510
+
511
+ @classmethod
512
+ def from_dict(cls, data: dict) -> GetDurableExecutionStateRequest:
513
+ return cls(
514
+ durable_execution_arn=data["DurableExecutionArn"],
515
+ checkpoint_token=data["CheckpointToken"],
516
+ marker=data.get("Marker"),
517
+ max_items=data.get("MaxItems", 0),
518
+ )
519
+
520
+ def to_dict(self) -> dict[str, Any]:
521
+ result: dict[str, Any] = {
522
+ "DurableExecutionArn": self.durable_execution_arn,
523
+ "CheckpointToken": self.checkpoint_token,
524
+ }
525
+ if self.marker is not None:
526
+ result["Marker"] = self.marker
527
+ if self.max_items is not None:
528
+ result["MaxItems"] = self.max_items
529
+ return result
530
+
531
+
532
+ @dataclass(frozen=True)
533
+ class GetDurableExecutionStateResponse:
534
+ """Response containing durable execution state operations."""
535
+
536
+ operations: list[Operation]
537
+ next_marker: str | None = None
538
+
539
+ @classmethod
540
+ def from_dict(cls, data: dict) -> GetDurableExecutionStateResponse:
541
+ operations = [
542
+ Operation.from_dict(op_data) for op_data in data.get("Operations", [])
543
+ ]
544
+ return cls(
545
+ operations=operations,
546
+ next_marker=data.get("NextMarker"),
547
+ )
548
+
549
+ def to_dict(self) -> dict[str, Any]:
550
+ result: dict[str, Any] = {
551
+ "Operations": [op.to_dict() for op in self.operations]
552
+ }
553
+ if self.next_marker is not None:
554
+ result["NextMarker"] = self.next_marker
555
+ return result
556
+
557
+
558
+ # endregion smithy_api_models
559
+
560
+
561
+ # region event_structures
562
+ # Event-related structures from Smithy model
563
+ @dataclass(frozen=True)
564
+ class EventInput:
565
+ """Event input structure."""
566
+
567
+ payload: str | None = None
568
+ truncated: bool = False
569
+
570
+ @classmethod
571
+ def from_dict(cls, data: dict) -> EventInput:
572
+ return cls(
573
+ payload=data.get("Payload"),
574
+ truncated=data.get("Truncated", False),
575
+ )
576
+
577
+ def to_dict(self) -> dict[str, Any]:
578
+ result: dict[str, Any] = {"Truncated": self.truncated}
579
+ if self.payload is not None:
580
+ result["Payload"] = self.payload
581
+ return result
582
+
583
+ @classmethod
584
+ def from_details(
585
+ cls,
586
+ details: ExecutionDetails,
587
+ include: bool = False, # noqa: FBT001, FBT002
588
+ ) -> EventInput:
589
+ details_input: str | None = details.input_payload if details else None
590
+ payload: str | None = details_input if include else None
591
+ truncated: bool = not include
592
+ return cls(payload=payload, truncated=truncated)
593
+
594
+ @classmethod
595
+ def from_start_durable_execution_input(
596
+ cls,
597
+ start_durable_execution_input: StartDurableExecutionInput,
598
+ include: bool = False, # noqa: FBT001, FBT002
599
+ ) -> EventInput:
600
+ input: str | None = start_durable_execution_input.input
601
+ truncated: bool = not include
602
+ return cls(input, truncated)
603
+
604
+
605
+ @dataclass(frozen=True)
606
+ class EventResult:
607
+ """Event result structure."""
608
+
609
+ payload: str | None = None
610
+ truncated: bool = False
611
+
612
+ @classmethod
613
+ def from_dict(cls, data: dict) -> EventResult:
614
+ return cls(
615
+ payload=data.get("Payload"),
616
+ truncated=data.get("Truncated", False),
617
+ )
618
+
619
+ def to_dict(self) -> dict[str, Any]:
620
+ result: dict[str, Any] = {"Truncated": self.truncated}
621
+ if self.payload is not None:
622
+ result["Payload"] = self.payload
623
+ return result
624
+
625
+ @classmethod
626
+ def from_details(
627
+ cls,
628
+ details: CallbackDetails | StepDetails | ChainedInvokeDetails | ContextDetails,
629
+ include: bool = False, # noqa: FBT001, FBT002
630
+ ) -> EventResult:
631
+ details_result: str | None = details.result if details else None
632
+ payload: str | None = details_result if include else None
633
+ truncated: bool = not include
634
+ return cls(payload=payload, truncated=truncated)
635
+
636
+ @classmethod
637
+ def from_durable_execution_invocation_output(
638
+ cls,
639
+ durable_execution_invocation_output: DurableExecutionInvocationOutput,
640
+ include: bool = False, # noqa: FBT001, FBT002
641
+ ) -> EventResult:
642
+ truncated: bool = not include
643
+ return cls(durable_execution_invocation_output.result, truncated)
644
+
645
+
646
+ @dataclass(frozen=True)
647
+ class EventError:
648
+ """Event error structure."""
649
+
650
+ payload: ErrorObject | None = None
651
+ truncated: bool = False
652
+
653
+ @classmethod
654
+ def from_dict(cls, data: dict) -> EventError:
655
+ payload = None
656
+ if payload_data := data.get("Payload"):
657
+ payload = ErrorObject.from_dict(payload_data)
658
+
659
+ return cls(
660
+ payload=payload,
661
+ truncated=data.get("Truncated", False),
662
+ )
663
+
664
+ def to_dict(self) -> dict[str, Any]:
665
+ result: dict[str, Any] = {"Truncated": self.truncated}
666
+ if self.payload is not None:
667
+ result["Payload"] = self.payload.to_dict()
668
+ return result
669
+
670
+ @classmethod
671
+ def from_details(
672
+ cls,
673
+ details: CallbackDetails | StepDetails | ChainedInvokeDetails | ContextDetails,
674
+ include: bool = False, # noqa: FBT001, FBT002
675
+ ) -> EventError:
676
+ error_object: ErrorObject | None = details.error if details else None
677
+ truncated: bool = not include
678
+ return cls(error_object, truncated)
679
+
680
+ @classmethod
681
+ def from_durable_execution_invocation_output(
682
+ cls,
683
+ durable_execution_invocation_output: DurableExecutionInvocationOutput,
684
+ include: bool = False, # noqa: FBT001, FBT002
685
+ ) -> EventError:
686
+ truncated: bool = not include
687
+ return cls(durable_execution_invocation_output.error, truncated)
688
+
689
+
690
+ @dataclass(frozen=True)
691
+ class RetryDetails:
692
+ """Retry details structure."""
693
+
694
+ current_attempt: int = 0
695
+ next_attempt_delay_seconds: int | None = None
696
+
697
+ @classmethod
698
+ def from_dict(cls, data: dict) -> RetryDetails:
699
+ return cls(
700
+ current_attempt=data.get("CurrentAttempt", 0),
701
+ next_attempt_delay_seconds=data.get("NextAttemptDelaySeconds"),
702
+ )
703
+
704
+ def to_dict(self) -> dict[str, Any]:
705
+ result: dict[str, Any] = {"CurrentAttempt": self.current_attempt}
706
+ if self.next_attempt_delay_seconds is not None:
707
+ result["NextAttemptDelaySeconds"] = self.next_attempt_delay_seconds
708
+ return result
709
+
710
+
711
+ # Event detail structures
712
+ @dataclass(frozen=True)
713
+ class ExecutionStartedDetails:
714
+ """Execution started event details."""
715
+
716
+ input: EventInput | None = None
717
+ execution_timeout: int | None = None
718
+
719
+ @classmethod
720
+ def from_dict(cls, data: dict) -> ExecutionStartedDetails:
721
+ input_data = None
722
+ if input_dict := data.get("Input"):
723
+ input_data = EventInput.from_dict(input_dict)
724
+
725
+ return cls(
726
+ input=input_data,
727
+ execution_timeout=data.get("ExecutionTimeout"),
728
+ )
729
+
730
+ def to_dict(self) -> dict[str, Any]:
731
+ result: dict[str, Any] = {}
732
+ if self.input is not None:
733
+ result["Input"] = self.input.to_dict()
734
+ if self.execution_timeout is not None:
735
+ result["ExecutionTimeout"] = self.execution_timeout
736
+ return result
737
+
738
+
739
+ @dataclass(frozen=True)
740
+ class ExecutionSucceededDetails:
741
+ """Execution succeeded event details."""
742
+
743
+ result: EventResult | None = None
744
+
745
+ @classmethod
746
+ def from_dict(cls, data: dict) -> ExecutionSucceededDetails:
747
+ result_data = None
748
+ if result_dict := data.get("Result"):
749
+ result_data = EventResult.from_dict(result_dict)
750
+
751
+ return cls(result=result_data)
752
+
753
+ def to_dict(self) -> dict[str, Any]:
754
+ result: dict[str, Any] = {}
755
+ if self.result is not None:
756
+ result["Result"] = self.result.to_dict()
757
+ return result
758
+
759
+
760
+ @dataclass(frozen=True)
761
+ class ExecutionFailedDetails:
762
+ """Execution failed event details."""
763
+
764
+ error: EventError | None = None
765
+
766
+ @classmethod
767
+ def from_dict(cls, data: dict) -> ExecutionFailedDetails:
768
+ error_data = None
769
+ if error_dict := data.get("Error"):
770
+ error_data = EventError.from_dict(error_dict)
771
+
772
+ return cls(error=error_data)
773
+
774
+ def to_dict(self) -> dict[str, Any]:
775
+ result: dict[str, Any] = {}
776
+ if self.error is not None:
777
+ result["Error"] = self.error.to_dict()
778
+ return result
779
+
780
+
781
+ @dataclass(frozen=True)
782
+ class ExecutionTimedOutDetails:
783
+ """Execution timed out event details."""
784
+
785
+ error: EventError | None = None
786
+
787
+ @classmethod
788
+ def from_dict(cls, data: dict) -> ExecutionTimedOutDetails:
789
+ error_data = None
790
+ if error_dict := data.get("Error"):
791
+ error_data = EventError.from_dict(error_dict)
792
+
793
+ return cls(error=error_data)
794
+
795
+ def to_dict(self) -> dict[str, Any]:
796
+ result: dict[str, Any] = {}
797
+ if self.error is not None:
798
+ result["Error"] = self.error.to_dict()
799
+ return result
800
+
801
+
802
+ @dataclass(frozen=True)
803
+ class ExecutionStoppedDetails:
804
+ """Execution stopped event details."""
805
+
806
+ error: EventError | None = None
807
+
808
+ @classmethod
809
+ def from_dict(cls, data: dict) -> ExecutionStoppedDetails:
810
+ error_data = None
811
+ if error_dict := data.get("Error"):
812
+ error_data = EventError.from_dict(error_dict)
813
+
814
+ return cls(error=error_data)
815
+
816
+ def to_dict(self) -> dict[str, Any]:
817
+ result: dict[str, Any] = {}
818
+ if self.error is not None:
819
+ result["Error"] = self.error.to_dict()
820
+ return result
821
+
822
+
823
+ @dataclass(frozen=True)
824
+ class ContextStartedDetails:
825
+ """Context started event details."""
826
+
827
+ @classmethod
828
+ def from_dict(cls, data: dict) -> ContextStartedDetails: # noqa: ARG003
829
+ return cls()
830
+
831
+ def to_dict(self) -> dict[str, Any]:
832
+ return {}
833
+
834
+
835
+ @dataclass(frozen=True)
836
+ class ContextSucceededDetails:
837
+ """Context succeeded event details."""
838
+
839
+ result: EventResult | None = None
840
+
841
+ @classmethod
842
+ def from_dict(cls, data: dict) -> ContextSucceededDetails:
843
+ result_data = None
844
+ if result_dict := data.get("Result"):
845
+ result_data = EventResult.from_dict(result_dict)
846
+
847
+ return cls(result=result_data)
848
+
849
+ def to_dict(self) -> dict[str, Any]:
850
+ result: dict[str, Any] = {}
851
+ if self.result is not None:
852
+ result["Result"] = self.result.to_dict()
853
+ return result
854
+
855
+
856
+ @dataclass(frozen=True)
857
+ class ContextFailedDetails:
858
+ """Context failed event details."""
859
+
860
+ error: EventError | None = None
861
+
862
+ @classmethod
863
+ def from_dict(cls, data: dict) -> ContextFailedDetails:
864
+ error_data = None
865
+ if error_dict := data.get("Error"):
866
+ error_data = EventError.from_dict(error_dict)
867
+
868
+ return cls(error=error_data)
869
+
870
+ def to_dict(self) -> dict[str, Any]:
871
+ result: dict[str, Any] = {}
872
+ if self.error is not None:
873
+ result["Error"] = self.error.to_dict()
874
+ return result
875
+
876
+
877
+ @dataclass(frozen=True)
878
+ class WaitStartedDetails:
879
+ """Wait started event details."""
880
+
881
+ duration: int | None = None
882
+ scheduled_end_timestamp: datetime.datetime | None = None
883
+
884
+ @classmethod
885
+ def from_dict(cls, data: dict) -> WaitStartedDetails:
886
+ return cls(
887
+ duration=data.get("Duration"),
888
+ scheduled_end_timestamp=data.get("ScheduledEndTimestamp"),
889
+ )
890
+
891
+ def to_dict(self) -> dict[str, Any]:
892
+ result: dict[str, Any] = {}
893
+ if self.duration is not None:
894
+ result["Duration"] = self.duration
895
+ if self.scheduled_end_timestamp is not None:
896
+ result["ScheduledEndTimestamp"] = self.scheduled_end_timestamp
897
+ return result
898
+
899
+
900
+ @dataclass(frozen=True)
901
+ class WaitSucceededDetails:
902
+ """Wait succeeded event details."""
903
+
904
+ duration: int | None = None
905
+
906
+ @classmethod
907
+ def from_dict(cls, data: dict) -> WaitSucceededDetails:
908
+ return cls(duration=data.get("Duration"))
909
+
910
+ def to_dict(self) -> dict[str, Any]:
911
+ result: dict[str, Any] = {}
912
+ if self.duration is not None:
913
+ result["Duration"] = self.duration
914
+ return result
915
+
916
+
917
+ @dataclass(frozen=True)
918
+ class WaitCancelledDetails:
919
+ """Wait cancelled event details."""
920
+
921
+ error: EventError | None = None
922
+
923
+ @classmethod
924
+ def from_dict(cls, data: dict) -> WaitCancelledDetails:
925
+ error_data = None
926
+ if error_dict := data.get("Error"):
927
+ error_data = EventError.from_dict(error_dict)
928
+
929
+ return cls(error=error_data)
930
+
931
+ def to_dict(self) -> dict[str, Any]:
932
+ result: dict[str, Any] = {}
933
+ if self.error is not None:
934
+ result["Error"] = self.error.to_dict()
935
+ return result
936
+
937
+
938
+ @dataclass(frozen=True)
939
+ class StepStartedDetails:
940
+ """Step started event details."""
941
+
942
+ @classmethod
943
+ def from_dict(cls, data: dict) -> StepStartedDetails: # noqa: ARG003
944
+ return cls()
945
+
946
+ def to_dict(self) -> dict[str, Any]:
947
+ return {}
948
+
949
+
950
+ @dataclass(frozen=True)
951
+ class StepSucceededDetails:
952
+ """Step succeeded event details."""
953
+
954
+ result: EventResult | None = None
955
+ retry_details: RetryDetails | None = None
956
+
957
+ @classmethod
958
+ def from_dict(cls, data: dict) -> StepSucceededDetails:
959
+ result_data = None
960
+ if result_dict := data.get("Result"):
961
+ result_data = EventResult.from_dict(result_dict)
962
+
963
+ retry_details_data = None
964
+ if retry_dict := data.get("RetryDetails"):
965
+ retry_details_data = RetryDetails.from_dict(retry_dict)
966
+
967
+ return cls(result=result_data, retry_details=retry_details_data)
968
+
969
+ def to_dict(self) -> dict[str, Any]:
970
+ result: dict[str, Any] = {}
971
+ if self.result is not None:
972
+ result["Result"] = self.result.to_dict()
973
+ if self.retry_details is not None:
974
+ result["RetryDetails"] = self.retry_details.to_dict()
975
+ return result
976
+
977
+
978
+ @dataclass(frozen=True)
979
+ class StepFailedDetails:
980
+ """Step failed event details."""
981
+
982
+ error: EventError | None = None
983
+ retry_details: RetryDetails | None = None
984
+
985
+ @classmethod
986
+ def from_dict(cls, data: dict) -> StepFailedDetails:
987
+ error_data = None
988
+ if error_dict := data.get("Error"):
989
+ error_data = EventError.from_dict(error_dict)
990
+
991
+ retry_details_data = None
992
+ if retry_dict := data.get("RetryDetails"):
993
+ retry_details_data = RetryDetails.from_dict(retry_dict)
994
+
995
+ return cls(error=error_data, retry_details=retry_details_data)
996
+
997
+ def to_dict(self) -> dict[str, Any]:
998
+ result: dict[str, Any] = {}
999
+ if self.error is not None:
1000
+ result["Error"] = self.error.to_dict()
1001
+ if self.retry_details is not None:
1002
+ result["RetryDetails"] = self.retry_details.to_dict()
1003
+ return result
1004
+
1005
+
1006
+ @dataclass(frozen=True)
1007
+ class ChainedInvokePendingDetails:
1008
+ """Chained Invoke Pending event details."""
1009
+
1010
+ input: EventInput | None = None
1011
+ function_name: str | None = None
1012
+
1013
+ @classmethod
1014
+ def from_dict(cls, data: dict) -> ChainedInvokePendingDetails:
1015
+ input_data = None
1016
+ if input_dict := data.get("Input"):
1017
+ input_data = EventInput.from_dict(input_dict)
1018
+
1019
+ return cls(
1020
+ input=input_data,
1021
+ function_name=data.get("FunctionName"),
1022
+ )
1023
+
1024
+ def to_dict(self) -> dict[str, Any]:
1025
+ result: dict[str, Any] = {}
1026
+ if self.input is not None:
1027
+ result["Input"] = self.input.to_dict()
1028
+ if self.function_name is not None:
1029
+ result["FunctionName"] = self.function_name
1030
+ return result
1031
+
1032
+
1033
+ @dataclass(frozen=True)
1034
+ class ChainedInvokeStartedDetails:
1035
+ """Chained invoke started event details."""
1036
+
1037
+ durable_execution_arn: str | None = None
1038
+
1039
+ @classmethod
1040
+ def from_dict(cls, data: dict) -> ChainedInvokeStartedDetails:
1041
+ return cls(
1042
+ durable_execution_arn=data.get("DurableExecutionArn"),
1043
+ )
1044
+
1045
+ def to_dict(self) -> dict[str, Any]:
1046
+ result: dict[str, Any] = {}
1047
+ if self.durable_execution_arn is not None:
1048
+ result["DurableExecutionArn"] = self.durable_execution_arn
1049
+ return result
1050
+
1051
+
1052
+ @dataclass(frozen=True)
1053
+ class ChainedInvokeSucceededDetails:
1054
+ """Chained invoke succeeded event details."""
1055
+
1056
+ result: EventResult | None = None
1057
+
1058
+ @classmethod
1059
+ def from_dict(cls, data: dict) -> ChainedInvokeSucceededDetails:
1060
+ result_data = None
1061
+ if result_dict := data.get("Result"):
1062
+ result_data = EventResult.from_dict(result_dict)
1063
+
1064
+ return cls(result=result_data)
1065
+
1066
+ def to_dict(self) -> dict[str, Any]:
1067
+ result: dict[str, Any] = {}
1068
+ if self.result is not None:
1069
+ result["Result"] = self.result.to_dict()
1070
+ return result
1071
+
1072
+
1073
+ @dataclass(frozen=True)
1074
+ class ChainedInvokeFailedDetails:
1075
+ """Chained invoke failed event details."""
1076
+
1077
+ error: EventError | None = None
1078
+
1079
+ @classmethod
1080
+ def from_dict(cls, data: dict) -> ChainedInvokeFailedDetails:
1081
+ error_data = None
1082
+ if error_dict := data.get("Error"):
1083
+ error_data = EventError.from_dict(error_dict)
1084
+
1085
+ return cls(error=error_data)
1086
+
1087
+ def to_dict(self) -> dict[str, Any]:
1088
+ result: dict[str, Any] = {}
1089
+ if self.error is not None:
1090
+ result["Error"] = self.error.to_dict()
1091
+ return result
1092
+
1093
+
1094
+ @dataclass(frozen=True)
1095
+ class ChainedInvokeTimedOutDetails:
1096
+ """Chained invoke timed out event details."""
1097
+
1098
+ error: EventError | None = None
1099
+
1100
+ @classmethod
1101
+ def from_dict(cls, data: dict) -> ChainedInvokeTimedOutDetails:
1102
+ error_data = None
1103
+ if error_dict := data.get("Error"):
1104
+ error_data = EventError.from_dict(error_dict)
1105
+
1106
+ return cls(error=error_data)
1107
+
1108
+ def to_dict(self) -> dict[str, Any]:
1109
+ result: dict[str, Any] = {}
1110
+ if self.error is not None:
1111
+ result["Error"] = self.error.to_dict()
1112
+ return result
1113
+
1114
+
1115
+ @dataclass(frozen=True)
1116
+ class ChainedInvokeStoppedDetails:
1117
+ """Chained invoke stopped event details."""
1118
+
1119
+ error: EventError | None = None
1120
+
1121
+ @classmethod
1122
+ def from_dict(cls, data: dict) -> ChainedInvokeStoppedDetails:
1123
+ error_data = None
1124
+ if error_dict := data.get("Error"):
1125
+ error_data = EventError.from_dict(error_dict)
1126
+
1127
+ return cls(error=error_data)
1128
+
1129
+ def to_dict(self) -> dict[str, Any]:
1130
+ result: dict[str, Any] = {}
1131
+ if self.error is not None:
1132
+ result["Error"] = self.error.to_dict()
1133
+ return result
1134
+
1135
+
1136
+ @dataclass(frozen=True)
1137
+ class CallbackStartedDetails:
1138
+ """Callback started event details."""
1139
+
1140
+ callback_id: str | None = None
1141
+ heartbeat_timeout: int | None = None
1142
+ timeout: int | None = None
1143
+
1144
+ @classmethod
1145
+ def from_dict(cls, data: dict) -> CallbackStartedDetails:
1146
+ return cls(
1147
+ callback_id=data.get("CallbackId"),
1148
+ heartbeat_timeout=data.get("HeartbeatTimeout"),
1149
+ timeout=data.get("Timeout"),
1150
+ )
1151
+
1152
+ def to_dict(self) -> dict[str, Any]:
1153
+ result: dict[str, Any] = {}
1154
+ if self.callback_id is not None:
1155
+ result["CallbackId"] = self.callback_id
1156
+ if self.heartbeat_timeout is not None:
1157
+ result["HeartbeatTimeout"] = self.heartbeat_timeout
1158
+ if self.timeout is not None:
1159
+ result["Timeout"] = self.timeout
1160
+ return result
1161
+
1162
+
1163
+ @dataclass(frozen=True)
1164
+ class CallbackSucceededDetails:
1165
+ """Callback succeeded event details."""
1166
+
1167
+ result: EventResult | None = None
1168
+
1169
+ @classmethod
1170
+ def from_dict(cls, data: dict) -> CallbackSucceededDetails:
1171
+ result_data = None
1172
+ if result_dict := data.get("Result"):
1173
+ result_data = EventResult.from_dict(result_dict)
1174
+
1175
+ return cls(result=result_data)
1176
+
1177
+ def to_dict(self) -> dict[str, Any]:
1178
+ result: dict[str, Any] = {}
1179
+ if self.result is not None:
1180
+ result["Result"] = self.result.to_dict()
1181
+ return result
1182
+
1183
+
1184
+ @dataclass(frozen=True)
1185
+ class CallbackFailedDetails:
1186
+ """Callback failed event details."""
1187
+
1188
+ error: EventError | None = None
1189
+
1190
+ @classmethod
1191
+ def from_dict(cls, data: dict) -> CallbackFailedDetails:
1192
+ error_data = None
1193
+ if error_dict := data.get("Error"):
1194
+ error_data = EventError.from_dict(error_dict)
1195
+
1196
+ return cls(error=error_data)
1197
+
1198
+ def to_dict(self) -> dict[str, Any]:
1199
+ result: dict[str, Any] = {}
1200
+ if self.error is not None:
1201
+ result["Error"] = self.error.to_dict()
1202
+ return result
1203
+
1204
+
1205
+ @dataclass(frozen=True)
1206
+ class CallbackTimedOutDetails:
1207
+ """Callback timed out event details."""
1208
+
1209
+ error: EventError | None = None
1210
+
1211
+ @classmethod
1212
+ def from_dict(cls, data: dict) -> CallbackTimedOutDetails:
1213
+ error_data = None
1214
+ if error_dict := data.get("Error"):
1215
+ error_data = EventError.from_dict(error_dict)
1216
+
1217
+ return cls(error=error_data)
1218
+
1219
+ def to_dict(self) -> dict[str, Any]:
1220
+ result: dict[str, Any] = {}
1221
+ if self.error is not None:
1222
+ result["Error"] = self.error.to_dict()
1223
+ return result
1224
+
1225
+
1226
+ @dataclass(frozen=True)
1227
+ class InvocationCompletedDetails:
1228
+ """Invocation completed event details."""
1229
+
1230
+ start_timestamp: datetime.datetime
1231
+ end_timestamp: datetime.datetime
1232
+ request_id: str
1233
+
1234
+ @classmethod
1235
+ def from_dict(cls, data: dict) -> InvocationCompletedDetails:
1236
+ return cls(
1237
+ start_timestamp=data["StartTimestamp"],
1238
+ end_timestamp=data["EndTimestamp"],
1239
+ request_id=data["RequestId"],
1240
+ )
1241
+
1242
+ def to_dict(self) -> dict[str, Any]:
1243
+ return {
1244
+ "StartTimestamp": self.start_timestamp,
1245
+ "EndTimestamp": self.end_timestamp,
1246
+ "RequestId": self.request_id,
1247
+ }
1248
+
1249
+
1250
+ # endregion event_structures
1251
+
1252
+
1253
+ @dataclass(frozen=True)
1254
+ class EventCreationContext:
1255
+ operation: Operation
1256
+ event_id: int
1257
+ durable_execution_arn: str
1258
+ start_durable_execution_input: StartDurableExecutionInput
1259
+ durable_execution_invocation_output: DurableExecutionInvocationOutput | None = None
1260
+ operation_update: OperationUpdate | None = None
1261
+ include_execution_data: bool = False # noqa: FBT001, FBT002
1262
+
1263
+ @classmethod
1264
+ def create(
1265
+ cls,
1266
+ operation: Operation,
1267
+ event_id: int,
1268
+ durable_execution_arn: str,
1269
+ start_input: StartDurableExecutionInput,
1270
+ result: DurableExecutionInvocationOutput | None = None,
1271
+ operation_update: OperationUpdate | None = None,
1272
+ include_execution_data: bool = False, # noqa: FBT001, FBT002
1273
+ ) -> EventCreationContext:
1274
+ return cls(
1275
+ operation=operation,
1276
+ event_id=event_id,
1277
+ durable_execution_arn=durable_execution_arn,
1278
+ start_durable_execution_input=start_input,
1279
+ durable_execution_invocation_output=result,
1280
+ operation_update=operation_update,
1281
+ include_execution_data=include_execution_data,
1282
+ )
1283
+
1284
+ @property
1285
+ def sub_type(self) -> str | None:
1286
+ return self.operation.sub_type.value if self.operation.sub_type else None
1287
+
1288
+ def get_retry_details(self) -> RetryDetails | None:
1289
+ if not self.operation.step_details or not self.operation_update:
1290
+ return None
1291
+
1292
+ delay = 0
1293
+ if (
1294
+ self.operation_update.operation_type == OperationType.STEP
1295
+ and self.operation_update.step_options
1296
+ ):
1297
+ delay = self.operation_update.step_options.next_attempt_delay_seconds
1298
+
1299
+ return RetryDetails(
1300
+ current_attempt=self.operation.step_details.attempt,
1301
+ next_attempt_delay_seconds=delay,
1302
+ )
1303
+
1304
+ @property
1305
+ def start_timestamp(self) -> datetime.datetime:
1306
+ return (
1307
+ self.operation.start_timestamp
1308
+ if self.operation.start_timestamp is not None
1309
+ else datetime.datetime.now(UTC)
1310
+ )
1311
+
1312
+ @property
1313
+ def end_timestamp(self) -> datetime.datetime:
1314
+ return (
1315
+ self.operation.end_timestamp
1316
+ if self.operation.end_timestamp is not None
1317
+ else datetime.datetime.now(UTC)
1318
+ )
1319
+
1320
+
1321
+ # region event_class
1322
+ @dataclass(frozen=True)
1323
+ class Event:
1324
+ """Event structure from Smithy model."""
1325
+
1326
+ event_type: str
1327
+ event_timestamp: datetime.datetime
1328
+ sub_type: str | None = None
1329
+ event_id: int = 1
1330
+ operation_id: str | None = None
1331
+ name: str | None = None
1332
+ parent_id: str | None = None
1333
+ execution_started_details: ExecutionStartedDetails | None = None
1334
+ execution_succeeded_details: ExecutionSucceededDetails | None = None
1335
+ execution_failed_details: ExecutionFailedDetails | None = None
1336
+ execution_timed_out_details: ExecutionTimedOutDetails | None = None
1337
+ execution_stopped_details: ExecutionStoppedDetails | None = None
1338
+ context_started_details: ContextStartedDetails | None = None
1339
+ context_succeeded_details: ContextSucceededDetails | None = None
1340
+ context_failed_details: ContextFailedDetails | None = None
1341
+ wait_started_details: WaitStartedDetails | None = None
1342
+ wait_succeeded_details: WaitSucceededDetails | None = None
1343
+ wait_cancelled_details: WaitCancelledDetails | None = None
1344
+ step_started_details: StepStartedDetails | None = None
1345
+ step_succeeded_details: StepSucceededDetails | None = None
1346
+ step_failed_details: StepFailedDetails | None = None
1347
+ chained_invoke_pending_details: ChainedInvokePendingDetails | None = None
1348
+ chained_invoke_started_details: ChainedInvokeStartedDetails | None = None
1349
+ chained_invoke_succeeded_details: ChainedInvokeSucceededDetails | None = None
1350
+ chained_invoke_failed_details: ChainedInvokeFailedDetails | None = None
1351
+ chained_invoke_timed_out_details: ChainedInvokeTimedOutDetails | None = None
1352
+ chained_invoke_stopped_details: ChainedInvokeStoppedDetails | None = None
1353
+ callback_started_details: CallbackStartedDetails | None = None
1354
+ callback_succeeded_details: CallbackSucceededDetails | None = None
1355
+ callback_failed_details: CallbackFailedDetails | None = None
1356
+ callback_timed_out_details: CallbackTimedOutDetails | None = None
1357
+ invocation_completed_details: InvocationCompletedDetails | None = None
1358
+
1359
+ @classmethod
1360
+ def from_dict(cls, data: dict) -> Event:
1361
+ # Parse all the detail structures
1362
+ execution_started_details = None
1363
+ if details_data := data.get("ExecutionStartedDetails"):
1364
+ execution_started_details = ExecutionStartedDetails.from_dict(details_data)
1365
+
1366
+ execution_succeeded_details = None
1367
+ if details_data := data.get("ExecutionSucceededDetails"):
1368
+ execution_succeeded_details = ExecutionSucceededDetails.from_dict(
1369
+ details_data
1370
+ )
1371
+
1372
+ execution_failed_details = None
1373
+ if details_data := data.get("ExecutionFailedDetails"):
1374
+ execution_failed_details = ExecutionFailedDetails.from_dict(details_data)
1375
+
1376
+ execution_timed_out_details = None
1377
+ if details_data := data.get("ExecutionTimedOutDetails"):
1378
+ execution_timed_out_details = ExecutionTimedOutDetails.from_dict(
1379
+ details_data
1380
+ )
1381
+
1382
+ execution_stopped_details = None
1383
+ if details_data := data.get("ExecutionStoppedDetails"):
1384
+ execution_stopped_details = ExecutionStoppedDetails.from_dict(details_data)
1385
+
1386
+ context_started_details = None
1387
+ if details_data := data.get("ContextStartedDetails"):
1388
+ context_started_details = ContextStartedDetails.from_dict(details_data)
1389
+
1390
+ context_succeeded_details = None
1391
+ if details_data := data.get("ContextSucceededDetails"):
1392
+ context_succeeded_details = ContextSucceededDetails.from_dict(details_data)
1393
+
1394
+ context_failed_details = None
1395
+ if details_data := data.get("ContextFailedDetails"):
1396
+ context_failed_details = ContextFailedDetails.from_dict(details_data)
1397
+
1398
+ wait_started_details = None
1399
+ if details_data := data.get("WaitStartedDetails"):
1400
+ wait_started_details = WaitStartedDetails.from_dict(details_data)
1401
+
1402
+ wait_succeeded_details = None
1403
+ if details_data := data.get("WaitSucceededDetails"):
1404
+ wait_succeeded_details = WaitSucceededDetails.from_dict(details_data)
1405
+
1406
+ wait_cancelled_details = None
1407
+ if details_data := data.get("WaitCancelledDetails"):
1408
+ wait_cancelled_details = WaitCancelledDetails.from_dict(details_data)
1409
+
1410
+ step_started_details = None
1411
+ if details_data := data.get("StepStartedDetails"):
1412
+ step_started_details = StepStartedDetails.from_dict(details_data)
1413
+
1414
+ step_succeeded_details = None
1415
+ if details_data := data.get("StepSucceededDetails"):
1416
+ step_succeeded_details = StepSucceededDetails.from_dict(details_data)
1417
+
1418
+ step_failed_details = None
1419
+ if details_data := data.get("StepFailedDetails"):
1420
+ step_failed_details = StepFailedDetails.from_dict(details_data)
1421
+
1422
+ chained_invoke_pending_details = None
1423
+ if details_data := data.get("ChainedInvokePendingDetails"):
1424
+ chained_invoke_pending_details = ChainedInvokePendingDetails.from_dict(
1425
+ details_data
1426
+ )
1427
+
1428
+ chained_invoke_started_details = None
1429
+ if details_data := data.get("ChainedInvokeStartedDetails"):
1430
+ chained_invoke_started_details = ChainedInvokeStartedDetails.from_dict(
1431
+ details_data
1432
+ )
1433
+
1434
+ chained_invoke_succeeded_details = None
1435
+ if details_data := data.get("ChainedInvokeSucceededDetails"):
1436
+ chained_invoke_succeeded_details = ChainedInvokeSucceededDetails.from_dict(
1437
+ details_data
1438
+ )
1439
+
1440
+ chained_invoke_failed_details = None
1441
+ if details_data := data.get("ChainedInvokeFailedDetails"):
1442
+ chained_invoke_failed_details = ChainedInvokeFailedDetails.from_dict(
1443
+ details_data
1444
+ )
1445
+
1446
+ chained_invoke_timed_out_details = None
1447
+ if details_data := data.get("ChainedInvokeTimedOutDetails"):
1448
+ chained_invoke_timed_out_details = ChainedInvokeTimedOutDetails.from_dict(
1449
+ details_data
1450
+ )
1451
+
1452
+ chained_invoke_stopped_details = None
1453
+ if details_data := data.get("ChainedInvokeStoppedDetails"):
1454
+ chained_invoke_stopped_details = ChainedInvokeStoppedDetails.from_dict(
1455
+ details_data
1456
+ )
1457
+
1458
+ callback_started_details = None
1459
+ if details_data := data.get("CallbackStartedDetails"):
1460
+ callback_started_details = CallbackStartedDetails.from_dict(details_data)
1461
+
1462
+ callback_succeeded_details = None
1463
+ if details_data := data.get("CallbackSucceededDetails"):
1464
+ callback_succeeded_details = CallbackSucceededDetails.from_dict(
1465
+ details_data
1466
+ )
1467
+
1468
+ callback_failed_details = None
1469
+ if details_data := data.get("CallbackFailedDetails"):
1470
+ callback_failed_details = CallbackFailedDetails.from_dict(details_data)
1471
+
1472
+ callback_timed_out_details = None
1473
+ if details_data := data.get("CallbackTimedOutDetails"):
1474
+ callback_timed_out_details = CallbackTimedOutDetails.from_dict(details_data)
1475
+
1476
+ invocation_completed_details = None
1477
+ if details_data := data.get("InvocationCompletedDetails"):
1478
+ invocation_completed_details = InvocationCompletedDetails.from_dict(
1479
+ details_data
1480
+ )
1481
+
1482
+ return cls(
1483
+ event_type=data["EventType"],
1484
+ event_timestamp=data["EventTimestamp"],
1485
+ sub_type=data.get("SubType"),
1486
+ event_id=data.get("EventId", 1),
1487
+ operation_id=data.get("Id"),
1488
+ name=data.get("Name"),
1489
+ parent_id=data.get("ParentId"),
1490
+ execution_started_details=execution_started_details,
1491
+ execution_succeeded_details=execution_succeeded_details,
1492
+ execution_failed_details=execution_failed_details,
1493
+ execution_timed_out_details=execution_timed_out_details,
1494
+ execution_stopped_details=execution_stopped_details,
1495
+ context_started_details=context_started_details,
1496
+ context_succeeded_details=context_succeeded_details,
1497
+ context_failed_details=context_failed_details,
1498
+ wait_started_details=wait_started_details,
1499
+ wait_succeeded_details=wait_succeeded_details,
1500
+ wait_cancelled_details=wait_cancelled_details,
1501
+ step_started_details=step_started_details,
1502
+ step_succeeded_details=step_succeeded_details,
1503
+ step_failed_details=step_failed_details,
1504
+ chained_invoke_pending_details=chained_invoke_pending_details,
1505
+ chained_invoke_started_details=chained_invoke_started_details,
1506
+ chained_invoke_succeeded_details=chained_invoke_succeeded_details,
1507
+ chained_invoke_failed_details=chained_invoke_failed_details,
1508
+ chained_invoke_timed_out_details=chained_invoke_timed_out_details,
1509
+ chained_invoke_stopped_details=chained_invoke_stopped_details,
1510
+ callback_started_details=callback_started_details,
1511
+ callback_succeeded_details=callback_succeeded_details,
1512
+ callback_failed_details=callback_failed_details,
1513
+ callback_timed_out_details=callback_timed_out_details,
1514
+ invocation_completed_details=invocation_completed_details,
1515
+ )
1516
+
1517
+ def to_dict(self) -> dict[str, Any]:
1518
+ result: dict[str, Any] = {
1519
+ "EventType": self.event_type,
1520
+ "EventTimestamp": self.event_timestamp,
1521
+ "EventId": self.event_id,
1522
+ }
1523
+ if self.sub_type is not None:
1524
+ result["SubType"] = self.sub_type
1525
+ if self.operation_id is not None:
1526
+ result["Id"] = self.operation_id
1527
+ if self.name is not None:
1528
+ result["Name"] = self.name
1529
+ if self.parent_id is not None:
1530
+ result["ParentId"] = self.parent_id
1531
+ if self.execution_started_details is not None:
1532
+ result["ExecutionStartedDetails"] = self.execution_started_details.to_dict()
1533
+ if self.execution_succeeded_details is not None:
1534
+ result["ExecutionSucceededDetails"] = (
1535
+ self.execution_succeeded_details.to_dict()
1536
+ )
1537
+ if self.execution_failed_details is not None:
1538
+ result["ExecutionFailedDetails"] = self.execution_failed_details.to_dict()
1539
+ if self.execution_timed_out_details is not None:
1540
+ result["ExecutionTimedOutDetails"] = (
1541
+ self.execution_timed_out_details.to_dict()
1542
+ )
1543
+ if self.execution_stopped_details is not None:
1544
+ result["ExecutionStoppedDetails"] = self.execution_stopped_details.to_dict()
1545
+ if self.context_started_details is not None:
1546
+ result["ContextStartedDetails"] = self.context_started_details.to_dict()
1547
+ if self.context_succeeded_details is not None:
1548
+ result["ContextSucceededDetails"] = self.context_succeeded_details.to_dict()
1549
+ if self.context_failed_details is not None:
1550
+ result["ContextFailedDetails"] = self.context_failed_details.to_dict()
1551
+ if self.wait_started_details is not None:
1552
+ result["WaitStartedDetails"] = self.wait_started_details.to_dict()
1553
+ if self.wait_succeeded_details is not None:
1554
+ result["WaitSucceededDetails"] = self.wait_succeeded_details.to_dict()
1555
+ if self.wait_cancelled_details is not None:
1556
+ result["WaitCancelledDetails"] = self.wait_cancelled_details.to_dict()
1557
+ if self.step_started_details is not None:
1558
+ result["StepStartedDetails"] = self.step_started_details.to_dict()
1559
+ if self.step_succeeded_details is not None:
1560
+ result["StepSucceededDetails"] = self.step_succeeded_details.to_dict()
1561
+ if self.step_failed_details is not None:
1562
+ result["StepFailedDetails"] = self.step_failed_details.to_dict()
1563
+ if self.chained_invoke_pending_details is not None:
1564
+ result["ChainedInvokePendingDetails"] = (
1565
+ self.chained_invoke_pending_details.to_dict()
1566
+ )
1567
+ if self.chained_invoke_started_details is not None:
1568
+ result["ChainedInvokeStartedDetails"] = (
1569
+ self.chained_invoke_started_details.to_dict()
1570
+ )
1571
+ if self.chained_invoke_succeeded_details is not None:
1572
+ result["ChainedInvokeSucceededDetails"] = (
1573
+ self.chained_invoke_succeeded_details.to_dict()
1574
+ )
1575
+ if self.chained_invoke_failed_details is not None:
1576
+ result["ChainedInvokeFailedDetails"] = (
1577
+ self.chained_invoke_failed_details.to_dict()
1578
+ )
1579
+ if self.chained_invoke_timed_out_details is not None:
1580
+ result["ChainedInvokeTimedOutDetails"] = (
1581
+ self.chained_invoke_timed_out_details.to_dict()
1582
+ )
1583
+ if self.chained_invoke_stopped_details is not None:
1584
+ result["ChainedInvokeStoppedDetails"] = (
1585
+ self.chained_invoke_stopped_details.to_dict()
1586
+ )
1587
+ if self.callback_started_details is not None:
1588
+ result["CallbackStartedDetails"] = self.callback_started_details.to_dict()
1589
+ if self.callback_succeeded_details is not None:
1590
+ result["CallbackSucceededDetails"] = (
1591
+ self.callback_succeeded_details.to_dict()
1592
+ )
1593
+ if self.callback_failed_details is not None:
1594
+ result["CallbackFailedDetails"] = self.callback_failed_details.to_dict()
1595
+ if self.callback_timed_out_details is not None:
1596
+ result["CallbackTimedOutDetails"] = (
1597
+ self.callback_timed_out_details.to_dict()
1598
+ )
1599
+ if self.invocation_completed_details is not None:
1600
+ result["InvocationCompletedDetails"] = (
1601
+ self.invocation_completed_details.to_dict()
1602
+ )
1603
+ return result
1604
+
1605
+ # region execution
1606
+ @classmethod
1607
+ def create_execution_event_started(cls, context: EventCreationContext) -> Event:
1608
+ execution_details: ExecutionDetails | None = context.operation.execution_details
1609
+ event_input: EventInput | None = (
1610
+ EventInput.from_details(execution_details, context.include_execution_data)
1611
+ if execution_details
1612
+ else None
1613
+ )
1614
+ execution_timeout: int | None = (
1615
+ context.start_durable_execution_input.execution_timeout_seconds
1616
+ )
1617
+
1618
+ return cls(
1619
+ event_type=EventType.EXECUTION_STARTED.value,
1620
+ event_timestamp=context.start_timestamp,
1621
+ sub_type=context.sub_type,
1622
+ event_id=context.event_id,
1623
+ operation_id=context.operation.operation_id,
1624
+ name=context.operation.name,
1625
+ parent_id=context.operation.parent_id,
1626
+ execution_started_details=ExecutionStartedDetails(
1627
+ input=event_input,
1628
+ execution_timeout=execution_timeout,
1629
+ ),
1630
+ )
1631
+
1632
+ @classmethod
1633
+ def create_execution_event_succeeded(cls, context: EventCreationContext) -> Event:
1634
+ result: EventResult | None = (
1635
+ EventResult.from_durable_execution_invocation_output(
1636
+ context.durable_execution_invocation_output,
1637
+ context.include_execution_data,
1638
+ )
1639
+ if context.durable_execution_invocation_output
1640
+ else None
1641
+ )
1642
+ return cls(
1643
+ event_type=EventType.EXECUTION_SUCCEEDED.value,
1644
+ event_timestamp=context.end_timestamp,
1645
+ sub_type=context.sub_type,
1646
+ event_id=context.event_id,
1647
+ operation_id=context.operation.operation_id,
1648
+ name=context.operation.name,
1649
+ parent_id=context.operation.parent_id,
1650
+ execution_succeeded_details=ExecutionSucceededDetails(result=result),
1651
+ )
1652
+
1653
+ @classmethod
1654
+ def create_execution_event_failed(cls, context: EventCreationContext) -> Event:
1655
+ error: EventError | None = (
1656
+ EventError.from_durable_execution_invocation_output(
1657
+ context.durable_execution_invocation_output,
1658
+ include=context.include_execution_data,
1659
+ )
1660
+ if context.durable_execution_invocation_output
1661
+ else None
1662
+ )
1663
+ return cls(
1664
+ event_type=EventType.EXECUTION_FAILED.value,
1665
+ event_timestamp=context.end_timestamp,
1666
+ sub_type=context.sub_type,
1667
+ event_id=context.event_id,
1668
+ operation_id=context.operation.operation_id,
1669
+ name=context.operation.name,
1670
+ parent_id=context.operation.parent_id,
1671
+ execution_failed_details=ExecutionFailedDetails(error=error),
1672
+ )
1673
+
1674
+ @classmethod
1675
+ def create_execution_event_timed_out(cls, context: EventCreationContext) -> Event:
1676
+ error: EventError | None = (
1677
+ EventError.from_durable_execution_invocation_output(
1678
+ context.durable_execution_invocation_output,
1679
+ include=context.include_execution_data,
1680
+ )
1681
+ if context.durable_execution_invocation_output
1682
+ else None
1683
+ )
1684
+ return cls(
1685
+ event_type=EventType.EXECUTION_TIMED_OUT.value,
1686
+ event_timestamp=context.end_timestamp,
1687
+ sub_type=context.sub_type,
1688
+ event_id=context.event_id,
1689
+ operation_id=context.operation.operation_id,
1690
+ name=context.operation.name,
1691
+ parent_id=context.operation.parent_id,
1692
+ execution_timed_out_details=ExecutionTimedOutDetails(error=error),
1693
+ )
1694
+
1695
+ @classmethod
1696
+ def create_execution_event_stopped(cls, context: EventCreationContext) -> Event:
1697
+ error: EventError | None = (
1698
+ EventError.from_durable_execution_invocation_output(
1699
+ context.durable_execution_invocation_output,
1700
+ include=context.include_execution_data,
1701
+ )
1702
+ if context.durable_execution_invocation_output
1703
+ else None
1704
+ )
1705
+ return cls(
1706
+ event_type=EventType.EXECUTION_STOPPED.value,
1707
+ event_timestamp=context.end_timestamp,
1708
+ sub_type=context.sub_type,
1709
+ event_id=context.event_id,
1710
+ operation_id=context.operation.operation_id,
1711
+ name=context.operation.name,
1712
+ parent_id=context.operation.parent_id,
1713
+ execution_stopped_details=ExecutionStoppedDetails(error=error),
1714
+ )
1715
+
1716
+ @classmethod
1717
+ def create_execution_event(cls, context: EventCreationContext) -> Event:
1718
+ """Create execution event based on action."""
1719
+ match context.operation.status:
1720
+ case OperationStatus.STARTED:
1721
+ return cls.create_execution_event_started(context)
1722
+ case OperationStatus.SUCCEEDED:
1723
+ return cls.create_execution_event_succeeded(context)
1724
+ case OperationStatus.FAILED:
1725
+ return cls.create_execution_event_failed(context)
1726
+ case OperationStatus.TIMED_OUT:
1727
+ return cls.create_execution_event_timed_out(context)
1728
+ case OperationStatus.STOPPED:
1729
+ return cls.create_execution_event_stopped(context)
1730
+ case _:
1731
+ msg = f"Operation status {context.operation.status} is not valid for execution operations. Valid statuses are: STARTED, SUCCEEDED, FAILED, TIMED_OUT, STOPPED"
1732
+ raise InvalidParameterValueException(msg)
1733
+
1734
+ # endregion execution
1735
+
1736
+ # region context
1737
+ @classmethod
1738
+ def create_context_event_started(cls, context: EventCreationContext) -> Event:
1739
+ return cls(
1740
+ event_type=EventType.CONTEXT_STARTED.value,
1741
+ event_timestamp=context.start_timestamp,
1742
+ sub_type=context.sub_type,
1743
+ event_id=context.event_id,
1744
+ operation_id=context.operation.operation_id,
1745
+ name=context.operation.name,
1746
+ parent_id=context.operation.parent_id,
1747
+ context_started_details=ContextStartedDetails(),
1748
+ )
1749
+
1750
+ @classmethod
1751
+ def create_context_event_succeeded(cls, context: EventCreationContext) -> Event:
1752
+ context_details: ContextDetails | None = context.operation.context_details
1753
+ event_result: EventResult | None = (
1754
+ EventResult.from_details(context_details, context.include_execution_data)
1755
+ if context_details
1756
+ else None
1757
+ )
1758
+ return cls(
1759
+ event_type=EventType.CONTEXT_SUCCEEDED.value,
1760
+ event_timestamp=context.end_timestamp,
1761
+ sub_type=context.sub_type,
1762
+ event_id=context.event_id,
1763
+ operation_id=context.operation.operation_id,
1764
+ name=context.operation.name,
1765
+ parent_id=context.operation.parent_id,
1766
+ context_succeeded_details=ContextSucceededDetails(result=event_result),
1767
+ )
1768
+
1769
+ @classmethod
1770
+ def create_context_event_failed(cls, context: EventCreationContext) -> Event:
1771
+ context_details: ContextDetails | None = context.operation.context_details
1772
+ event_error: EventError | None = (
1773
+ EventError.from_details(context_details) if context_details else None
1774
+ )
1775
+ return cls(
1776
+ event_type=EventType.CONTEXT_FAILED.value,
1777
+ event_timestamp=context.end_timestamp,
1778
+ sub_type=context.sub_type,
1779
+ event_id=context.event_id,
1780
+ operation_id=context.operation.operation_id,
1781
+ name=context.operation.name,
1782
+ parent_id=context.operation.parent_id,
1783
+ context_failed_details=ContextFailedDetails(error=event_error),
1784
+ )
1785
+
1786
+ @classmethod
1787
+ def create_context_event(cls, context: EventCreationContext) -> Event:
1788
+ """Create context event based on action."""
1789
+ match context.operation.status:
1790
+ case OperationStatus.STARTED:
1791
+ return cls.create_context_event_started(context)
1792
+ case OperationStatus.SUCCEEDED:
1793
+ return cls.create_context_event_succeeded(context)
1794
+ case OperationStatus.FAILED:
1795
+ return cls.create_context_event_failed(context)
1796
+ case _:
1797
+ msg = (
1798
+ f"Operation status {context.operation.status} is not valid for context operations. "
1799
+ f"Valid statuses are: STARTED, SUCCEEDED, FAILED"
1800
+ )
1801
+ raise InvalidParameterValueException(msg)
1802
+
1803
+ # endregion context
1804
+
1805
+ # region wait
1806
+ @classmethod
1807
+ def create_wait_event_started(cls, context: EventCreationContext) -> Event:
1808
+ wait_details: WaitDetails | None = context.operation.wait_details
1809
+ scheduled_end_timestamp: datetime.datetime | None = (
1810
+ wait_details.scheduled_end_timestamp if wait_details else None
1811
+ )
1812
+ duration: int | None = None
1813
+ if (
1814
+ wait_details
1815
+ and wait_details.scheduled_end_timestamp
1816
+ and context.operation.start_timestamp
1817
+ ):
1818
+ duration = round(
1819
+ (
1820
+ wait_details.scheduled_end_timestamp
1821
+ - context.operation.start_timestamp
1822
+ ).total_seconds()
1823
+ )
1824
+ return cls(
1825
+ event_type=EventType.WAIT_STARTED.value,
1826
+ event_timestamp=context.start_timestamp,
1827
+ sub_type=context.sub_type,
1828
+ event_id=context.event_id,
1829
+ operation_id=context.operation.operation_id,
1830
+ name=context.operation.name,
1831
+ parent_id=context.operation.parent_id,
1832
+ wait_started_details=WaitStartedDetails(
1833
+ duration=duration,
1834
+ scheduled_end_timestamp=scheduled_end_timestamp,
1835
+ ),
1836
+ )
1837
+
1838
+ @classmethod
1839
+ def create_wait_event_succeeded(cls, context: EventCreationContext) -> Event:
1840
+ wait_details: WaitDetails | None = context.operation.wait_details
1841
+ duration: int | None = None
1842
+ if (
1843
+ wait_details
1844
+ and wait_details.scheduled_end_timestamp
1845
+ and context.operation.start_timestamp
1846
+ ):
1847
+ duration = round(
1848
+ (
1849
+ wait_details.scheduled_end_timestamp - context.start_timestamp
1850
+ ).total_seconds()
1851
+ )
1852
+ return cls(
1853
+ event_type=EventType.WAIT_SUCCEEDED.value,
1854
+ event_timestamp=context.end_timestamp,
1855
+ sub_type=context.sub_type,
1856
+ event_id=context.event_id,
1857
+ operation_id=context.operation.operation_id,
1858
+ name=context.operation.name,
1859
+ parent_id=context.operation.parent_id,
1860
+ wait_succeeded_details=WaitSucceededDetails(duration=duration),
1861
+ )
1862
+
1863
+ @classmethod
1864
+ def create_wait_event_cancelled(cls, context: EventCreationContext) -> Event:
1865
+ error: EventError | None = None
1866
+ if (
1867
+ context.operation_update
1868
+ and context.operation_update.operation_type == OperationType.WAIT
1869
+ and context.operation_update.action == OperationAction.CANCEL
1870
+ ):
1871
+ error = EventError(
1872
+ context.operation_update.error, not context.include_execution_data
1873
+ )
1874
+ return cls(
1875
+ event_type=EventType.WAIT_CANCELLED.value,
1876
+ event_timestamp=context.end_timestamp,
1877
+ sub_type=context.sub_type,
1878
+ event_id=context.event_id,
1879
+ operation_id=context.operation.operation_id,
1880
+ name=context.operation.name,
1881
+ parent_id=context.operation.parent_id,
1882
+ wait_cancelled_details=WaitCancelledDetails(error=error),
1883
+ )
1884
+
1885
+ @classmethod
1886
+ def create_wait_event(cls, context: EventCreationContext) -> Event:
1887
+ """Create wait event based on action."""
1888
+ match context.operation.status:
1889
+ case OperationStatus.STARTED:
1890
+ return cls.create_wait_event_started(context)
1891
+ case OperationStatus.SUCCEEDED:
1892
+ return cls.create_wait_event_succeeded(context)
1893
+ case OperationStatus.CANCELLED:
1894
+ return cls.create_wait_event_cancelled(context)
1895
+ case _:
1896
+ msg = (
1897
+ f"Operation status {context.operation.status} is not valid for wait operations. "
1898
+ f"Valid statuses are: STARTED, SUCCEEDED, CANCELLED"
1899
+ )
1900
+ raise InvalidParameterValueException(msg)
1901
+
1902
+ # endregion wait
1903
+
1904
+ # region step
1905
+ @classmethod
1906
+ def create_step_event_started(cls, context: EventCreationContext) -> Event:
1907
+ return cls(
1908
+ event_type=EventType.STEP_STARTED.value,
1909
+ event_timestamp=context.start_timestamp,
1910
+ sub_type=context.sub_type,
1911
+ event_id=context.event_id,
1912
+ operation_id=context.operation.operation_id,
1913
+ name=context.operation.name,
1914
+ parent_id=context.operation.parent_id,
1915
+ step_started_details=StepStartedDetails(),
1916
+ )
1917
+
1918
+ @classmethod
1919
+ def create_step_event_succeeded(cls, context: EventCreationContext) -> Event:
1920
+ step_details: StepDetails | None = context.operation.step_details
1921
+ event_result: EventResult | None = (
1922
+ EventResult.from_details(step_details, context.include_execution_data)
1923
+ if step_details
1924
+ else None
1925
+ )
1926
+ return cls(
1927
+ event_type=EventType.STEP_SUCCEEDED.value,
1928
+ event_timestamp=context.end_timestamp,
1929
+ sub_type=context.sub_type,
1930
+ event_id=context.event_id,
1931
+ operation_id=context.operation.operation_id,
1932
+ name=context.operation.name,
1933
+ parent_id=context.operation.parent_id,
1934
+ step_succeeded_details=StepSucceededDetails(
1935
+ result=event_result,
1936
+ retry_details=context.get_retry_details(),
1937
+ ),
1938
+ )
1939
+
1940
+ @classmethod
1941
+ def create_step_event_failed(cls, context: EventCreationContext) -> Event:
1942
+ step_details: StepDetails | None = context.operation.step_details
1943
+ event_error: EventError | None = (
1944
+ EventError.from_details(
1945
+ step_details, include=context.include_execution_data
1946
+ )
1947
+ if step_details
1948
+ else None
1949
+ )
1950
+ return cls(
1951
+ event_type=EventType.STEP_FAILED.value,
1952
+ event_timestamp=context.end_timestamp,
1953
+ sub_type=context.sub_type,
1954
+ event_id=context.event_id,
1955
+ operation_id=context.operation.operation_id,
1956
+ name=context.operation.name,
1957
+ parent_id=context.operation.parent_id,
1958
+ step_failed_details=StepFailedDetails(
1959
+ error=event_error,
1960
+ retry_details=context.get_retry_details(),
1961
+ ),
1962
+ )
1963
+
1964
+ @classmethod
1965
+ def create_step_event(cls, context: EventCreationContext) -> Event:
1966
+ """Create step event based on action."""
1967
+ match context.operation.status:
1968
+ case OperationStatus.STARTED:
1969
+ return cls.create_step_event_started(context)
1970
+ case OperationStatus.SUCCEEDED:
1971
+ return cls.create_step_event_succeeded(context)
1972
+ case OperationStatus.FAILED:
1973
+ return cls.create_step_event_failed(context)
1974
+ case _:
1975
+ msg = (
1976
+ f"Operation status {context.operation.status} is not valid for step operations. "
1977
+ f"Valid statuses are: STARTED, SUCCEEDED, FAILED"
1978
+ )
1979
+ raise InvalidParameterValueException(msg)
1980
+
1981
+ # endregion step
1982
+
1983
+ # region chained_invoke
1984
+ @classmethod
1985
+ def create_chained_invoke_event_pending(
1986
+ cls, context: EventCreationContext
1987
+ ) -> Event:
1988
+ input: EventInput = EventInput.from_start_durable_execution_input(
1989
+ context.start_durable_execution_input, context.include_execution_data
1990
+ )
1991
+ return cls(
1992
+ event_type=EventType.CHAINED_INVOKE_STARTED.value,
1993
+ event_timestamp=context.start_timestamp,
1994
+ sub_type=context.sub_type,
1995
+ event_id=context.event_id,
1996
+ operation_id=context.operation.operation_id,
1997
+ name=context.operation.name,
1998
+ parent_id=context.operation.parent_id,
1999
+ chained_invoke_pending_details=ChainedInvokePendingDetails(
2000
+ input=input,
2001
+ function_name=context.start_durable_execution_input.function_name,
2002
+ ),
2003
+ )
2004
+
2005
+ @classmethod
2006
+ def create_chained_invoke_event_started(
2007
+ cls, context: EventCreationContext
2008
+ ) -> Event:
2009
+ return cls(
2010
+ event_type=EventType.CHAINED_INVOKE_STARTED.value,
2011
+ event_timestamp=context.start_timestamp,
2012
+ sub_type=context.sub_type,
2013
+ event_id=context.event_id,
2014
+ operation_id=context.operation.operation_id,
2015
+ name=context.operation.name,
2016
+ parent_id=context.operation.parent_id,
2017
+ chained_invoke_started_details=ChainedInvokeStartedDetails(
2018
+ durable_execution_arn=context.durable_execution_arn
2019
+ ),
2020
+ )
2021
+
2022
+ @classmethod
2023
+ def create_chained_invoke_event_succeeded(
2024
+ cls, context: EventCreationContext
2025
+ ) -> Event:
2026
+ chained_invoke_details: ChainedInvokeDetails | None = (
2027
+ context.operation.chained_invoke_details
2028
+ )
2029
+ event_result: EventResult | None = (
2030
+ EventResult.from_details(
2031
+ chained_invoke_details, context.include_execution_data
2032
+ )
2033
+ if chained_invoke_details
2034
+ else None
2035
+ )
2036
+ return cls(
2037
+ event_type=EventType.CHAINED_INVOKE_SUCCEEDED.value,
2038
+ event_timestamp=context.end_timestamp,
2039
+ sub_type=context.sub_type,
2040
+ event_id=context.event_id,
2041
+ operation_id=context.operation.operation_id,
2042
+ name=context.operation.name,
2043
+ parent_id=context.operation.parent_id,
2044
+ chained_invoke_succeeded_details=ChainedInvokeSucceededDetails(
2045
+ result=event_result
2046
+ ),
2047
+ )
2048
+
2049
+ @classmethod
2050
+ def create_chained_invoke_event_failed(cls, context: EventCreationContext) -> Event:
2051
+ chained_invoke_details: ChainedInvokeDetails | None = (
2052
+ context.operation.chained_invoke_details
2053
+ )
2054
+ event_error: EventError | None = (
2055
+ EventError.from_details(
2056
+ chained_invoke_details, include=context.include_execution_data
2057
+ )
2058
+ if chained_invoke_details
2059
+ else None
2060
+ )
2061
+ return cls(
2062
+ event_type=EventType.CHAINED_INVOKE_FAILED.value,
2063
+ event_timestamp=context.end_timestamp,
2064
+ sub_type=context.sub_type,
2065
+ event_id=context.event_id,
2066
+ operation_id=context.operation.operation_id,
2067
+ name=context.operation.name,
2068
+ parent_id=context.operation.parent_id,
2069
+ chained_invoke_failed_details=ChainedInvokeFailedDetails(error=event_error),
2070
+ )
2071
+
2072
+ @classmethod
2073
+ def create_chained_invoke_event_timed_out(
2074
+ cls, context: EventCreationContext
2075
+ ) -> Event:
2076
+ chained_invoke_details: ChainedInvokeDetails | None = (
2077
+ context.operation.chained_invoke_details
2078
+ )
2079
+ event_error: EventError | None = (
2080
+ EventError.from_details(
2081
+ chained_invoke_details, include=context.include_execution_data
2082
+ )
2083
+ if chained_invoke_details
2084
+ else None
2085
+ )
2086
+ return cls(
2087
+ event_type=EventType.CHAINED_INVOKE_TIMED_OUT.value,
2088
+ event_timestamp=context.end_timestamp,
2089
+ sub_type=context.sub_type,
2090
+ event_id=context.event_id,
2091
+ operation_id=context.operation.operation_id,
2092
+ name=context.operation.name,
2093
+ parent_id=context.operation.parent_id,
2094
+ chained_invoke_timed_out_details=ChainedInvokeTimedOutDetails(
2095
+ error=event_error
2096
+ ),
2097
+ )
2098
+
2099
+ @classmethod
2100
+ def create_chained_invoke_event_stopped(
2101
+ cls, context: EventCreationContext
2102
+ ) -> Event:
2103
+ chained_invoke_details: ChainedInvokeDetails | None = (
2104
+ context.operation.chained_invoke_details
2105
+ )
2106
+ event_error: EventError | None = (
2107
+ EventError.from_details(
2108
+ chained_invoke_details, include=context.include_execution_data
2109
+ )
2110
+ if chained_invoke_details
2111
+ else None
2112
+ )
2113
+ return cls(
2114
+ event_type=EventType.CHAINED_INVOKE_STOPPED.value,
2115
+ event_timestamp=context.end_timestamp,
2116
+ sub_type=context.sub_type,
2117
+ event_id=context.event_id,
2118
+ operation_id=context.operation.operation_id,
2119
+ name=context.operation.name,
2120
+ parent_id=context.operation.parent_id,
2121
+ chained_invoke_stopped_details=ChainedInvokeStoppedDetails(
2122
+ error=event_error
2123
+ ),
2124
+ )
2125
+
2126
+ @classmethod
2127
+ def create_chained_invoke_event(cls, context: EventCreationContext) -> Event:
2128
+ """Create chained invoke event based on action."""
2129
+ match context.operation.status:
2130
+ case OperationStatus.PENDING:
2131
+ return cls.create_chained_invoke_event_pending(context)
2132
+ case OperationStatus.STARTED:
2133
+ return cls.create_chained_invoke_event_started(context)
2134
+ case OperationStatus.SUCCEEDED:
2135
+ return cls.create_chained_invoke_event_succeeded(context)
2136
+ case OperationStatus.FAILED:
2137
+ return cls.create_chained_invoke_event_failed(context)
2138
+ case OperationStatus.TIMED_OUT:
2139
+ return cls.create_chained_invoke_event_timed_out(context)
2140
+ case OperationStatus.STOPPED:
2141
+ return cls.create_chained_invoke_event_stopped(context)
2142
+ case _:
2143
+ msg = (
2144
+ f"Operation status {context.operation.status} is not valid for chained invoke operations. Valid statuses are: "
2145
+ f"STARTED, SUCCEEDED, FAILED, TIMED_OUT, STOPPED"
2146
+ )
2147
+ raise InvalidParameterValueException(msg)
2148
+
2149
+ # endregion chained_invoke
2150
+
2151
+ # region callback
2152
+ @classmethod
2153
+ def create_callback_event_started(cls, context: EventCreationContext) -> Event:
2154
+ callback_details: CallbackDetails | None = context.operation.callback_details
2155
+ callback_id: str | None = (
2156
+ callback_details.callback_id if callback_details else None
2157
+ )
2158
+ callback_options: CallbackOptions | None = (
2159
+ context.operation_update.callback_options
2160
+ if context.operation_update
2161
+ else None
2162
+ )
2163
+ timeout: int | None = (
2164
+ callback_options.timeout_seconds if callback_options else None
2165
+ )
2166
+ heartbeat_timeout: int | None = (
2167
+ callback_options.heartbeat_timeout_seconds if callback_options else None
2168
+ )
2169
+ return cls(
2170
+ event_type=EventType.CALLBACK_STARTED.value,
2171
+ event_timestamp=context.start_timestamp,
2172
+ sub_type=context.sub_type,
2173
+ event_id=context.event_id,
2174
+ operation_id=context.operation.operation_id,
2175
+ name=context.operation.name,
2176
+ parent_id=context.operation.parent_id,
2177
+ callback_started_details=CallbackStartedDetails(
2178
+ callback_id=callback_id,
2179
+ timeout=timeout,
2180
+ heartbeat_timeout=heartbeat_timeout,
2181
+ ),
2182
+ )
2183
+
2184
+ @classmethod
2185
+ def create_callback_event_succeeded(cls, context: EventCreationContext) -> Event:
2186
+ callback_details: CallbackDetails | None = context.operation.callback_details
2187
+ event_result: EventResult | None = (
2188
+ EventResult.from_details(callback_details, context.include_execution_data)
2189
+ if callback_details
2190
+ else None
2191
+ )
2192
+ return cls(
2193
+ event_type=EventType.CALLBACK_SUCCEEDED.value,
2194
+ event_timestamp=context.end_timestamp,
2195
+ sub_type=context.sub_type,
2196
+ event_id=context.event_id,
2197
+ operation_id=context.operation.operation_id,
2198
+ name=context.operation.name,
2199
+ parent_id=context.operation.parent_id,
2200
+ callback_succeeded_details=CallbackSucceededDetails(result=event_result),
2201
+ )
2202
+
2203
+ @classmethod
2204
+ def create_callback_event_failed(cls, context: EventCreationContext) -> Event:
2205
+ callback_details: CallbackDetails | None = context.operation.callback_details
2206
+ event_error: EventError | None = (
2207
+ EventError.from_details(callback_details) if callback_details else None
2208
+ )
2209
+ return cls(
2210
+ event_type=EventType.CALLBACK_FAILED.value,
2211
+ event_timestamp=context.end_timestamp,
2212
+ sub_type=context.sub_type,
2213
+ event_id=context.event_id,
2214
+ operation_id=context.operation.operation_id,
2215
+ name=context.operation.name,
2216
+ parent_id=context.operation.parent_id,
2217
+ callback_failed_details=CallbackFailedDetails(error=event_error),
2218
+ )
2219
+
2220
+ @classmethod
2221
+ def create_callback_event_timed_out(cls, context: EventCreationContext) -> Event:
2222
+ callback_details: CallbackDetails | None = context.operation.callback_details
2223
+ event_error: EventError | None = (
2224
+ EventError.from_details(callback_details) if callback_details else None
2225
+ )
2226
+ return cls(
2227
+ event_type=EventType.CALLBACK_TIMED_OUT.value,
2228
+ event_timestamp=context.end_timestamp,
2229
+ sub_type=context.sub_type,
2230
+ event_id=context.event_id,
2231
+ operation_id=context.operation.operation_id,
2232
+ name=context.operation.name,
2233
+ parent_id=context.operation.parent_id,
2234
+ callback_timed_out_details=CallbackTimedOutDetails(error=event_error),
2235
+ )
2236
+
2237
+ @classmethod
2238
+ def create_callback_event(cls, context: EventCreationContext) -> Event:
2239
+ """Create callback event based on action."""
2240
+ match context.operation.status:
2241
+ case OperationStatus.STARTED:
2242
+ return cls.create_callback_event_started(context)
2243
+ case OperationStatus.SUCCEEDED:
2244
+ return cls.create_callback_event_succeeded(context)
2245
+ case OperationStatus.FAILED:
2246
+ return cls.create_callback_event_failed(context)
2247
+ case OperationStatus.TIMED_OUT:
2248
+ return cls.create_callback_event_timed_out(context)
2249
+ case _:
2250
+ msg = (
2251
+ f"Operation status {context.operation.status} is not valid for callback operations. "
2252
+ f"Valid statuses are: STARTED, SUCCEEDED, FAILED, TIMED_OUT"
2253
+ )
2254
+ raise InvalidParameterValueException(msg)
2255
+
2256
+ # endregion callback
2257
+
2258
+ # region invocation_completed
2259
+ @classmethod
2260
+ def create_invocation_completed(
2261
+ cls,
2262
+ event_id: int,
2263
+ event_timestamp: datetime.datetime,
2264
+ start_timestamp: datetime.datetime,
2265
+ end_timestamp: datetime.datetime,
2266
+ request_id: str,
2267
+ ) -> Event:
2268
+ """Create invocation completed event."""
2269
+ return cls(
2270
+ event_type=EventType.INVOCATION_COMPLETED.value,
2271
+ event_timestamp=event_timestamp,
2272
+ event_id=event_id,
2273
+ invocation_completed_details=InvocationCompletedDetails(
2274
+ start_timestamp=start_timestamp,
2275
+ end_timestamp=end_timestamp,
2276
+ request_id=request_id,
2277
+ ),
2278
+ )
2279
+
2280
+ # endregion invocation_completed
2281
+
2282
+ @classmethod
2283
+ def create_event_started(cls, context: EventCreationContext) -> Event:
2284
+ """Convert operation to started event."""
2285
+ if context.operation.start_timestamp is None:
2286
+ msg: str = "Operation start timestamp cannot be None when converting to started event"
2287
+ raise InvalidParameterValueException(msg)
2288
+
2289
+ match context.operation.operation_type:
2290
+ case OperationType.EXECUTION:
2291
+ return cls.create_execution_event_started(context)
2292
+ case OperationType.CONTEXT:
2293
+ return cls.create_context_event_started(context)
2294
+ case OperationType.WAIT:
2295
+ return cls.create_wait_event_started(context)
2296
+ case OperationType.STEP:
2297
+ return cls.create_step_event_started(context)
2298
+ case OperationType.CHAINED_INVOKE:
2299
+ return cls.create_chained_invoke_event_started(context)
2300
+ case OperationType.CALLBACK:
2301
+ return cls.create_callback_event_started(context)
2302
+ case _:
2303
+ msg = f"Unknown operation type: {context.operation.operation_type}"
2304
+ raise InvalidParameterValueException(msg)
2305
+
2306
+ @classmethod
2307
+ def from_event_with_id(cls, event: Event, event_id: int) -> Event:
2308
+ """Create a new Event from an existing event with updated event_id."""
2309
+ return cls(
2310
+ event_type=event.event_type,
2311
+ event_timestamp=event.event_timestamp,
2312
+ sub_type=event.sub_type,
2313
+ event_id=event_id,
2314
+ operation_id=event.operation_id,
2315
+ name=event.name,
2316
+ parent_id=event.parent_id,
2317
+ execution_started_details=event.execution_started_details,
2318
+ execution_succeeded_details=event.execution_succeeded_details,
2319
+ execution_failed_details=event.execution_failed_details,
2320
+ execution_timed_out_details=event.execution_timed_out_details,
2321
+ execution_stopped_details=event.execution_stopped_details,
2322
+ context_started_details=event.context_started_details,
2323
+ context_succeeded_details=event.context_succeeded_details,
2324
+ context_failed_details=event.context_failed_details,
2325
+ wait_started_details=event.wait_started_details,
2326
+ wait_succeeded_details=event.wait_succeeded_details,
2327
+ wait_cancelled_details=event.wait_cancelled_details,
2328
+ step_started_details=event.step_started_details,
2329
+ step_succeeded_details=event.step_succeeded_details,
2330
+ step_failed_details=event.step_failed_details,
2331
+ chained_invoke_pending_details=event.chained_invoke_pending_details,
2332
+ chained_invoke_started_details=event.chained_invoke_started_details,
2333
+ chained_invoke_succeeded_details=event.chained_invoke_succeeded_details,
2334
+ chained_invoke_failed_details=event.chained_invoke_failed_details,
2335
+ chained_invoke_timed_out_details=event.chained_invoke_timed_out_details,
2336
+ chained_invoke_stopped_details=event.chained_invoke_stopped_details,
2337
+ callback_started_details=event.callback_started_details,
2338
+ callback_succeeded_details=event.callback_succeeded_details,
2339
+ callback_failed_details=event.callback_failed_details,
2340
+ callback_timed_out_details=event.callback_timed_out_details,
2341
+ )
2342
+
2343
+ @classmethod
2344
+ def create_event_terminated(cls, context: EventCreationContext) -> Event:
2345
+ """Convert operation to finished event."""
2346
+ operation: Operation = context.operation
2347
+ if operation.end_timestamp is None:
2348
+ msg: str = "Operation end timestamp cannot be None when converting to finished event"
2349
+ raise InvalidParameterValueException(msg)
2350
+
2351
+ if operation.status not in TERMINAL_STATUSES:
2352
+ msg = f"Operation status must be one of SUCCEEDED, FAILED, TIMED_OUT, STOPPED, or CANCELLED. Got: {operation.status}"
2353
+ raise InvalidParameterValueException(msg)
2354
+
2355
+ match operation.operation_type:
2356
+ case OperationType.EXECUTION:
2357
+ return cls.create_execution_event(context)
2358
+ case OperationType.CONTEXT:
2359
+ return cls.create_context_event(context)
2360
+ case OperationType.WAIT:
2361
+ return cls.create_wait_event(context)
2362
+ case OperationType.STEP:
2363
+ return cls.create_step_event(context)
2364
+ case OperationType.CHAINED_INVOKE:
2365
+ return cls.create_chained_invoke_event(context)
2366
+ case OperationType.CALLBACK:
2367
+ return cls.create_callback_event(context)
2368
+ case _:
2369
+ msg = f"Unknown operation type: {operation.operation_type}"
2370
+ raise InvalidParameterValueException(msg)
2371
+
2372
+
2373
+ # endregion event_class
2374
+
2375
+
2376
+ # region history_models
2377
+ @dataclass(frozen=True)
2378
+ class HistoryEventTypeConfig:
2379
+ """Configuration for how to process a specific event type."""
2380
+
2381
+ operation_type: OperationType | None
2382
+ operation_status: OperationStatus | None
2383
+ is_start_event: bool
2384
+ is_end_event: bool
2385
+ has_result: bool # Whether this event type contains result/error data
2386
+
2387
+
2388
+ # Mapping of event types to their processing configuration
2389
+ # This matches the TypeScript historyEventTypes constant
2390
+ HISTORY_EVENT_TYPES: dict[str, HistoryEventTypeConfig] = {
2391
+ "ExecutionStarted": HistoryEventTypeConfig(
2392
+ operation_type=OperationType.EXECUTION,
2393
+ operation_status=OperationStatus.STARTED,
2394
+ is_start_event=True,
2395
+ is_end_event=False,
2396
+ has_result=False,
2397
+ ),
2398
+ "ExecutionFailed": HistoryEventTypeConfig(
2399
+ operation_type=OperationType.EXECUTION,
2400
+ operation_status=OperationStatus.FAILED,
2401
+ is_start_event=False,
2402
+ is_end_event=True,
2403
+ has_result=False,
2404
+ ),
2405
+ "ExecutionStopped": HistoryEventTypeConfig(
2406
+ operation_type=OperationType.EXECUTION,
2407
+ operation_status=OperationStatus.STOPPED,
2408
+ is_start_event=False,
2409
+ is_end_event=True,
2410
+ has_result=False,
2411
+ ),
2412
+ "ExecutionSucceeded": HistoryEventTypeConfig(
2413
+ operation_type=OperationType.EXECUTION,
2414
+ operation_status=OperationStatus.SUCCEEDED,
2415
+ is_start_event=False,
2416
+ is_end_event=True,
2417
+ has_result=False,
2418
+ ),
2419
+ "ExecutionTimedOut": HistoryEventTypeConfig(
2420
+ operation_type=OperationType.EXECUTION,
2421
+ operation_status=OperationStatus.TIMED_OUT,
2422
+ is_start_event=False,
2423
+ is_end_event=True,
2424
+ has_result=False,
2425
+ ),
2426
+ "CallbackStarted": HistoryEventTypeConfig(
2427
+ operation_type=OperationType.CALLBACK,
2428
+ operation_status=OperationStatus.STARTED,
2429
+ is_start_event=True,
2430
+ is_end_event=False,
2431
+ has_result=False,
2432
+ ),
2433
+ "CallbackFailed": HistoryEventTypeConfig(
2434
+ operation_type=OperationType.CALLBACK,
2435
+ operation_status=OperationStatus.FAILED,
2436
+ is_start_event=False,
2437
+ is_end_event=True,
2438
+ has_result=True,
2439
+ ),
2440
+ "CallbackSucceeded": HistoryEventTypeConfig(
2441
+ operation_type=OperationType.CALLBACK,
2442
+ operation_status=OperationStatus.SUCCEEDED,
2443
+ is_start_event=False,
2444
+ is_end_event=True,
2445
+ has_result=True,
2446
+ ),
2447
+ "CallbackTimedOut": HistoryEventTypeConfig(
2448
+ operation_type=OperationType.CALLBACK,
2449
+ operation_status=OperationStatus.TIMED_OUT,
2450
+ is_start_event=False,
2451
+ is_end_event=True,
2452
+ has_result=True,
2453
+ ),
2454
+ "ContextStarted": HistoryEventTypeConfig(
2455
+ operation_type=OperationType.CONTEXT,
2456
+ operation_status=OperationStatus.STARTED,
2457
+ is_start_event=True,
2458
+ is_end_event=False,
2459
+ has_result=False,
2460
+ ),
2461
+ "ContextFailed": HistoryEventTypeConfig(
2462
+ operation_type=OperationType.CONTEXT,
2463
+ operation_status=OperationStatus.FAILED,
2464
+ is_start_event=False,
2465
+ is_end_event=True,
2466
+ has_result=True,
2467
+ ),
2468
+ "ContextSucceeded": HistoryEventTypeConfig(
2469
+ operation_type=OperationType.CONTEXT,
2470
+ operation_status=OperationStatus.SUCCEEDED,
2471
+ is_start_event=False,
2472
+ is_end_event=True,
2473
+ has_result=True,
2474
+ ),
2475
+ "ChainedInvokeStarted": HistoryEventTypeConfig(
2476
+ operation_type=OperationType.CHAINED_INVOKE,
2477
+ operation_status=OperationStatus.STARTED,
2478
+ is_start_event=True,
2479
+ is_end_event=False,
2480
+ has_result=False,
2481
+ ),
2482
+ "ChainedInvokeFailed": HistoryEventTypeConfig(
2483
+ operation_type=OperationType.CHAINED_INVOKE,
2484
+ operation_status=OperationStatus.FAILED,
2485
+ is_start_event=False,
2486
+ is_end_event=True,
2487
+ has_result=True,
2488
+ ),
2489
+ "ChainedInvokeSucceeded": HistoryEventTypeConfig(
2490
+ operation_type=OperationType.CHAINED_INVOKE,
2491
+ operation_status=OperationStatus.SUCCEEDED,
2492
+ is_start_event=False,
2493
+ is_end_event=True,
2494
+ has_result=True,
2495
+ ),
2496
+ "ChainedInvokeTimedOut": HistoryEventTypeConfig(
2497
+ operation_type=OperationType.CHAINED_INVOKE,
2498
+ operation_status=OperationStatus.TIMED_OUT,
2499
+ is_start_event=False,
2500
+ is_end_event=True,
2501
+ has_result=True,
2502
+ ),
2503
+ "ChainedInvokeCancelled": HistoryEventTypeConfig(
2504
+ operation_type=OperationType.CHAINED_INVOKE,
2505
+ operation_status=OperationStatus.CANCELLED,
2506
+ is_start_event=False,
2507
+ is_end_event=True,
2508
+ has_result=True,
2509
+ ),
2510
+ "StepStarted": HistoryEventTypeConfig(
2511
+ operation_type=OperationType.STEP,
2512
+ operation_status=OperationStatus.STARTED,
2513
+ is_start_event=True,
2514
+ is_end_event=False,
2515
+ has_result=False,
2516
+ ),
2517
+ "StepFailed": HistoryEventTypeConfig(
2518
+ operation_type=OperationType.STEP,
2519
+ operation_status=OperationStatus.FAILED,
2520
+ is_start_event=False,
2521
+ is_end_event=True,
2522
+ has_result=True,
2523
+ ),
2524
+ "StepSucceeded": HistoryEventTypeConfig(
2525
+ operation_type=OperationType.STEP,
2526
+ operation_status=OperationStatus.SUCCEEDED,
2527
+ is_start_event=False,
2528
+ is_end_event=True,
2529
+ has_result=True,
2530
+ ),
2531
+ "WaitStarted": HistoryEventTypeConfig(
2532
+ operation_type=OperationType.WAIT,
2533
+ operation_status=OperationStatus.STARTED,
2534
+ is_start_event=True,
2535
+ is_end_event=False,
2536
+ has_result=True,
2537
+ ),
2538
+ "WaitSucceeded": HistoryEventTypeConfig(
2539
+ operation_type=OperationType.WAIT,
2540
+ operation_status=OperationStatus.SUCCEEDED,
2541
+ is_start_event=False,
2542
+ is_end_event=True,
2543
+ has_result=True,
2544
+ ),
2545
+ "WaitCancelled": HistoryEventTypeConfig(
2546
+ operation_type=OperationType.WAIT,
2547
+ operation_status=OperationStatus.CANCELLED,
2548
+ is_start_event=False,
2549
+ is_end_event=True,
2550
+ has_result=True,
2551
+ ),
2552
+ # TODO: add support for populating invocation information from InvocationCompleted event
2553
+ "InvocationCompleted": HistoryEventTypeConfig(
2554
+ operation_type=None,
2555
+ operation_status=None,
2556
+ is_start_event=False,
2557
+ is_end_event=False,
2558
+ has_result=True,
2559
+ ),
2560
+ }
2561
+
2562
+
2563
+ def events_to_operations(events: list[Event]) -> list[Operation]:
2564
+ """Convert a list of history events into operations.
2565
+
2566
+ This function processes raw history events and groups them by operation ID,
2567
+ creating comprehensive operation objects following the TypeScript pattern from
2568
+ aws-durable-execution-sdk-js-testing.
2569
+
2570
+ Multiple events for the same operation_id are merged together, with each event
2571
+ contributing its specific fields (e.g., CallbackStarted provides callback_id,
2572
+ CallbackSucceeded provides result).
2573
+
2574
+ Args:
2575
+ events: List of history events to process
2576
+
2577
+ Returns:
2578
+ List of operations, one per unique operation ID
2579
+
2580
+ Raises:
2581
+ InvalidParameterValueException: When required fields are missing from an event
2582
+
2583
+ Note:
2584
+ InvocationCompleted events are currently skipped as they don't represent
2585
+ operations. Future enhancement: populate invocation information from these
2586
+ events (TODO).
2587
+ """
2588
+ operations_map: dict[str, Operation] = {}
2589
+
2590
+ for event in events:
2591
+ if not event.event_type:
2592
+ msg = "Missing required 'event_type' field in event"
2593
+ raise InvalidParameterValueException(msg)
2594
+
2595
+ # Get event type configuration
2596
+ event_config: HistoryEventTypeConfig | None = HISTORY_EVENT_TYPES.get(
2597
+ event.event_type
2598
+ )
2599
+ if not event_config:
2600
+ msg = f"Unknown event type: {event.event_type}"
2601
+ raise InvalidParameterValueException(msg)
2602
+
2603
+ # TODO: add support for populating invocation information from InvocationCompleted event
2604
+ if event.event_type == "InvocationCompleted":
2605
+ continue
2606
+
2607
+ if not event.operation_id:
2608
+ msg = f"Missing required 'operation_id' field in event {event.event_id}"
2609
+ raise InvalidParameterValueException(msg)
2610
+
2611
+ # Get previous operation if it exists
2612
+ previous_operation: Operation | None = operations_map.get(event.operation_id)
2613
+
2614
+ # Get operation type and status from configuration
2615
+ operation_type: OperationType = (
2616
+ event_config.operation_type or OperationType.EXECUTION
2617
+ )
2618
+ status: OperationStatus = (
2619
+ event_config.operation_status or OperationStatus.PENDING
2620
+ )
2621
+
2622
+ # Parse sub_type
2623
+ sub_type: OperationSubType | None = None
2624
+ if event.sub_type:
2625
+ try:
2626
+ sub_type = OperationSubType(event.sub_type)
2627
+ except ValueError as e:
2628
+ raise InvalidParameterValueException(str(e)) from e
2629
+
2630
+ # Create base operation
2631
+ operation = Operation(
2632
+ operation_id=event.operation_id,
2633
+ operation_type=operation_type,
2634
+ status=status,
2635
+ name=event.name,
2636
+ parent_id=event.parent_id,
2637
+ sub_type=sub_type,
2638
+ start_timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
2639
+ )
2640
+
2641
+ # Merge with previous operation if it exists
2642
+ # Most fields are immutable, so they get preserved from previous events
2643
+ if previous_operation:
2644
+ operation = replace(
2645
+ operation,
2646
+ name=operation.name or previous_operation.name,
2647
+ parent_id=operation.parent_id or previous_operation.parent_id,
2648
+ sub_type=operation.sub_type or previous_operation.sub_type,
2649
+ start_timestamp=previous_operation.start_timestamp,
2650
+ end_timestamp=previous_operation.end_timestamp,
2651
+ execution_details=previous_operation.execution_details,
2652
+ context_details=previous_operation.context_details,
2653
+ step_details=previous_operation.step_details,
2654
+ wait_details=previous_operation.wait_details,
2655
+ callback_details=previous_operation.callback_details,
2656
+ chained_invoke_details=previous_operation.chained_invoke_details,
2657
+ )
2658
+
2659
+ # Set timestamps based on event configuration
2660
+ if event_config.is_start_event:
2661
+ operation = replace(operation, start_timestamp=event.event_timestamp)
2662
+ if event_config.is_end_event:
2663
+ operation = replace(operation, end_timestamp=event.event_timestamp)
2664
+
2665
+ # Add operation-specific details incrementally
2666
+ # Each event type contributes only the fields it has
2667
+
2668
+ # EXECUTION details
2669
+ if (
2670
+ operation_type == OperationType.EXECUTION
2671
+ and event.execution_started_details
2672
+ and event.execution_started_details.input
2673
+ ):
2674
+ operation = replace(
2675
+ operation,
2676
+ execution_details=ExecutionDetails(
2677
+ input_payload=event.execution_started_details.input.payload
2678
+ ),
2679
+ )
2680
+
2681
+ # CALLBACK details - merge callback_id, result, and error from different events
2682
+ if operation_type == OperationType.CALLBACK:
2683
+ existing_cb: CallbackDetails | None = operation.callback_details
2684
+ callback_id: str = existing_cb.callback_id if existing_cb else ""
2685
+ result: str | None = existing_cb.result if existing_cb else None
2686
+ error: ErrorObject | None = existing_cb.error if existing_cb else None
2687
+
2688
+ # CallbackStarted provides callback_id
2689
+ if event.callback_started_details:
2690
+ callback_id = event.callback_started_details.callback_id or callback_id
2691
+
2692
+ # CallbackSucceeded provides result
2693
+ if (
2694
+ event.callback_succeeded_details
2695
+ and event.callback_succeeded_details.result
2696
+ ):
2697
+ result = event.callback_succeeded_details.result.payload
2698
+
2699
+ # CallbackFailed provides error
2700
+ if event.callback_failed_details and event.callback_failed_details.error:
2701
+ error = event.callback_failed_details.error.payload
2702
+
2703
+ # CallbackTimedOut provides error
2704
+ if (
2705
+ event.callback_timed_out_details
2706
+ and event.callback_timed_out_details.error
2707
+ ):
2708
+ error = event.callback_timed_out_details.error.payload
2709
+
2710
+ operation = replace(
2711
+ operation,
2712
+ callback_details=CallbackDetails(
2713
+ callback_id=callback_id,
2714
+ result=result,
2715
+ error=error,
2716
+ ),
2717
+ )
2718
+
2719
+ # STEP details - only update if this event type has result data
2720
+ if operation_type == OperationType.STEP and event_config.has_result:
2721
+ existing_step: StepDetails | None = operation.step_details
2722
+ result_val: str | None = existing_step.result if existing_step else None
2723
+ error_val: ErrorObject | None = (
2724
+ existing_step.error if existing_step else None
2725
+ )
2726
+ attempt: int = existing_step.attempt if existing_step else 0
2727
+ next_attempt_ts: datetime.datetime | None = (
2728
+ existing_step.next_attempt_timestamp if existing_step else None
2729
+ )
2730
+
2731
+ # StepSucceeded provides result
2732
+ if event.step_succeeded_details:
2733
+ if event.step_succeeded_details.result:
2734
+ result_val = event.step_succeeded_details.result.payload
2735
+ if event.step_succeeded_details.retry_details:
2736
+ attempt = event.step_succeeded_details.retry_details.current_attempt
2737
+
2738
+ # StepFailed provides error and retry details
2739
+ if event.step_failed_details:
2740
+ if event.step_failed_details.error:
2741
+ error_val = event.step_failed_details.error.payload
2742
+ if event.step_failed_details.retry_details:
2743
+ attempt = event.step_failed_details.retry_details.current_attempt
2744
+ if (
2745
+ event.step_failed_details.retry_details.next_attempt_delay_seconds
2746
+ is not None
2747
+ ):
2748
+ next_attempt_ts = event.event_timestamp + datetime.timedelta(
2749
+ seconds=event.step_failed_details.retry_details.next_attempt_delay_seconds
2750
+ )
2751
+
2752
+ operation = replace(
2753
+ operation,
2754
+ step_details=StepDetails(
2755
+ result=result_val,
2756
+ error=error_val,
2757
+ attempt=attempt,
2758
+ next_attempt_timestamp=next_attempt_ts,
2759
+ ),
2760
+ )
2761
+
2762
+ # WAIT details
2763
+ if operation_type == OperationType.WAIT and event.wait_started_details:
2764
+ operation = replace(
2765
+ operation,
2766
+ wait_details=WaitDetails(
2767
+ scheduled_end_timestamp=event.wait_started_details.scheduled_end_timestamp
2768
+ ),
2769
+ )
2770
+
2771
+ # CONTEXT details - only update if this event type has result data (matching TypeScript hasResult)
2772
+ if operation_type == OperationType.CONTEXT and event_config.has_result:
2773
+ if (
2774
+ event.context_succeeded_details
2775
+ and event.context_succeeded_details.result
2776
+ ):
2777
+ operation = replace(
2778
+ operation,
2779
+ context_details=ContextDetails(
2780
+ result=event.context_succeeded_details.result.payload,
2781
+ error=None,
2782
+ ),
2783
+ )
2784
+ elif event.context_failed_details and event.context_failed_details.error:
2785
+ operation = replace(
2786
+ operation,
2787
+ context_details=ContextDetails(
2788
+ result=None,
2789
+ error=event.context_failed_details.error.payload,
2790
+ ),
2791
+ )
2792
+
2793
+ # CHAINED_INVOKE details - only update if this event type has result data (matching TypeScript hasResult)
2794
+ if operation_type == OperationType.CHAINED_INVOKE and event_config.has_result:
2795
+ if (
2796
+ event.chained_invoke_succeeded_details
2797
+ and event.chained_invoke_succeeded_details.result
2798
+ ):
2799
+ operation = replace(
2800
+ operation,
2801
+ chained_invoke_details=ChainedInvokeDetails(
2802
+ result=event.chained_invoke_succeeded_details.result.payload,
2803
+ error=None,
2804
+ ),
2805
+ )
2806
+ elif (
2807
+ event.chained_invoke_failed_details
2808
+ and event.chained_invoke_failed_details.error
2809
+ ):
2810
+ operation = replace(
2811
+ operation,
2812
+ chained_invoke_details=ChainedInvokeDetails(
2813
+ result=None,
2814
+ error=event.chained_invoke_failed_details.error.payload,
2815
+ ),
2816
+ )
2817
+
2818
+ # Store in map
2819
+ operations_map[event.operation_id] = operation
2820
+
2821
+ return list(operations_map.values())
2822
+
2823
+
2824
+ @dataclass(frozen=True)
2825
+ class GetDurableExecutionHistoryRequest:
2826
+ """Request to get durable execution history."""
2827
+
2828
+ durable_execution_arn: str
2829
+ include_execution_data: bool | None = None
2830
+ reverse_order: bool | None = None
2831
+ marker: str | None = None
2832
+ max_items: int = 0
2833
+
2834
+ @classmethod
2835
+ def from_dict(cls, data: dict) -> GetDurableExecutionHistoryRequest:
2836
+ return cls(
2837
+ durable_execution_arn=data["DurableExecutionArn"],
2838
+ include_execution_data=data.get("IncludeExecutionData"),
2839
+ reverse_order=data.get("ReverseOrder"),
2840
+ marker=data.get("Marker"),
2841
+ max_items=data.get("MaxItems", 0),
2842
+ )
2843
+
2844
+ def to_dict(self) -> dict[str, Any]:
2845
+ result: dict[str, Any] = {"DurableExecutionArn": self.durable_execution_arn}
2846
+ if self.include_execution_data is not None:
2847
+ result["IncludeExecutionData"] = self.include_execution_data
2848
+ if self.reverse_order is not None:
2849
+ result["ReverseOrder"] = self.reverse_order
2850
+ if self.marker is not None:
2851
+ result["Marker"] = self.marker
2852
+ if self.max_items is not None:
2853
+ result["MaxItems"] = self.max_items
2854
+ return result
2855
+
2856
+
2857
+ @dataclass(frozen=True)
2858
+ class GetDurableExecutionHistoryResponse:
2859
+ """Response containing durable execution history events."""
2860
+
2861
+ events: list[Event]
2862
+ next_marker: str | None = None
2863
+
2864
+ @classmethod
2865
+ def from_dict(cls, data: dict) -> GetDurableExecutionHistoryResponse:
2866
+ events = [Event.from_dict(event_data) for event_data in data.get("Events", [])]
2867
+ return cls(
2868
+ events=events,
2869
+ next_marker=data.get("NextMarker"),
2870
+ )
2871
+
2872
+ def to_dict(self) -> dict[str, Any]:
2873
+ result: dict[str, Any] = {"Events": [event.to_dict() for event in self.events]}
2874
+ if self.next_marker is not None:
2875
+ result["NextMarker"] = self.next_marker
2876
+ return result
2877
+
2878
+
2879
+ @dataclass(frozen=True)
2880
+ class ListDurableExecutionsByFunctionRequest:
2881
+ """Request to list durable executions by function."""
2882
+
2883
+ function_name: str
2884
+ qualifier: str | None = None
2885
+ durable_execution_name: str | None = None
2886
+ status_filter: list[str] | None = None
2887
+ started_after: str | None = None
2888
+ started_before: str | None = None
2889
+ marker: str | None = None
2890
+ max_items: int = 0
2891
+ reverse_order: bool | None = None
2892
+
2893
+ @classmethod
2894
+ def from_dict(cls, data: dict) -> ListDurableExecutionsByFunctionRequest:
2895
+ # Handle query parameters that may be lists
2896
+ function_name = data.get("FunctionName")
2897
+ if isinstance(function_name, list):
2898
+ function_name = function_name[0] if function_name else ""
2899
+ elif not function_name:
2900
+ function_name = ""
2901
+
2902
+ qualifier = data.get("Qualifier") or data.get("functionVersion")
2903
+ if isinstance(qualifier, list):
2904
+ qualifier = qualifier[0] if qualifier else None
2905
+
2906
+ durable_execution_name = data.get("DurableExecutionName") or data.get(
2907
+ "executionName"
2908
+ )
2909
+ if isinstance(durable_execution_name, list):
2910
+ durable_execution_name = (
2911
+ durable_execution_name[0] if durable_execution_name else None
2912
+ )
2913
+
2914
+ status_filter = data.get("StatusFilter") or data.get("statusFilter")
2915
+ if isinstance(status_filter, list):
2916
+ status_filter = status_filter if status_filter else None
2917
+ elif status_filter:
2918
+ status_filter = [status_filter]
2919
+
2920
+ started_after = data.get("StartedAfter") or data.get("startedAfter")
2921
+ if isinstance(started_after, list):
2922
+ started_after = started_after[0] if started_after else None
2923
+
2924
+ started_before = data.get("StartedBefore") or data.get("startedBefore")
2925
+ if isinstance(started_before, list):
2926
+ started_before = started_before[0] if started_before else None
2927
+
2928
+ marker = data.get("Marker") or data.get("marker")
2929
+ if isinstance(marker, list):
2930
+ marker = marker[0] if marker else None
2931
+
2932
+ max_items = data.get("MaxItems") or data.get("maxItems", 0)
2933
+ if isinstance(max_items, list):
2934
+ max_items = int(max_items[0]) if max_items else 0
2935
+
2936
+ reverse_order = data.get("ReverseOrder") or data.get("reverseOrder")
2937
+ if isinstance(reverse_order, list):
2938
+ reverse_order = (
2939
+ reverse_order[0].lower() in ("true", "1", "yes")
2940
+ if reverse_order
2941
+ else None
2942
+ )
2943
+ elif isinstance(reverse_order, str):
2944
+ reverse_order = reverse_order.lower() in ("true", "1", "yes")
2945
+
2946
+ return cls(
2947
+ function_name=function_name,
2948
+ qualifier=qualifier,
2949
+ durable_execution_name=durable_execution_name,
2950
+ status_filter=status_filter,
2951
+ started_after=started_after,
2952
+ started_before=started_before,
2953
+ marker=marker,
2954
+ max_items=max_items,
2955
+ reverse_order=reverse_order,
2956
+ )
2957
+
2958
+ def to_dict(self) -> dict[str, Any]:
2959
+ result: dict[str, Any] = {"FunctionName": self.function_name}
2960
+ if self.qualifier is not None:
2961
+ result["Qualifier"] = self.qualifier
2962
+ if self.durable_execution_name is not None:
2963
+ result["DurableExecutionName"] = self.durable_execution_name
2964
+ if self.status_filter is not None:
2965
+ result["StatusFilter"] = self.status_filter
2966
+ if self.started_after is not None:
2967
+ result["StartedAfter"] = self.started_after
2968
+ if self.started_before is not None:
2969
+ result["StartedBefore"] = self.started_before
2970
+ if self.marker is not None:
2971
+ result["Marker"] = self.marker
2972
+ if self.max_items is not None:
2973
+ result["MaxItems"] = self.max_items
2974
+ if self.reverse_order is not None:
2975
+ result["ReverseOrder"] = self.reverse_order
2976
+ return result
2977
+
2978
+
2979
+ @dataclass(frozen=True)
2980
+ class ListDurableExecutionsByFunctionResponse:
2981
+ """Response containing list of durable executions by function."""
2982
+
2983
+ durable_executions: list[Execution]
2984
+ next_marker: str | None = None
2985
+
2986
+ @classmethod
2987
+ def from_dict(cls, data: dict) -> ListDurableExecutionsByFunctionResponse:
2988
+ executions = [
2989
+ Execution.from_dict(exec_data)
2990
+ for exec_data in data.get("DurableExecutions", [])
2991
+ ]
2992
+ return cls(
2993
+ durable_executions=executions,
2994
+ next_marker=data.get("NextMarker"),
2995
+ )
2996
+
2997
+ def to_dict(self) -> dict[str, Any]:
2998
+ result: dict[str, Any] = {
2999
+ "DurableExecutions": [exe.to_dict() for exe in self.durable_executions]
3000
+ }
3001
+ if self.next_marker is not None:
3002
+ result["NextMarker"] = self.next_marker
3003
+ return result
3004
+
3005
+
3006
+ # endregion history_models
3007
+
3008
+
3009
+ # region callback_models
3010
+ # Callback-related models
3011
+ @dataclass(frozen=True)
3012
+ class SendDurableExecutionCallbackSuccessRequest:
3013
+ """Request to send callback success."""
3014
+
3015
+ callback_id: str
3016
+ result: bytes | None = None
3017
+
3018
+ @classmethod
3019
+ def from_dict(cls, data: dict) -> SendDurableExecutionCallbackSuccessRequest:
3020
+ return cls(
3021
+ callback_id=data["CallbackId"],
3022
+ result=data.get("Result"),
3023
+ )
3024
+
3025
+ def to_dict(self) -> dict[str, Any]:
3026
+ result: dict[str, Any] = {"CallbackId": self.callback_id}
3027
+ if self.result is not None:
3028
+ result["Result"] = self.result
3029
+ return result
3030
+
3031
+
3032
+ @dataclass(frozen=True)
3033
+ class SendDurableExecutionCallbackSuccessResponse:
3034
+ """Response from sending callback success."""
3035
+
3036
+
3037
+ @dataclass(frozen=True)
3038
+ class SendDurableExecutionCallbackFailureRequest:
3039
+ """Request to send callback failure."""
3040
+
3041
+ callback_id: str
3042
+ error: ErrorObject | None = None
3043
+
3044
+ @classmethod
3045
+ def from_dict(
3046
+ cls, data: dict, callback_id: str
3047
+ ) -> SendDurableExecutionCallbackFailureRequest:
3048
+ error = ErrorObject.from_dict(data) if data else None
3049
+
3050
+ return cls(
3051
+ callback_id=callback_id,
3052
+ error=error,
3053
+ )
3054
+
3055
+ def to_dict(self) -> dict[str, Any]:
3056
+ result: dict[str, Any] = {"CallbackId": self.callback_id}
3057
+ if self.error is not None:
3058
+ result["Error"] = self.error.to_dict()
3059
+ return result
3060
+
3061
+
3062
+ @dataclass(frozen=True)
3063
+ class SendDurableExecutionCallbackFailureResponse:
3064
+ """Response from sending callback failure."""
3065
+
3066
+
3067
+ @dataclass(frozen=True)
3068
+ class SendDurableExecutionCallbackHeartbeatRequest:
3069
+ """Request to send callback heartbeat."""
3070
+
3071
+ callback_id: str
3072
+
3073
+ @classmethod
3074
+ def from_dict(cls, data: dict) -> SendDurableExecutionCallbackHeartbeatRequest:
3075
+ return cls(callback_id=data["CallbackId"])
3076
+
3077
+ def to_dict(self) -> dict[str, Any]:
3078
+ return {"CallbackId": self.callback_id}
3079
+
3080
+
3081
+ @dataclass(frozen=True)
3082
+ class SendDurableExecutionCallbackHeartbeatResponse:
3083
+ """Response from sending callback heartbeat."""
3084
+
3085
+
3086
+ # endregion callback_models
3087
+
3088
+
3089
+ # region checkpoint_models
3090
+ # Checkpoint-related models
3091
+ @dataclass(frozen=True)
3092
+ class CheckpointUpdatedExecutionState:
3093
+ """Updated execution state from checkpoint."""
3094
+
3095
+ operations: list[Operation]
3096
+ next_marker: str | None = None
3097
+
3098
+ @classmethod
3099
+ def from_dict(cls, data: dict) -> CheckpointUpdatedExecutionState:
3100
+ operations = [
3101
+ Operation.from_dict(op_data) for op_data in data.get("Operations", [])
3102
+ ]
3103
+ return cls(
3104
+ operations=operations,
3105
+ next_marker=data.get("NextMarker"),
3106
+ )
3107
+
3108
+ def to_dict(self) -> dict[str, Any]:
3109
+ result: dict[str, Any] = {
3110
+ "Operations": [op.to_dict() for op in self.operations]
3111
+ }
3112
+ if self.next_marker is not None:
3113
+ result["NextMarker"] = self.next_marker
3114
+ return result
3115
+
3116
+
3117
+ @dataclass(frozen=True)
3118
+ class CheckpointDurableExecutionRequest:
3119
+ """Request to checkpoint a durable execution."""
3120
+
3121
+ durable_execution_arn: str
3122
+ checkpoint_token: str
3123
+ updates: list[OperationUpdate] | None = None
3124
+ client_token: str | None = None
3125
+
3126
+ @classmethod
3127
+ def from_dict(
3128
+ cls, data: dict, durable_execution_arn: str
3129
+ ) -> CheckpointDurableExecutionRequest:
3130
+ updates = None
3131
+ if updates_data := data.get("Updates"):
3132
+ updates = []
3133
+ for update_data in updates_data:
3134
+ # Map dictionary fields to OperationUpdate constructor parameters
3135
+ operation_update = OperationUpdate(
3136
+ operation_id=update_data["Id"],
3137
+ operation_type=OperationType(update_data["Type"]),
3138
+ action=OperationAction(update_data["Action"]),
3139
+ parent_id=update_data.get("ParentId"),
3140
+ name=update_data.get("Name"),
3141
+ sub_type=OperationSubType(update_data["SubType"])
3142
+ if update_data.get("SubType")
3143
+ else None,
3144
+ payload=update_data.get("Payload"),
3145
+ error=ErrorObject.from_dict(update_data["Error"])
3146
+ if update_data.get("Error")
3147
+ else None,
3148
+ context_options=ContextOptions.from_dict(
3149
+ update_data["ContextOptions"]
3150
+ )
3151
+ if update_data.get("ContextOptions")
3152
+ else None,
3153
+ step_options=StepOptions.from_dict(update_data["StepOptions"])
3154
+ if update_data.get("StepOptions")
3155
+ else None,
3156
+ wait_options=WaitOptions.from_dict(update_data["WaitOptions"])
3157
+ if update_data.get("WaitOptions")
3158
+ else None,
3159
+ callback_options=CallbackOptions.from_dict(
3160
+ update_data["CallbackOptions"]
3161
+ )
3162
+ if update_data.get("CallbackOptions")
3163
+ else None,
3164
+ chained_invoke_options=ChainedInvokeOptions.from_dict(
3165
+ update_data["ChainedInvokeOptions"]
3166
+ )
3167
+ if update_data.get("ChainedInvokeOptions")
3168
+ else None,
3169
+ )
3170
+ updates.append(operation_update)
3171
+
3172
+ return cls(
3173
+ durable_execution_arn=durable_execution_arn,
3174
+ checkpoint_token=data["CheckpointToken"],
3175
+ updates=updates,
3176
+ client_token=data.get("ClientToken"),
3177
+ )
3178
+
3179
+ def to_dict(self) -> dict[str, Any]:
3180
+ result: dict[str, Any] = {
3181
+ "DurableExecutionArn": self.durable_execution_arn,
3182
+ "CheckpointToken": self.checkpoint_token,
3183
+ }
3184
+ if self.updates is not None:
3185
+ result["Updates"] = [update.to_dict() for update in self.updates]
3186
+ if self.client_token is not None:
3187
+ result["ClientToken"] = self.client_token
3188
+ return result
3189
+
3190
+
3191
+ @dataclass(frozen=True)
3192
+ class CheckpointDurableExecutionResponse:
3193
+ """Response from checkpointing a durable execution."""
3194
+
3195
+ checkpoint_token: str
3196
+ new_execution_state: CheckpointUpdatedExecutionState | None = None
3197
+
3198
+ @classmethod
3199
+ def from_dict(cls, data: dict) -> CheckpointDurableExecutionResponse:
3200
+ new_execution_state = None
3201
+ if state_data := data.get("NewExecutionState"):
3202
+ new_execution_state = CheckpointUpdatedExecutionState.from_dict(state_data)
3203
+
3204
+ return cls(
3205
+ checkpoint_token=data["CheckpointToken"],
3206
+ new_execution_state=new_execution_state,
3207
+ )
3208
+
3209
+ def to_dict(self) -> dict[str, Any]:
3210
+ result: dict[str, Any] = {"CheckpointToken": self.checkpoint_token}
3211
+ if self.new_execution_state is not None:
3212
+ result["NewExecutionState"] = self.new_execution_state.to_dict()
3213
+ return result
3214
+
3215
+
3216
+ # endregion checkpoint_models
3217
+
3218
+
3219
+ # region error_models
3220
+ # Error response structure for consistent error handling
3221
+ @dataclass(frozen=True)
3222
+ class ErrorResponse:
3223
+ """Structured error response for web service operations."""
3224
+
3225
+ error_type: str
3226
+ error_message: str
3227
+ error_code: str | None = None
3228
+ request_id: str | None = None
3229
+
3230
+ @classmethod
3231
+ def from_dict(cls, data: dict) -> ErrorResponse:
3232
+ """Create ErrorResponse from dictionary.
3233
+
3234
+ Args:
3235
+ data: Dictionary containing error data
3236
+
3237
+ Returns:
3238
+ ErrorResponse: The error response object
3239
+ """
3240
+ error_data = data.get("error", data) # Support both nested and flat structures
3241
+ return cls(
3242
+ error_type=error_data["type"],
3243
+ error_message=error_data["message"],
3244
+ error_code=error_data.get("code"),
3245
+ request_id=error_data.get("requestId"),
3246
+ )
3247
+
3248
+ def to_dict(self) -> dict[str, Any]:
3249
+ """Convert ErrorResponse to dictionary.
3250
+
3251
+ Returns:
3252
+ dict: Dictionary representation of the error response
3253
+ """
3254
+ error_data: dict[str, Any] = {
3255
+ "type": self.error_type,
3256
+ "message": self.error_message,
3257
+ }
3258
+
3259
+ if self.error_code is not None:
3260
+ error_data["code"] = self.error_code
3261
+ if self.request_id is not None:
3262
+ error_data["requestId"] = self.request_id
3263
+
3264
+ return {"error": error_data}
3265
+
3266
+
3267
+ # endregion error_models