async-durable-execution-runner 2.0.0a1__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 (55) hide show
  1. LICENSE +175 -0
  2. NOTICE +8 -0
  3. VERSION.py +5 -0
  4. async_durable_execution_runner/__about__.py +33 -0
  5. async_durable_execution_runner/__init__.py +23 -0
  6. async_durable_execution_runner/checkpoint/__init__.py +1 -0
  7. async_durable_execution_runner/checkpoint/processor.py +101 -0
  8. async_durable_execution_runner/checkpoint/processors/__init__.py +1 -0
  9. async_durable_execution_runner/checkpoint/processors/base.py +199 -0
  10. async_durable_execution_runner/checkpoint/processors/callback.py +89 -0
  11. async_durable_execution_runner/checkpoint/processors/context.py +59 -0
  12. async_durable_execution_runner/checkpoint/processors/execution.py +52 -0
  13. async_durable_execution_runner/checkpoint/processors/step.py +124 -0
  14. async_durable_execution_runner/checkpoint/processors/wait.py +95 -0
  15. async_durable_execution_runner/checkpoint/transformer.py +104 -0
  16. async_durable_execution_runner/checkpoint/validators/__init__.py +1 -0
  17. async_durable_execution_runner/checkpoint/validators/checkpoint.py +242 -0
  18. async_durable_execution_runner/checkpoint/validators/operations/__init__.py +1 -0
  19. async_durable_execution_runner/checkpoint/validators/operations/callback.py +45 -0
  20. async_durable_execution_runner/checkpoint/validators/operations/context.py +73 -0
  21. async_durable_execution_runner/checkpoint/validators/operations/execution.py +47 -0
  22. async_durable_execution_runner/checkpoint/validators/operations/invoke.py +56 -0
  23. async_durable_execution_runner/checkpoint/validators/operations/step.py +106 -0
  24. async_durable_execution_runner/checkpoint/validators/operations/wait.py +54 -0
  25. async_durable_execution_runner/checkpoint/validators/transitions.py +66 -0
  26. async_durable_execution_runner/cli.py +498 -0
  27. async_durable_execution_runner/client.py +50 -0
  28. async_durable_execution_runner/exceptions.py +288 -0
  29. async_durable_execution_runner/execution.py +444 -0
  30. async_durable_execution_runner/executor.py +1234 -0
  31. async_durable_execution_runner/invoker.py +340 -0
  32. async_durable_execution_runner/model.py +3296 -0
  33. async_durable_execution_runner/observer.py +144 -0
  34. async_durable_execution_runner/py.typed +1 -0
  35. async_durable_execution_runner/runner.py +1167 -0
  36. async_durable_execution_runner/scheduler.py +246 -0
  37. async_durable_execution_runner/stores/__init__.py +1 -0
  38. async_durable_execution_runner/stores/base.py +147 -0
  39. async_durable_execution_runner/stores/filesystem.py +79 -0
  40. async_durable_execution_runner/stores/memory.py +38 -0
  41. async_durable_execution_runner/stores/sqlite.py +273 -0
  42. async_durable_execution_runner/token.py +49 -0
  43. async_durable_execution_runner/web/__init__.py +1 -0
  44. async_durable_execution_runner/web/errors.py +8 -0
  45. async_durable_execution_runner/web/handlers.py +813 -0
  46. async_durable_execution_runner/web/models.py +266 -0
  47. async_durable_execution_runner/web/routes.py +692 -0
  48. async_durable_execution_runner/web/serialization.py +235 -0
  49. async_durable_execution_runner/web/server.py +243 -0
  50. async_durable_execution_runner-2.0.0a1.dist-info/METADATA +238 -0
  51. async_durable_execution_runner-2.0.0a1.dist-info/RECORD +55 -0
  52. async_durable_execution_runner-2.0.0a1.dist-info/WHEEL +4 -0
  53. async_durable_execution_runner-2.0.0a1.dist-info/entry_points.txt +2 -0
  54. async_durable_execution_runner-2.0.0a1.dist-info/licenses/LICENSE +175 -0
  55. async_durable_execution_runner-2.0.0a1.dist-info/licenses/NOTICE +1 -0
@@ -0,0 +1,3296 @@
1
+ """Model classes for the web API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ import json
7
+ from dataclasses import dataclass, replace
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+ from async_durable_execution.execution import DurableExecutionInvocationOutput
12
+
13
+ # Import existing types from the main SDK - REUSE EVERYTHING POSSIBLE
14
+ from async_durable_execution.lambda_service import (
15
+ CallbackDetails,
16
+ CallbackOptions,
17
+ ChainedInvokeDetails,
18
+ ChainedInvokeOptions,
19
+ ContextDetails,
20
+ ContextOptions,
21
+ ErrorObject,
22
+ ExecutionDetails,
23
+ Operation,
24
+ OperationAction,
25
+ OperationStatus,
26
+ OperationSubType,
27
+ OperationType,
28
+ OperationUpdate,
29
+ StepDetails,
30
+ StepOptions,
31
+ TimestampConverter,
32
+ WaitDetails,
33
+ WaitOptions,
34
+ )
35
+ from async_durable_execution.types import (
36
+ LambdaContext as LambdaContextProtocol,
37
+ )
38
+ from dateutil.tz import UTC
39
+
40
+ from async_durable_execution_runner.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
+ @classmethod
1243
+ def from_json_dict(cls, data: dict) -> InvocationCompletedDetails:
1244
+ """Deserialize from JSON dict with Unix millisecond timestamps."""
1245
+ start_ts: datetime.datetime | None = TimestampConverter.from_unix_millis(
1246
+ data["StartTimestamp"]
1247
+ ) # type: ignore[arg-type]
1248
+ end_ts: datetime.datetime | None = TimestampConverter.from_unix_millis(
1249
+ data["EndTimestamp"]
1250
+ ) # type: ignore[arg-type]
1251
+
1252
+ if start_ts is None or end_ts is None:
1253
+ raise InvalidParameterValueException(
1254
+ "StartTimestamp and EndTimestamp cannot be null"
1255
+ )
1256
+
1257
+ return cls(
1258
+ start_timestamp=start_ts,
1259
+ end_timestamp=end_ts,
1260
+ request_id=data["RequestId"],
1261
+ )
1262
+
1263
+ def to_dict(self) -> dict[str, Any]:
1264
+ return {
1265
+ "StartTimestamp": self.start_timestamp,
1266
+ "EndTimestamp": self.end_timestamp,
1267
+ "RequestId": self.request_id,
1268
+ }
1269
+
1270
+ def to_json_dict(self) -> dict[str, Any]:
1271
+ """Convert to JSON-serializable dict with Unix millisecond timestamps."""
1272
+ return {
1273
+ "StartTimestamp": TimestampConverter.to_unix_millis(self.start_timestamp),
1274
+ "EndTimestamp": TimestampConverter.to_unix_millis(self.end_timestamp),
1275
+ "RequestId": self.request_id,
1276
+ }
1277
+
1278
+
1279
+ # endregion event_structures
1280
+
1281
+
1282
+ @dataclass(frozen=True)
1283
+ class EventCreationContext:
1284
+ operation: Operation
1285
+ event_id: int
1286
+ durable_execution_arn: str
1287
+ start_durable_execution_input: StartDurableExecutionInput
1288
+ durable_execution_invocation_output: DurableExecutionInvocationOutput | None = None
1289
+ operation_update: OperationUpdate | None = None
1290
+ include_execution_data: bool = False # noqa: FBT001, FBT002
1291
+
1292
+ @classmethod
1293
+ def create(
1294
+ cls,
1295
+ operation: Operation,
1296
+ event_id: int,
1297
+ durable_execution_arn: str,
1298
+ start_input: StartDurableExecutionInput,
1299
+ result: DurableExecutionInvocationOutput | None = None,
1300
+ operation_update: OperationUpdate | None = None,
1301
+ include_execution_data: bool = False, # noqa: FBT001, FBT002
1302
+ ) -> EventCreationContext:
1303
+ return cls(
1304
+ operation=operation,
1305
+ event_id=event_id,
1306
+ durable_execution_arn=durable_execution_arn,
1307
+ start_durable_execution_input=start_input,
1308
+ durable_execution_invocation_output=result,
1309
+ operation_update=operation_update,
1310
+ include_execution_data=include_execution_data,
1311
+ )
1312
+
1313
+ @property
1314
+ def sub_type(self) -> str | None:
1315
+ return self.operation.sub_type.value if self.operation.sub_type else None
1316
+
1317
+ def get_retry_details(self) -> RetryDetails | None:
1318
+ if not self.operation.step_details or not self.operation_update:
1319
+ return None
1320
+
1321
+ delay = 0
1322
+ if (
1323
+ self.operation_update.operation_type == OperationType.STEP
1324
+ and self.operation_update.step_options
1325
+ ):
1326
+ delay = self.operation_update.step_options.next_attempt_delay_seconds
1327
+
1328
+ return RetryDetails(
1329
+ current_attempt=self.operation.step_details.attempt,
1330
+ next_attempt_delay_seconds=delay,
1331
+ )
1332
+
1333
+ @property
1334
+ def start_timestamp(self) -> datetime.datetime:
1335
+ return (
1336
+ self.operation.start_timestamp
1337
+ if self.operation.start_timestamp is not None
1338
+ else datetime.datetime.now(UTC)
1339
+ )
1340
+
1341
+ @property
1342
+ def end_timestamp(self) -> datetime.datetime:
1343
+ return (
1344
+ self.operation.end_timestamp
1345
+ if self.operation.end_timestamp is not None
1346
+ else datetime.datetime.now(UTC)
1347
+ )
1348
+
1349
+
1350
+ # region event_class
1351
+ @dataclass(frozen=True)
1352
+ class Event:
1353
+ """Event structure from Smithy model."""
1354
+
1355
+ event_type: str
1356
+ event_timestamp: datetime.datetime
1357
+ sub_type: str | None = None
1358
+ event_id: int = 1
1359
+ operation_id: str | None = None
1360
+ name: str | None = None
1361
+ parent_id: str | None = None
1362
+ execution_started_details: ExecutionStartedDetails | None = None
1363
+ execution_succeeded_details: ExecutionSucceededDetails | None = None
1364
+ execution_failed_details: ExecutionFailedDetails | None = None
1365
+ execution_timed_out_details: ExecutionTimedOutDetails | None = None
1366
+ execution_stopped_details: ExecutionStoppedDetails | None = None
1367
+ context_started_details: ContextStartedDetails | None = None
1368
+ context_succeeded_details: ContextSucceededDetails | None = None
1369
+ context_failed_details: ContextFailedDetails | None = None
1370
+ wait_started_details: WaitStartedDetails | None = None
1371
+ wait_succeeded_details: WaitSucceededDetails | None = None
1372
+ wait_cancelled_details: WaitCancelledDetails | None = None
1373
+ step_started_details: StepStartedDetails | None = None
1374
+ step_succeeded_details: StepSucceededDetails | None = None
1375
+ step_failed_details: StepFailedDetails | None = None
1376
+ chained_invoke_pending_details: ChainedInvokePendingDetails | None = None
1377
+ chained_invoke_started_details: ChainedInvokeStartedDetails | None = None
1378
+ chained_invoke_succeeded_details: ChainedInvokeSucceededDetails | None = None
1379
+ chained_invoke_failed_details: ChainedInvokeFailedDetails | None = None
1380
+ chained_invoke_timed_out_details: ChainedInvokeTimedOutDetails | None = None
1381
+ chained_invoke_stopped_details: ChainedInvokeStoppedDetails | None = None
1382
+ callback_started_details: CallbackStartedDetails | None = None
1383
+ callback_succeeded_details: CallbackSucceededDetails | None = None
1384
+ callback_failed_details: CallbackFailedDetails | None = None
1385
+ callback_timed_out_details: CallbackTimedOutDetails | None = None
1386
+ invocation_completed_details: InvocationCompletedDetails | None = None
1387
+
1388
+ @classmethod
1389
+ def from_dict(cls, data: dict) -> Event:
1390
+ # Parse all the detail structures
1391
+ execution_started_details = None
1392
+ if details_data := data.get("ExecutionStartedDetails"):
1393
+ execution_started_details = ExecutionStartedDetails.from_dict(details_data)
1394
+
1395
+ execution_succeeded_details = None
1396
+ if details_data := data.get("ExecutionSucceededDetails"):
1397
+ execution_succeeded_details = ExecutionSucceededDetails.from_dict(
1398
+ details_data
1399
+ )
1400
+
1401
+ execution_failed_details = None
1402
+ if details_data := data.get("ExecutionFailedDetails"):
1403
+ execution_failed_details = ExecutionFailedDetails.from_dict(details_data)
1404
+
1405
+ execution_timed_out_details = None
1406
+ if details_data := data.get("ExecutionTimedOutDetails"):
1407
+ execution_timed_out_details = ExecutionTimedOutDetails.from_dict(
1408
+ details_data
1409
+ )
1410
+
1411
+ execution_stopped_details = None
1412
+ if details_data := data.get("ExecutionStoppedDetails"):
1413
+ execution_stopped_details = ExecutionStoppedDetails.from_dict(details_data)
1414
+
1415
+ context_started_details = None
1416
+ if details_data := data.get("ContextStartedDetails"):
1417
+ context_started_details = ContextStartedDetails.from_dict(details_data)
1418
+
1419
+ context_succeeded_details = None
1420
+ if details_data := data.get("ContextSucceededDetails"):
1421
+ context_succeeded_details = ContextSucceededDetails.from_dict(details_data)
1422
+
1423
+ context_failed_details = None
1424
+ if details_data := data.get("ContextFailedDetails"):
1425
+ context_failed_details = ContextFailedDetails.from_dict(details_data)
1426
+
1427
+ wait_started_details = None
1428
+ if details_data := data.get("WaitStartedDetails"):
1429
+ wait_started_details = WaitStartedDetails.from_dict(details_data)
1430
+
1431
+ wait_succeeded_details = None
1432
+ if details_data := data.get("WaitSucceededDetails"):
1433
+ wait_succeeded_details = WaitSucceededDetails.from_dict(details_data)
1434
+
1435
+ wait_cancelled_details = None
1436
+ if details_data := data.get("WaitCancelledDetails"):
1437
+ wait_cancelled_details = WaitCancelledDetails.from_dict(details_data)
1438
+
1439
+ step_started_details = None
1440
+ if details_data := data.get("StepStartedDetails"):
1441
+ step_started_details = StepStartedDetails.from_dict(details_data)
1442
+
1443
+ step_succeeded_details = None
1444
+ if details_data := data.get("StepSucceededDetails"):
1445
+ step_succeeded_details = StepSucceededDetails.from_dict(details_data)
1446
+
1447
+ step_failed_details = None
1448
+ if details_data := data.get("StepFailedDetails"):
1449
+ step_failed_details = StepFailedDetails.from_dict(details_data)
1450
+
1451
+ chained_invoke_pending_details = None
1452
+ if details_data := data.get("ChainedInvokePendingDetails"):
1453
+ chained_invoke_pending_details = ChainedInvokePendingDetails.from_dict(
1454
+ details_data
1455
+ )
1456
+
1457
+ chained_invoke_started_details = None
1458
+ if details_data := data.get("ChainedInvokeStartedDetails"):
1459
+ chained_invoke_started_details = ChainedInvokeStartedDetails.from_dict(
1460
+ details_data
1461
+ )
1462
+
1463
+ chained_invoke_succeeded_details = None
1464
+ if details_data := data.get("ChainedInvokeSucceededDetails"):
1465
+ chained_invoke_succeeded_details = ChainedInvokeSucceededDetails.from_dict(
1466
+ details_data
1467
+ )
1468
+
1469
+ chained_invoke_failed_details = None
1470
+ if details_data := data.get("ChainedInvokeFailedDetails"):
1471
+ chained_invoke_failed_details = ChainedInvokeFailedDetails.from_dict(
1472
+ details_data
1473
+ )
1474
+
1475
+ chained_invoke_timed_out_details = None
1476
+ if details_data := data.get("ChainedInvokeTimedOutDetails"):
1477
+ chained_invoke_timed_out_details = ChainedInvokeTimedOutDetails.from_dict(
1478
+ details_data
1479
+ )
1480
+
1481
+ chained_invoke_stopped_details = None
1482
+ if details_data := data.get("ChainedInvokeStoppedDetails"):
1483
+ chained_invoke_stopped_details = ChainedInvokeStoppedDetails.from_dict(
1484
+ details_data
1485
+ )
1486
+
1487
+ callback_started_details = None
1488
+ if details_data := data.get("CallbackStartedDetails"):
1489
+ callback_started_details = CallbackStartedDetails.from_dict(details_data)
1490
+
1491
+ callback_succeeded_details = None
1492
+ if details_data := data.get("CallbackSucceededDetails"):
1493
+ callback_succeeded_details = CallbackSucceededDetails.from_dict(
1494
+ details_data
1495
+ )
1496
+
1497
+ callback_failed_details = None
1498
+ if details_data := data.get("CallbackFailedDetails"):
1499
+ callback_failed_details = CallbackFailedDetails.from_dict(details_data)
1500
+
1501
+ callback_timed_out_details = None
1502
+ if details_data := data.get("CallbackTimedOutDetails"):
1503
+ callback_timed_out_details = CallbackTimedOutDetails.from_dict(details_data)
1504
+
1505
+ invocation_completed_details = None
1506
+ if details_data := data.get("InvocationCompletedDetails"):
1507
+ invocation_completed_details = InvocationCompletedDetails.from_dict(
1508
+ details_data
1509
+ )
1510
+
1511
+ return cls(
1512
+ event_type=data["EventType"],
1513
+ event_timestamp=data["EventTimestamp"],
1514
+ sub_type=data.get("SubType"),
1515
+ event_id=data.get("EventId", 1),
1516
+ operation_id=data.get("Id"),
1517
+ name=data.get("Name"),
1518
+ parent_id=data.get("ParentId"),
1519
+ execution_started_details=execution_started_details,
1520
+ execution_succeeded_details=execution_succeeded_details,
1521
+ execution_failed_details=execution_failed_details,
1522
+ execution_timed_out_details=execution_timed_out_details,
1523
+ execution_stopped_details=execution_stopped_details,
1524
+ context_started_details=context_started_details,
1525
+ context_succeeded_details=context_succeeded_details,
1526
+ context_failed_details=context_failed_details,
1527
+ wait_started_details=wait_started_details,
1528
+ wait_succeeded_details=wait_succeeded_details,
1529
+ wait_cancelled_details=wait_cancelled_details,
1530
+ step_started_details=step_started_details,
1531
+ step_succeeded_details=step_succeeded_details,
1532
+ step_failed_details=step_failed_details,
1533
+ chained_invoke_pending_details=chained_invoke_pending_details,
1534
+ chained_invoke_started_details=chained_invoke_started_details,
1535
+ chained_invoke_succeeded_details=chained_invoke_succeeded_details,
1536
+ chained_invoke_failed_details=chained_invoke_failed_details,
1537
+ chained_invoke_timed_out_details=chained_invoke_timed_out_details,
1538
+ chained_invoke_stopped_details=chained_invoke_stopped_details,
1539
+ callback_started_details=callback_started_details,
1540
+ callback_succeeded_details=callback_succeeded_details,
1541
+ callback_failed_details=callback_failed_details,
1542
+ callback_timed_out_details=callback_timed_out_details,
1543
+ invocation_completed_details=invocation_completed_details,
1544
+ )
1545
+
1546
+ def to_dict(self) -> dict[str, Any]:
1547
+ result: dict[str, Any] = {
1548
+ "EventType": self.event_type,
1549
+ "EventTimestamp": self.event_timestamp,
1550
+ "EventId": self.event_id,
1551
+ }
1552
+ if self.sub_type is not None:
1553
+ result["SubType"] = self.sub_type
1554
+ if self.operation_id is not None:
1555
+ result["Id"] = self.operation_id
1556
+ if self.name is not None:
1557
+ result["Name"] = self.name
1558
+ if self.parent_id is not None:
1559
+ result["ParentId"] = self.parent_id
1560
+ if self.execution_started_details is not None:
1561
+ result["ExecutionStartedDetails"] = self.execution_started_details.to_dict()
1562
+ if self.execution_succeeded_details is not None:
1563
+ result["ExecutionSucceededDetails"] = (
1564
+ self.execution_succeeded_details.to_dict()
1565
+ )
1566
+ if self.execution_failed_details is not None:
1567
+ result["ExecutionFailedDetails"] = self.execution_failed_details.to_dict()
1568
+ if self.execution_timed_out_details is not None:
1569
+ result["ExecutionTimedOutDetails"] = (
1570
+ self.execution_timed_out_details.to_dict()
1571
+ )
1572
+ if self.execution_stopped_details is not None:
1573
+ result["ExecutionStoppedDetails"] = self.execution_stopped_details.to_dict()
1574
+ if self.context_started_details is not None:
1575
+ result["ContextStartedDetails"] = self.context_started_details.to_dict()
1576
+ if self.context_succeeded_details is not None:
1577
+ result["ContextSucceededDetails"] = self.context_succeeded_details.to_dict()
1578
+ if self.context_failed_details is not None:
1579
+ result["ContextFailedDetails"] = self.context_failed_details.to_dict()
1580
+ if self.wait_started_details is not None:
1581
+ result["WaitStartedDetails"] = self.wait_started_details.to_dict()
1582
+ if self.wait_succeeded_details is not None:
1583
+ result["WaitSucceededDetails"] = self.wait_succeeded_details.to_dict()
1584
+ if self.wait_cancelled_details is not None:
1585
+ result["WaitCancelledDetails"] = self.wait_cancelled_details.to_dict()
1586
+ if self.step_started_details is not None:
1587
+ result["StepStartedDetails"] = self.step_started_details.to_dict()
1588
+ if self.step_succeeded_details is not None:
1589
+ result["StepSucceededDetails"] = self.step_succeeded_details.to_dict()
1590
+ if self.step_failed_details is not None:
1591
+ result["StepFailedDetails"] = self.step_failed_details.to_dict()
1592
+ if self.chained_invoke_pending_details is not None:
1593
+ result["ChainedInvokePendingDetails"] = (
1594
+ self.chained_invoke_pending_details.to_dict()
1595
+ )
1596
+ if self.chained_invoke_started_details is not None:
1597
+ result["ChainedInvokeStartedDetails"] = (
1598
+ self.chained_invoke_started_details.to_dict()
1599
+ )
1600
+ if self.chained_invoke_succeeded_details is not None:
1601
+ result["ChainedInvokeSucceededDetails"] = (
1602
+ self.chained_invoke_succeeded_details.to_dict()
1603
+ )
1604
+ if self.chained_invoke_failed_details is not None:
1605
+ result["ChainedInvokeFailedDetails"] = (
1606
+ self.chained_invoke_failed_details.to_dict()
1607
+ )
1608
+ if self.chained_invoke_timed_out_details is not None:
1609
+ result["ChainedInvokeTimedOutDetails"] = (
1610
+ self.chained_invoke_timed_out_details.to_dict()
1611
+ )
1612
+ if self.chained_invoke_stopped_details is not None:
1613
+ result["ChainedInvokeStoppedDetails"] = (
1614
+ self.chained_invoke_stopped_details.to_dict()
1615
+ )
1616
+ if self.callback_started_details is not None:
1617
+ result["CallbackStartedDetails"] = self.callback_started_details.to_dict()
1618
+ if self.callback_succeeded_details is not None:
1619
+ result["CallbackSucceededDetails"] = (
1620
+ self.callback_succeeded_details.to_dict()
1621
+ )
1622
+ if self.callback_failed_details is not None:
1623
+ result["CallbackFailedDetails"] = self.callback_failed_details.to_dict()
1624
+ if self.callback_timed_out_details is not None:
1625
+ result["CallbackTimedOutDetails"] = (
1626
+ self.callback_timed_out_details.to_dict()
1627
+ )
1628
+ if self.invocation_completed_details is not None:
1629
+ result["InvocationCompletedDetails"] = (
1630
+ self.invocation_completed_details.to_dict()
1631
+ )
1632
+ return result
1633
+
1634
+ # region execution
1635
+ @classmethod
1636
+ def create_execution_event_started(cls, context: EventCreationContext) -> Event:
1637
+ execution_details: ExecutionDetails | None = context.operation.execution_details
1638
+ event_input: EventInput | None = (
1639
+ EventInput.from_details(execution_details, context.include_execution_data)
1640
+ if execution_details
1641
+ else None
1642
+ )
1643
+ execution_timeout: int | None = (
1644
+ context.start_durable_execution_input.execution_timeout_seconds
1645
+ )
1646
+
1647
+ return cls(
1648
+ event_type=EventType.EXECUTION_STARTED.value,
1649
+ event_timestamp=context.start_timestamp,
1650
+ sub_type=context.sub_type,
1651
+ event_id=context.event_id,
1652
+ operation_id=context.operation.operation_id,
1653
+ name=context.operation.name,
1654
+ parent_id=context.operation.parent_id,
1655
+ execution_started_details=ExecutionStartedDetails(
1656
+ input=event_input,
1657
+ execution_timeout=execution_timeout,
1658
+ ),
1659
+ )
1660
+
1661
+ @classmethod
1662
+ def create_execution_event_succeeded(cls, context: EventCreationContext) -> Event:
1663
+ result: EventResult | None = (
1664
+ EventResult.from_durable_execution_invocation_output(
1665
+ context.durable_execution_invocation_output,
1666
+ context.include_execution_data,
1667
+ )
1668
+ if context.durable_execution_invocation_output
1669
+ else None
1670
+ )
1671
+ return cls(
1672
+ event_type=EventType.EXECUTION_SUCCEEDED.value,
1673
+ event_timestamp=context.end_timestamp,
1674
+ sub_type=context.sub_type,
1675
+ event_id=context.event_id,
1676
+ operation_id=context.operation.operation_id,
1677
+ name=context.operation.name,
1678
+ parent_id=context.operation.parent_id,
1679
+ execution_succeeded_details=ExecutionSucceededDetails(result=result),
1680
+ )
1681
+
1682
+ @classmethod
1683
+ def create_execution_event_failed(cls, context: EventCreationContext) -> Event:
1684
+ error: EventError | None = (
1685
+ EventError.from_durable_execution_invocation_output(
1686
+ context.durable_execution_invocation_output,
1687
+ include=context.include_execution_data,
1688
+ )
1689
+ if context.durable_execution_invocation_output
1690
+ else None
1691
+ )
1692
+ return cls(
1693
+ event_type=EventType.EXECUTION_FAILED.value,
1694
+ event_timestamp=context.end_timestamp,
1695
+ sub_type=context.sub_type,
1696
+ event_id=context.event_id,
1697
+ operation_id=context.operation.operation_id,
1698
+ name=context.operation.name,
1699
+ parent_id=context.operation.parent_id,
1700
+ execution_failed_details=ExecutionFailedDetails(error=error),
1701
+ )
1702
+
1703
+ @classmethod
1704
+ def create_execution_event_timed_out(cls, context: EventCreationContext) -> Event:
1705
+ error: EventError | None = (
1706
+ EventError.from_durable_execution_invocation_output(
1707
+ context.durable_execution_invocation_output,
1708
+ include=context.include_execution_data,
1709
+ )
1710
+ if context.durable_execution_invocation_output
1711
+ else None
1712
+ )
1713
+ return cls(
1714
+ event_type=EventType.EXECUTION_TIMED_OUT.value,
1715
+ event_timestamp=context.end_timestamp,
1716
+ sub_type=context.sub_type,
1717
+ event_id=context.event_id,
1718
+ operation_id=context.operation.operation_id,
1719
+ name=context.operation.name,
1720
+ parent_id=context.operation.parent_id,
1721
+ execution_timed_out_details=ExecutionTimedOutDetails(error=error),
1722
+ )
1723
+
1724
+ @classmethod
1725
+ def create_execution_event_stopped(cls, context: EventCreationContext) -> Event:
1726
+ error: EventError | None = (
1727
+ EventError.from_durable_execution_invocation_output(
1728
+ context.durable_execution_invocation_output,
1729
+ include=context.include_execution_data,
1730
+ )
1731
+ if context.durable_execution_invocation_output
1732
+ else None
1733
+ )
1734
+ return cls(
1735
+ event_type=EventType.EXECUTION_STOPPED.value,
1736
+ event_timestamp=context.end_timestamp,
1737
+ sub_type=context.sub_type,
1738
+ event_id=context.event_id,
1739
+ operation_id=context.operation.operation_id,
1740
+ name=context.operation.name,
1741
+ parent_id=context.operation.parent_id,
1742
+ execution_stopped_details=ExecutionStoppedDetails(error=error),
1743
+ )
1744
+
1745
+ @classmethod
1746
+ def create_execution_event(cls, context: EventCreationContext) -> Event:
1747
+ """Create execution event based on action."""
1748
+ match context.operation.status:
1749
+ case OperationStatus.STARTED:
1750
+ return cls.create_execution_event_started(context)
1751
+ case OperationStatus.SUCCEEDED:
1752
+ return cls.create_execution_event_succeeded(context)
1753
+ case OperationStatus.FAILED:
1754
+ return cls.create_execution_event_failed(context)
1755
+ case OperationStatus.TIMED_OUT:
1756
+ return cls.create_execution_event_timed_out(context)
1757
+ case OperationStatus.STOPPED:
1758
+ return cls.create_execution_event_stopped(context)
1759
+ case _:
1760
+ msg = f"Operation status {context.operation.status} is not valid for execution operations. Valid statuses are: STARTED, SUCCEEDED, FAILED, TIMED_OUT, STOPPED"
1761
+ raise InvalidParameterValueException(msg)
1762
+
1763
+ # endregion execution
1764
+
1765
+ # region context
1766
+ @classmethod
1767
+ def create_context_event_started(cls, context: EventCreationContext) -> Event:
1768
+ return cls(
1769
+ event_type=EventType.CONTEXT_STARTED.value,
1770
+ event_timestamp=context.start_timestamp,
1771
+ sub_type=context.sub_type,
1772
+ event_id=context.event_id,
1773
+ operation_id=context.operation.operation_id,
1774
+ name=context.operation.name,
1775
+ parent_id=context.operation.parent_id,
1776
+ context_started_details=ContextStartedDetails(),
1777
+ )
1778
+
1779
+ @classmethod
1780
+ def create_context_event_succeeded(cls, context: EventCreationContext) -> Event:
1781
+ context_details: ContextDetails | None = context.operation.context_details
1782
+ event_result: EventResult | None = (
1783
+ EventResult.from_details(context_details, context.include_execution_data)
1784
+ if context_details
1785
+ else None
1786
+ )
1787
+ return cls(
1788
+ event_type=EventType.CONTEXT_SUCCEEDED.value,
1789
+ event_timestamp=context.end_timestamp,
1790
+ sub_type=context.sub_type,
1791
+ event_id=context.event_id,
1792
+ operation_id=context.operation.operation_id,
1793
+ name=context.operation.name,
1794
+ parent_id=context.operation.parent_id,
1795
+ context_succeeded_details=ContextSucceededDetails(result=event_result),
1796
+ )
1797
+
1798
+ @classmethod
1799
+ def create_context_event_failed(cls, context: EventCreationContext) -> Event:
1800
+ context_details: ContextDetails | None = context.operation.context_details
1801
+ event_error: EventError | None = (
1802
+ EventError.from_details(context_details) if context_details else None
1803
+ )
1804
+ return cls(
1805
+ event_type=EventType.CONTEXT_FAILED.value,
1806
+ event_timestamp=context.end_timestamp,
1807
+ sub_type=context.sub_type,
1808
+ event_id=context.event_id,
1809
+ operation_id=context.operation.operation_id,
1810
+ name=context.operation.name,
1811
+ parent_id=context.operation.parent_id,
1812
+ context_failed_details=ContextFailedDetails(error=event_error),
1813
+ )
1814
+
1815
+ @classmethod
1816
+ def create_context_event(cls, context: EventCreationContext) -> Event:
1817
+ """Create context event based on action."""
1818
+ match context.operation.status:
1819
+ case OperationStatus.STARTED:
1820
+ return cls.create_context_event_started(context)
1821
+ case OperationStatus.SUCCEEDED:
1822
+ return cls.create_context_event_succeeded(context)
1823
+ case OperationStatus.FAILED:
1824
+ return cls.create_context_event_failed(context)
1825
+ case _:
1826
+ msg = (
1827
+ f"Operation status {context.operation.status} is not valid for context operations. "
1828
+ f"Valid statuses are: STARTED, SUCCEEDED, FAILED"
1829
+ )
1830
+ raise InvalidParameterValueException(msg)
1831
+
1832
+ # endregion context
1833
+
1834
+ # region wait
1835
+ @classmethod
1836
+ def create_wait_event_started(cls, context: EventCreationContext) -> Event:
1837
+ wait_details: WaitDetails | None = context.operation.wait_details
1838
+ scheduled_end_timestamp: datetime.datetime | None = (
1839
+ wait_details.scheduled_end_timestamp if wait_details else None
1840
+ )
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
1850
+ - context.operation.start_timestamp
1851
+ ).total_seconds()
1852
+ )
1853
+ return cls(
1854
+ event_type=EventType.WAIT_STARTED.value,
1855
+ event_timestamp=context.start_timestamp,
1856
+ sub_type=context.sub_type,
1857
+ event_id=context.event_id,
1858
+ operation_id=context.operation.operation_id,
1859
+ name=context.operation.name,
1860
+ parent_id=context.operation.parent_id,
1861
+ wait_started_details=WaitStartedDetails(
1862
+ duration=duration,
1863
+ scheduled_end_timestamp=scheduled_end_timestamp,
1864
+ ),
1865
+ )
1866
+
1867
+ @classmethod
1868
+ def create_wait_event_succeeded(cls, context: EventCreationContext) -> Event:
1869
+ wait_details: WaitDetails | None = context.operation.wait_details
1870
+ duration: int | None = None
1871
+ if (
1872
+ wait_details
1873
+ and wait_details.scheduled_end_timestamp
1874
+ and context.operation.start_timestamp
1875
+ ):
1876
+ duration = round(
1877
+ (
1878
+ wait_details.scheduled_end_timestamp - context.start_timestamp
1879
+ ).total_seconds()
1880
+ )
1881
+ return cls(
1882
+ event_type=EventType.WAIT_SUCCEEDED.value,
1883
+ event_timestamp=context.end_timestamp,
1884
+ sub_type=context.sub_type,
1885
+ event_id=context.event_id,
1886
+ operation_id=context.operation.operation_id,
1887
+ name=context.operation.name,
1888
+ parent_id=context.operation.parent_id,
1889
+ wait_succeeded_details=WaitSucceededDetails(duration=duration),
1890
+ )
1891
+
1892
+ @classmethod
1893
+ def create_wait_event_cancelled(cls, context: EventCreationContext) -> Event:
1894
+ error: EventError | None = None
1895
+ if (
1896
+ context.operation_update
1897
+ and context.operation_update.operation_type == OperationType.WAIT
1898
+ and context.operation_update.action == OperationAction.CANCEL
1899
+ ):
1900
+ error = EventError(
1901
+ context.operation_update.error, not context.include_execution_data
1902
+ )
1903
+ return cls(
1904
+ event_type=EventType.WAIT_CANCELLED.value,
1905
+ event_timestamp=context.end_timestamp,
1906
+ sub_type=context.sub_type,
1907
+ event_id=context.event_id,
1908
+ operation_id=context.operation.operation_id,
1909
+ name=context.operation.name,
1910
+ parent_id=context.operation.parent_id,
1911
+ wait_cancelled_details=WaitCancelledDetails(error=error),
1912
+ )
1913
+
1914
+ @classmethod
1915
+ def create_wait_event(cls, context: EventCreationContext) -> Event:
1916
+ """Create wait event based on action."""
1917
+ match context.operation.status:
1918
+ case OperationStatus.STARTED:
1919
+ return cls.create_wait_event_started(context)
1920
+ case OperationStatus.SUCCEEDED:
1921
+ return cls.create_wait_event_succeeded(context)
1922
+ case OperationStatus.CANCELLED:
1923
+ return cls.create_wait_event_cancelled(context)
1924
+ case _:
1925
+ msg = (
1926
+ f"Operation status {context.operation.status} is not valid for wait operations. "
1927
+ f"Valid statuses are: STARTED, SUCCEEDED, CANCELLED"
1928
+ )
1929
+ raise InvalidParameterValueException(msg)
1930
+
1931
+ # endregion wait
1932
+
1933
+ # region step
1934
+ @classmethod
1935
+ def create_step_event_started(cls, context: EventCreationContext) -> Event:
1936
+ return cls(
1937
+ event_type=EventType.STEP_STARTED.value,
1938
+ event_timestamp=context.start_timestamp,
1939
+ sub_type=context.sub_type,
1940
+ event_id=context.event_id,
1941
+ operation_id=context.operation.operation_id,
1942
+ name=context.operation.name,
1943
+ parent_id=context.operation.parent_id,
1944
+ step_started_details=StepStartedDetails(),
1945
+ )
1946
+
1947
+ @classmethod
1948
+ def create_step_event_succeeded(cls, context: EventCreationContext) -> Event:
1949
+ step_details: StepDetails | None = context.operation.step_details
1950
+ event_result: EventResult | None = (
1951
+ EventResult.from_details(step_details, context.include_execution_data)
1952
+ if step_details
1953
+ else None
1954
+ )
1955
+ return cls(
1956
+ event_type=EventType.STEP_SUCCEEDED.value,
1957
+ event_timestamp=context.end_timestamp,
1958
+ sub_type=context.sub_type,
1959
+ event_id=context.event_id,
1960
+ operation_id=context.operation.operation_id,
1961
+ name=context.operation.name,
1962
+ parent_id=context.operation.parent_id,
1963
+ step_succeeded_details=StepSucceededDetails(
1964
+ result=event_result,
1965
+ retry_details=context.get_retry_details(),
1966
+ ),
1967
+ )
1968
+
1969
+ @classmethod
1970
+ def create_step_event_failed(cls, context: EventCreationContext) -> Event:
1971
+ step_details: StepDetails | None = context.operation.step_details
1972
+ event_error: EventError | None = (
1973
+ EventError.from_details(
1974
+ step_details, include=context.include_execution_data
1975
+ )
1976
+ if step_details
1977
+ else None
1978
+ )
1979
+ return cls(
1980
+ event_type=EventType.STEP_FAILED.value,
1981
+ event_timestamp=context.end_timestamp,
1982
+ sub_type=context.sub_type,
1983
+ event_id=context.event_id,
1984
+ operation_id=context.operation.operation_id,
1985
+ name=context.operation.name,
1986
+ parent_id=context.operation.parent_id,
1987
+ step_failed_details=StepFailedDetails(
1988
+ error=event_error,
1989
+ retry_details=context.get_retry_details(),
1990
+ ),
1991
+ )
1992
+
1993
+ @classmethod
1994
+ def create_step_event(cls, context: EventCreationContext) -> Event:
1995
+ """Create step event based on action."""
1996
+ match context.operation.status:
1997
+ case OperationStatus.STARTED:
1998
+ return cls.create_step_event_started(context)
1999
+ case OperationStatus.SUCCEEDED:
2000
+ return cls.create_step_event_succeeded(context)
2001
+ case OperationStatus.FAILED:
2002
+ return cls.create_step_event_failed(context)
2003
+ case _:
2004
+ msg = (
2005
+ f"Operation status {context.operation.status} is not valid for step operations. "
2006
+ f"Valid statuses are: STARTED, SUCCEEDED, FAILED"
2007
+ )
2008
+ raise InvalidParameterValueException(msg)
2009
+
2010
+ # endregion step
2011
+
2012
+ # region chained_invoke
2013
+ @classmethod
2014
+ def create_chained_invoke_event_pending(
2015
+ cls, context: EventCreationContext
2016
+ ) -> Event:
2017
+ input: EventInput = EventInput.from_start_durable_execution_input(
2018
+ context.start_durable_execution_input, context.include_execution_data
2019
+ )
2020
+ return cls(
2021
+ event_type=EventType.CHAINED_INVOKE_STARTED.value,
2022
+ event_timestamp=context.start_timestamp,
2023
+ sub_type=context.sub_type,
2024
+ event_id=context.event_id,
2025
+ operation_id=context.operation.operation_id,
2026
+ name=context.operation.name,
2027
+ parent_id=context.operation.parent_id,
2028
+ chained_invoke_pending_details=ChainedInvokePendingDetails(
2029
+ input=input,
2030
+ function_name=context.start_durable_execution_input.function_name,
2031
+ ),
2032
+ )
2033
+
2034
+ @classmethod
2035
+ def create_chained_invoke_event_started(
2036
+ cls, context: EventCreationContext
2037
+ ) -> Event:
2038
+ return cls(
2039
+ event_type=EventType.CHAINED_INVOKE_STARTED.value,
2040
+ event_timestamp=context.start_timestamp,
2041
+ sub_type=context.sub_type,
2042
+ event_id=context.event_id,
2043
+ operation_id=context.operation.operation_id,
2044
+ name=context.operation.name,
2045
+ parent_id=context.operation.parent_id,
2046
+ chained_invoke_started_details=ChainedInvokeStartedDetails(
2047
+ durable_execution_arn=context.durable_execution_arn
2048
+ ),
2049
+ )
2050
+
2051
+ @classmethod
2052
+ def create_chained_invoke_event_succeeded(
2053
+ cls, context: EventCreationContext
2054
+ ) -> Event:
2055
+ chained_invoke_details: ChainedInvokeDetails | None = (
2056
+ context.operation.chained_invoke_details
2057
+ )
2058
+ event_result: EventResult | None = (
2059
+ EventResult.from_details(
2060
+ chained_invoke_details, context.include_execution_data
2061
+ )
2062
+ if chained_invoke_details
2063
+ else None
2064
+ )
2065
+ return cls(
2066
+ event_type=EventType.CHAINED_INVOKE_SUCCEEDED.value,
2067
+ event_timestamp=context.end_timestamp,
2068
+ sub_type=context.sub_type,
2069
+ event_id=context.event_id,
2070
+ operation_id=context.operation.operation_id,
2071
+ name=context.operation.name,
2072
+ parent_id=context.operation.parent_id,
2073
+ chained_invoke_succeeded_details=ChainedInvokeSucceededDetails(
2074
+ result=event_result
2075
+ ),
2076
+ )
2077
+
2078
+ @classmethod
2079
+ def create_chained_invoke_event_failed(cls, context: EventCreationContext) -> Event:
2080
+ chained_invoke_details: ChainedInvokeDetails | None = (
2081
+ context.operation.chained_invoke_details
2082
+ )
2083
+ event_error: EventError | None = (
2084
+ EventError.from_details(
2085
+ chained_invoke_details, include=context.include_execution_data
2086
+ )
2087
+ if chained_invoke_details
2088
+ else None
2089
+ )
2090
+ return cls(
2091
+ event_type=EventType.CHAINED_INVOKE_FAILED.value,
2092
+ event_timestamp=context.end_timestamp,
2093
+ sub_type=context.sub_type,
2094
+ event_id=context.event_id,
2095
+ operation_id=context.operation.operation_id,
2096
+ name=context.operation.name,
2097
+ parent_id=context.operation.parent_id,
2098
+ chained_invoke_failed_details=ChainedInvokeFailedDetails(error=event_error),
2099
+ )
2100
+
2101
+ @classmethod
2102
+ def create_chained_invoke_event_timed_out(
2103
+ cls, context: EventCreationContext
2104
+ ) -> Event:
2105
+ chained_invoke_details: ChainedInvokeDetails | None = (
2106
+ context.operation.chained_invoke_details
2107
+ )
2108
+ event_error: EventError | None = (
2109
+ EventError.from_details(
2110
+ chained_invoke_details, include=context.include_execution_data
2111
+ )
2112
+ if chained_invoke_details
2113
+ else None
2114
+ )
2115
+ return cls(
2116
+ event_type=EventType.CHAINED_INVOKE_TIMED_OUT.value,
2117
+ event_timestamp=context.end_timestamp,
2118
+ sub_type=context.sub_type,
2119
+ event_id=context.event_id,
2120
+ operation_id=context.operation.operation_id,
2121
+ name=context.operation.name,
2122
+ parent_id=context.operation.parent_id,
2123
+ chained_invoke_timed_out_details=ChainedInvokeTimedOutDetails(
2124
+ error=event_error
2125
+ ),
2126
+ )
2127
+
2128
+ @classmethod
2129
+ def create_chained_invoke_event_stopped(
2130
+ cls, context: EventCreationContext
2131
+ ) -> Event:
2132
+ chained_invoke_details: ChainedInvokeDetails | None = (
2133
+ context.operation.chained_invoke_details
2134
+ )
2135
+ event_error: EventError | None = (
2136
+ EventError.from_details(
2137
+ chained_invoke_details, include=context.include_execution_data
2138
+ )
2139
+ if chained_invoke_details
2140
+ else None
2141
+ )
2142
+ return cls(
2143
+ event_type=EventType.CHAINED_INVOKE_STOPPED.value,
2144
+ event_timestamp=context.end_timestamp,
2145
+ sub_type=context.sub_type,
2146
+ event_id=context.event_id,
2147
+ operation_id=context.operation.operation_id,
2148
+ name=context.operation.name,
2149
+ parent_id=context.operation.parent_id,
2150
+ chained_invoke_stopped_details=ChainedInvokeStoppedDetails(
2151
+ error=event_error
2152
+ ),
2153
+ )
2154
+
2155
+ @classmethod
2156
+ def create_chained_invoke_event(cls, context: EventCreationContext) -> Event:
2157
+ """Create chained invoke event based on action."""
2158
+ match context.operation.status:
2159
+ case OperationStatus.PENDING:
2160
+ return cls.create_chained_invoke_event_pending(context)
2161
+ case OperationStatus.STARTED:
2162
+ return cls.create_chained_invoke_event_started(context)
2163
+ case OperationStatus.SUCCEEDED:
2164
+ return cls.create_chained_invoke_event_succeeded(context)
2165
+ case OperationStatus.FAILED:
2166
+ return cls.create_chained_invoke_event_failed(context)
2167
+ case OperationStatus.TIMED_OUT:
2168
+ return cls.create_chained_invoke_event_timed_out(context)
2169
+ case OperationStatus.STOPPED:
2170
+ return cls.create_chained_invoke_event_stopped(context)
2171
+ case _:
2172
+ msg = (
2173
+ f"Operation status {context.operation.status} is not valid for chained invoke operations. Valid statuses are: "
2174
+ f"STARTED, SUCCEEDED, FAILED, TIMED_OUT, STOPPED"
2175
+ )
2176
+ raise InvalidParameterValueException(msg)
2177
+
2178
+ # endregion chained_invoke
2179
+
2180
+ # region callback
2181
+ @classmethod
2182
+ def create_callback_event_started(cls, context: EventCreationContext) -> Event:
2183
+ callback_details: CallbackDetails | None = context.operation.callback_details
2184
+ callback_id: str | None = (
2185
+ callback_details.callback_id if callback_details else None
2186
+ )
2187
+ callback_options: CallbackOptions | None = (
2188
+ context.operation_update.callback_options
2189
+ if context.operation_update
2190
+ else None
2191
+ )
2192
+ timeout: int | None = (
2193
+ callback_options.timeout_seconds if callback_options else None
2194
+ )
2195
+ heartbeat_timeout: int | None = (
2196
+ callback_options.heartbeat_timeout_seconds if callback_options else None
2197
+ )
2198
+ return cls(
2199
+ event_type=EventType.CALLBACK_STARTED.value,
2200
+ event_timestamp=context.start_timestamp,
2201
+ sub_type=context.sub_type,
2202
+ event_id=context.event_id,
2203
+ operation_id=context.operation.operation_id,
2204
+ name=context.operation.name,
2205
+ parent_id=context.operation.parent_id,
2206
+ callback_started_details=CallbackStartedDetails(
2207
+ callback_id=callback_id,
2208
+ timeout=timeout,
2209
+ heartbeat_timeout=heartbeat_timeout,
2210
+ ),
2211
+ )
2212
+
2213
+ @classmethod
2214
+ def create_callback_event_succeeded(cls, context: EventCreationContext) -> Event:
2215
+ callback_details: CallbackDetails | None = context.operation.callback_details
2216
+ event_result: EventResult | None = (
2217
+ EventResult.from_details(callback_details, context.include_execution_data)
2218
+ if callback_details
2219
+ else None
2220
+ )
2221
+ return cls(
2222
+ event_type=EventType.CALLBACK_SUCCEEDED.value,
2223
+ event_timestamp=context.end_timestamp,
2224
+ sub_type=context.sub_type,
2225
+ event_id=context.event_id,
2226
+ operation_id=context.operation.operation_id,
2227
+ name=context.operation.name,
2228
+ parent_id=context.operation.parent_id,
2229
+ callback_succeeded_details=CallbackSucceededDetails(result=event_result),
2230
+ )
2231
+
2232
+ @classmethod
2233
+ def create_callback_event_failed(cls, context: EventCreationContext) -> Event:
2234
+ callback_details: CallbackDetails | None = context.operation.callback_details
2235
+ event_error: EventError | None = (
2236
+ EventError.from_details(callback_details) if callback_details else None
2237
+ )
2238
+ return cls(
2239
+ event_type=EventType.CALLBACK_FAILED.value,
2240
+ event_timestamp=context.end_timestamp,
2241
+ sub_type=context.sub_type,
2242
+ event_id=context.event_id,
2243
+ operation_id=context.operation.operation_id,
2244
+ name=context.operation.name,
2245
+ parent_id=context.operation.parent_id,
2246
+ callback_failed_details=CallbackFailedDetails(error=event_error),
2247
+ )
2248
+
2249
+ @classmethod
2250
+ def create_callback_event_timed_out(cls, context: EventCreationContext) -> Event:
2251
+ callback_details: CallbackDetails | None = context.operation.callback_details
2252
+ event_error: EventError | None = (
2253
+ EventError.from_details(callback_details) if callback_details else None
2254
+ )
2255
+ return cls(
2256
+ event_type=EventType.CALLBACK_TIMED_OUT.value,
2257
+ event_timestamp=context.end_timestamp,
2258
+ sub_type=context.sub_type,
2259
+ event_id=context.event_id,
2260
+ operation_id=context.operation.operation_id,
2261
+ name=context.operation.name,
2262
+ parent_id=context.operation.parent_id,
2263
+ callback_timed_out_details=CallbackTimedOutDetails(error=event_error),
2264
+ )
2265
+
2266
+ @classmethod
2267
+ def create_callback_event(cls, context: EventCreationContext) -> Event:
2268
+ """Create callback event based on action."""
2269
+ match context.operation.status:
2270
+ case OperationStatus.STARTED:
2271
+ return cls.create_callback_event_started(context)
2272
+ case OperationStatus.SUCCEEDED:
2273
+ return cls.create_callback_event_succeeded(context)
2274
+ case OperationStatus.FAILED:
2275
+ return cls.create_callback_event_failed(context)
2276
+ case OperationStatus.TIMED_OUT:
2277
+ return cls.create_callback_event_timed_out(context)
2278
+ case _:
2279
+ msg = (
2280
+ f"Operation status {context.operation.status} is not valid for callback operations. "
2281
+ f"Valid statuses are: STARTED, SUCCEEDED, FAILED, TIMED_OUT"
2282
+ )
2283
+ raise InvalidParameterValueException(msg)
2284
+
2285
+ # endregion callback
2286
+
2287
+ # region invocation_completed
2288
+ @classmethod
2289
+ def create_invocation_completed(
2290
+ cls,
2291
+ event_id: int,
2292
+ event_timestamp: datetime.datetime,
2293
+ start_timestamp: datetime.datetime,
2294
+ end_timestamp: datetime.datetime,
2295
+ request_id: str,
2296
+ ) -> Event:
2297
+ """Create invocation completed event."""
2298
+ return cls(
2299
+ event_type=EventType.INVOCATION_COMPLETED.value,
2300
+ event_timestamp=event_timestamp,
2301
+ event_id=event_id,
2302
+ invocation_completed_details=InvocationCompletedDetails(
2303
+ start_timestamp=start_timestamp,
2304
+ end_timestamp=end_timestamp,
2305
+ request_id=request_id,
2306
+ ),
2307
+ )
2308
+
2309
+ # endregion invocation_completed
2310
+
2311
+ @classmethod
2312
+ def create_event_started(cls, context: EventCreationContext) -> Event:
2313
+ """Convert operation to started event."""
2314
+ if context.operation.start_timestamp is None:
2315
+ msg: str = "Operation start timestamp cannot be None when converting to started event"
2316
+ raise InvalidParameterValueException(msg)
2317
+
2318
+ match context.operation.operation_type:
2319
+ case OperationType.EXECUTION:
2320
+ return cls.create_execution_event_started(context)
2321
+ case OperationType.CONTEXT:
2322
+ return cls.create_context_event_started(context)
2323
+ case OperationType.WAIT:
2324
+ return cls.create_wait_event_started(context)
2325
+ case OperationType.STEP:
2326
+ return cls.create_step_event_started(context)
2327
+ case OperationType.CHAINED_INVOKE:
2328
+ return cls.create_chained_invoke_event_started(context)
2329
+ case OperationType.CALLBACK:
2330
+ return cls.create_callback_event_started(context)
2331
+ case _:
2332
+ msg = f"Unknown operation type: {context.operation.operation_type}"
2333
+ raise InvalidParameterValueException(msg)
2334
+
2335
+ @classmethod
2336
+ def from_event_with_id(cls, event: Event, event_id: int) -> Event:
2337
+ """Create a new Event from an existing event with updated event_id."""
2338
+ return cls(
2339
+ event_type=event.event_type,
2340
+ event_timestamp=event.event_timestamp,
2341
+ sub_type=event.sub_type,
2342
+ event_id=event_id,
2343
+ operation_id=event.operation_id,
2344
+ name=event.name,
2345
+ parent_id=event.parent_id,
2346
+ execution_started_details=event.execution_started_details,
2347
+ execution_succeeded_details=event.execution_succeeded_details,
2348
+ execution_failed_details=event.execution_failed_details,
2349
+ execution_timed_out_details=event.execution_timed_out_details,
2350
+ execution_stopped_details=event.execution_stopped_details,
2351
+ context_started_details=event.context_started_details,
2352
+ context_succeeded_details=event.context_succeeded_details,
2353
+ context_failed_details=event.context_failed_details,
2354
+ wait_started_details=event.wait_started_details,
2355
+ wait_succeeded_details=event.wait_succeeded_details,
2356
+ wait_cancelled_details=event.wait_cancelled_details,
2357
+ step_started_details=event.step_started_details,
2358
+ step_succeeded_details=event.step_succeeded_details,
2359
+ step_failed_details=event.step_failed_details,
2360
+ chained_invoke_pending_details=event.chained_invoke_pending_details,
2361
+ chained_invoke_started_details=event.chained_invoke_started_details,
2362
+ chained_invoke_succeeded_details=event.chained_invoke_succeeded_details,
2363
+ chained_invoke_failed_details=event.chained_invoke_failed_details,
2364
+ chained_invoke_timed_out_details=event.chained_invoke_timed_out_details,
2365
+ chained_invoke_stopped_details=event.chained_invoke_stopped_details,
2366
+ callback_started_details=event.callback_started_details,
2367
+ callback_succeeded_details=event.callback_succeeded_details,
2368
+ callback_failed_details=event.callback_failed_details,
2369
+ callback_timed_out_details=event.callback_timed_out_details,
2370
+ )
2371
+
2372
+ @classmethod
2373
+ def create_event_terminated(cls, context: EventCreationContext) -> Event:
2374
+ """Convert operation to finished event."""
2375
+ operation: Operation = context.operation
2376
+ if operation.end_timestamp is None:
2377
+ msg: str = "Operation end timestamp cannot be None when converting to finished event"
2378
+ raise InvalidParameterValueException(msg)
2379
+
2380
+ if operation.status not in TERMINAL_STATUSES:
2381
+ msg = f"Operation status must be one of SUCCEEDED, FAILED, TIMED_OUT, STOPPED, or CANCELLED. Got: {operation.status}"
2382
+ raise InvalidParameterValueException(msg)
2383
+
2384
+ match operation.operation_type:
2385
+ case OperationType.EXECUTION:
2386
+ return cls.create_execution_event(context)
2387
+ case OperationType.CONTEXT:
2388
+ return cls.create_context_event(context)
2389
+ case OperationType.WAIT:
2390
+ return cls.create_wait_event(context)
2391
+ case OperationType.STEP:
2392
+ return cls.create_step_event(context)
2393
+ case OperationType.CHAINED_INVOKE:
2394
+ return cls.create_chained_invoke_event(context)
2395
+ case OperationType.CALLBACK:
2396
+ return cls.create_callback_event(context)
2397
+ case _:
2398
+ msg = f"Unknown operation type: {operation.operation_type}"
2399
+ raise InvalidParameterValueException(msg)
2400
+
2401
+
2402
+ # endregion event_class
2403
+
2404
+
2405
+ # region history_models
2406
+ @dataclass(frozen=True)
2407
+ class HistoryEventTypeConfig:
2408
+ """Configuration for how to process a specific event type."""
2409
+
2410
+ operation_type: OperationType | None
2411
+ operation_status: OperationStatus | None
2412
+ is_start_event: bool
2413
+ is_end_event: bool
2414
+ has_result: bool # Whether this event type contains result/error data
2415
+
2416
+
2417
+ # Mapping of event types to their processing configuration
2418
+ # This matches the TypeScript historyEventTypes constant
2419
+ HISTORY_EVENT_TYPES: dict[str, HistoryEventTypeConfig] = {
2420
+ "ExecutionStarted": HistoryEventTypeConfig(
2421
+ operation_type=OperationType.EXECUTION,
2422
+ operation_status=OperationStatus.STARTED,
2423
+ is_start_event=True,
2424
+ is_end_event=False,
2425
+ has_result=False,
2426
+ ),
2427
+ "ExecutionFailed": HistoryEventTypeConfig(
2428
+ operation_type=OperationType.EXECUTION,
2429
+ operation_status=OperationStatus.FAILED,
2430
+ is_start_event=False,
2431
+ is_end_event=True,
2432
+ has_result=False,
2433
+ ),
2434
+ "ExecutionStopped": HistoryEventTypeConfig(
2435
+ operation_type=OperationType.EXECUTION,
2436
+ operation_status=OperationStatus.STOPPED,
2437
+ is_start_event=False,
2438
+ is_end_event=True,
2439
+ has_result=False,
2440
+ ),
2441
+ "ExecutionSucceeded": HistoryEventTypeConfig(
2442
+ operation_type=OperationType.EXECUTION,
2443
+ operation_status=OperationStatus.SUCCEEDED,
2444
+ is_start_event=False,
2445
+ is_end_event=True,
2446
+ has_result=False,
2447
+ ),
2448
+ "ExecutionTimedOut": HistoryEventTypeConfig(
2449
+ operation_type=OperationType.EXECUTION,
2450
+ operation_status=OperationStatus.TIMED_OUT,
2451
+ is_start_event=False,
2452
+ is_end_event=True,
2453
+ has_result=False,
2454
+ ),
2455
+ "CallbackStarted": HistoryEventTypeConfig(
2456
+ operation_type=OperationType.CALLBACK,
2457
+ operation_status=OperationStatus.STARTED,
2458
+ is_start_event=True,
2459
+ is_end_event=False,
2460
+ has_result=False,
2461
+ ),
2462
+ "CallbackFailed": HistoryEventTypeConfig(
2463
+ operation_type=OperationType.CALLBACK,
2464
+ operation_status=OperationStatus.FAILED,
2465
+ is_start_event=False,
2466
+ is_end_event=True,
2467
+ has_result=True,
2468
+ ),
2469
+ "CallbackSucceeded": HistoryEventTypeConfig(
2470
+ operation_type=OperationType.CALLBACK,
2471
+ operation_status=OperationStatus.SUCCEEDED,
2472
+ is_start_event=False,
2473
+ is_end_event=True,
2474
+ has_result=True,
2475
+ ),
2476
+ "CallbackTimedOut": HistoryEventTypeConfig(
2477
+ operation_type=OperationType.CALLBACK,
2478
+ operation_status=OperationStatus.TIMED_OUT,
2479
+ is_start_event=False,
2480
+ is_end_event=True,
2481
+ has_result=True,
2482
+ ),
2483
+ "ContextStarted": HistoryEventTypeConfig(
2484
+ operation_type=OperationType.CONTEXT,
2485
+ operation_status=OperationStatus.STARTED,
2486
+ is_start_event=True,
2487
+ is_end_event=False,
2488
+ has_result=False,
2489
+ ),
2490
+ "ContextFailed": HistoryEventTypeConfig(
2491
+ operation_type=OperationType.CONTEXT,
2492
+ operation_status=OperationStatus.FAILED,
2493
+ is_start_event=False,
2494
+ is_end_event=True,
2495
+ has_result=True,
2496
+ ),
2497
+ "ContextSucceeded": HistoryEventTypeConfig(
2498
+ operation_type=OperationType.CONTEXT,
2499
+ operation_status=OperationStatus.SUCCEEDED,
2500
+ is_start_event=False,
2501
+ is_end_event=True,
2502
+ has_result=True,
2503
+ ),
2504
+ "ChainedInvokeStarted": HistoryEventTypeConfig(
2505
+ operation_type=OperationType.CHAINED_INVOKE,
2506
+ operation_status=OperationStatus.STARTED,
2507
+ is_start_event=True,
2508
+ is_end_event=False,
2509
+ has_result=False,
2510
+ ),
2511
+ "ChainedInvokeFailed": HistoryEventTypeConfig(
2512
+ operation_type=OperationType.CHAINED_INVOKE,
2513
+ operation_status=OperationStatus.FAILED,
2514
+ is_start_event=False,
2515
+ is_end_event=True,
2516
+ has_result=True,
2517
+ ),
2518
+ "ChainedInvokeSucceeded": HistoryEventTypeConfig(
2519
+ operation_type=OperationType.CHAINED_INVOKE,
2520
+ operation_status=OperationStatus.SUCCEEDED,
2521
+ is_start_event=False,
2522
+ is_end_event=True,
2523
+ has_result=True,
2524
+ ),
2525
+ "ChainedInvokeTimedOut": HistoryEventTypeConfig(
2526
+ operation_type=OperationType.CHAINED_INVOKE,
2527
+ operation_status=OperationStatus.TIMED_OUT,
2528
+ is_start_event=False,
2529
+ is_end_event=True,
2530
+ has_result=True,
2531
+ ),
2532
+ "ChainedInvokeCancelled": HistoryEventTypeConfig(
2533
+ operation_type=OperationType.CHAINED_INVOKE,
2534
+ operation_status=OperationStatus.CANCELLED,
2535
+ is_start_event=False,
2536
+ is_end_event=True,
2537
+ has_result=True,
2538
+ ),
2539
+ "StepStarted": HistoryEventTypeConfig(
2540
+ operation_type=OperationType.STEP,
2541
+ operation_status=OperationStatus.STARTED,
2542
+ is_start_event=True,
2543
+ is_end_event=False,
2544
+ has_result=False,
2545
+ ),
2546
+ "StepFailed": HistoryEventTypeConfig(
2547
+ operation_type=OperationType.STEP,
2548
+ operation_status=OperationStatus.FAILED,
2549
+ is_start_event=False,
2550
+ is_end_event=True,
2551
+ has_result=True,
2552
+ ),
2553
+ "StepSucceeded": HistoryEventTypeConfig(
2554
+ operation_type=OperationType.STEP,
2555
+ operation_status=OperationStatus.SUCCEEDED,
2556
+ is_start_event=False,
2557
+ is_end_event=True,
2558
+ has_result=True,
2559
+ ),
2560
+ "WaitStarted": HistoryEventTypeConfig(
2561
+ operation_type=OperationType.WAIT,
2562
+ operation_status=OperationStatus.STARTED,
2563
+ is_start_event=True,
2564
+ is_end_event=False,
2565
+ has_result=True,
2566
+ ),
2567
+ "WaitSucceeded": HistoryEventTypeConfig(
2568
+ operation_type=OperationType.WAIT,
2569
+ operation_status=OperationStatus.SUCCEEDED,
2570
+ is_start_event=False,
2571
+ is_end_event=True,
2572
+ has_result=True,
2573
+ ),
2574
+ "WaitCancelled": HistoryEventTypeConfig(
2575
+ operation_type=OperationType.WAIT,
2576
+ operation_status=OperationStatus.CANCELLED,
2577
+ is_start_event=False,
2578
+ is_end_event=True,
2579
+ has_result=True,
2580
+ ),
2581
+ # TODO: add support for populating invocation information from InvocationCompleted event
2582
+ "InvocationCompleted": HistoryEventTypeConfig(
2583
+ operation_type=None,
2584
+ operation_status=None,
2585
+ is_start_event=False,
2586
+ is_end_event=False,
2587
+ has_result=True,
2588
+ ),
2589
+ }
2590
+
2591
+
2592
+ def events_to_operations(events: list[Event]) -> list[Operation]:
2593
+ """Convert a list of history events into operations.
2594
+
2595
+ This function processes raw history events and groups them by operation ID,
2596
+ creating comprehensive operation objects following the TypeScript pattern from
2597
+ aws-durable-execution-sdk-js-testing.
2598
+
2599
+ Multiple events for the same operation_id are merged together, with each event
2600
+ contributing its specific fields (e.g., CallbackStarted provides callback_id,
2601
+ CallbackSucceeded provides result).
2602
+
2603
+ Args:
2604
+ events: List of history events to process
2605
+
2606
+ Returns:
2607
+ List of operations, one per unique operation ID
2608
+
2609
+ Raises:
2610
+ InvalidParameterValueException: When required fields are missing from an event
2611
+
2612
+ Note:
2613
+ InvocationCompleted events are currently skipped as they don't represent
2614
+ operations. Future enhancement: populate invocation information from these
2615
+ events (TODO).
2616
+ """
2617
+ operations_map: dict[str, Operation] = {}
2618
+
2619
+ for event in events:
2620
+ if not event.event_type:
2621
+ msg = "Missing required 'event_type' field in event"
2622
+ raise InvalidParameterValueException(msg)
2623
+
2624
+ # Get event type configuration
2625
+ event_config: HistoryEventTypeConfig | None = HISTORY_EVENT_TYPES.get(
2626
+ event.event_type
2627
+ )
2628
+ if not event_config:
2629
+ msg = f"Unknown event type: {event.event_type}"
2630
+ raise InvalidParameterValueException(msg)
2631
+
2632
+ # TODO: add support for populating invocation information from InvocationCompleted event
2633
+ if event.event_type == "InvocationCompleted":
2634
+ continue
2635
+
2636
+ if not event.operation_id:
2637
+ msg = f"Missing required 'operation_id' field in event {event.event_id}"
2638
+ raise InvalidParameterValueException(msg)
2639
+
2640
+ # Get previous operation if it exists
2641
+ previous_operation: Operation | None = operations_map.get(event.operation_id)
2642
+
2643
+ # Get operation type and status from configuration
2644
+ operation_type: OperationType = (
2645
+ event_config.operation_type or OperationType.EXECUTION
2646
+ )
2647
+ status: OperationStatus = (
2648
+ event_config.operation_status or OperationStatus.PENDING
2649
+ )
2650
+
2651
+ # Parse sub_type
2652
+ sub_type: OperationSubType | None = None
2653
+ if event.sub_type:
2654
+ try:
2655
+ sub_type = OperationSubType(event.sub_type)
2656
+ except ValueError as e:
2657
+ raise InvalidParameterValueException(str(e)) from e
2658
+
2659
+ # Create base operation
2660
+ operation = Operation(
2661
+ operation_id=event.operation_id,
2662
+ operation_type=operation_type,
2663
+ status=status,
2664
+ name=event.name,
2665
+ parent_id=event.parent_id,
2666
+ sub_type=sub_type,
2667
+ start_timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
2668
+ )
2669
+
2670
+ # Merge with previous operation if it exists
2671
+ # Most fields are immutable, so they get preserved from previous events
2672
+ if previous_operation:
2673
+ operation = replace(
2674
+ operation,
2675
+ name=operation.name or previous_operation.name,
2676
+ parent_id=operation.parent_id or previous_operation.parent_id,
2677
+ sub_type=operation.sub_type or previous_operation.sub_type,
2678
+ start_timestamp=previous_operation.start_timestamp,
2679
+ end_timestamp=previous_operation.end_timestamp,
2680
+ execution_details=previous_operation.execution_details,
2681
+ context_details=previous_operation.context_details,
2682
+ step_details=previous_operation.step_details,
2683
+ wait_details=previous_operation.wait_details,
2684
+ callback_details=previous_operation.callback_details,
2685
+ chained_invoke_details=previous_operation.chained_invoke_details,
2686
+ )
2687
+
2688
+ # Set timestamps based on event configuration
2689
+ if event_config.is_start_event:
2690
+ operation = replace(operation, start_timestamp=event.event_timestamp)
2691
+ if event_config.is_end_event:
2692
+ operation = replace(operation, end_timestamp=event.event_timestamp)
2693
+
2694
+ # Add operation-specific details incrementally
2695
+ # Each event type contributes only the fields it has
2696
+
2697
+ # EXECUTION details
2698
+ if (
2699
+ operation_type == OperationType.EXECUTION
2700
+ and event.execution_started_details
2701
+ and event.execution_started_details.input
2702
+ ):
2703
+ operation = replace(
2704
+ operation,
2705
+ execution_details=ExecutionDetails(
2706
+ input_payload=event.execution_started_details.input.payload
2707
+ ),
2708
+ )
2709
+
2710
+ # CALLBACK details - merge callback_id, result, and error from different events
2711
+ if operation_type == OperationType.CALLBACK:
2712
+ existing_cb: CallbackDetails | None = operation.callback_details
2713
+ callback_id: str = existing_cb.callback_id if existing_cb else ""
2714
+ result: str | None = existing_cb.result if existing_cb else None
2715
+ error: ErrorObject | None = existing_cb.error if existing_cb else None
2716
+
2717
+ # CallbackStarted provides callback_id
2718
+ if event.callback_started_details:
2719
+ callback_id = event.callback_started_details.callback_id or callback_id
2720
+
2721
+ # CallbackSucceeded provides result
2722
+ if (
2723
+ event.callback_succeeded_details
2724
+ and event.callback_succeeded_details.result
2725
+ ):
2726
+ result = event.callback_succeeded_details.result.payload
2727
+
2728
+ # CallbackFailed provides error
2729
+ if event.callback_failed_details and event.callback_failed_details.error:
2730
+ error = event.callback_failed_details.error.payload
2731
+
2732
+ # CallbackTimedOut provides error
2733
+ if (
2734
+ event.callback_timed_out_details
2735
+ and event.callback_timed_out_details.error
2736
+ ):
2737
+ error = event.callback_timed_out_details.error.payload
2738
+
2739
+ operation = replace(
2740
+ operation,
2741
+ callback_details=CallbackDetails(
2742
+ callback_id=callback_id,
2743
+ result=result,
2744
+ error=error,
2745
+ ),
2746
+ )
2747
+
2748
+ # STEP details - only update if this event type has result data
2749
+ if operation_type == OperationType.STEP and event_config.has_result:
2750
+ existing_step: StepDetails | None = operation.step_details
2751
+ result_val: str | None = existing_step.result if existing_step else None
2752
+ error_val: ErrorObject | None = (
2753
+ existing_step.error if existing_step else None
2754
+ )
2755
+ attempt: int = existing_step.attempt if existing_step else 0
2756
+ next_attempt_ts: datetime.datetime | None = (
2757
+ existing_step.next_attempt_timestamp if existing_step else None
2758
+ )
2759
+
2760
+ # StepSucceeded provides result
2761
+ if event.step_succeeded_details:
2762
+ if event.step_succeeded_details.result:
2763
+ result_val = event.step_succeeded_details.result.payload
2764
+ if event.step_succeeded_details.retry_details:
2765
+ attempt = event.step_succeeded_details.retry_details.current_attempt
2766
+
2767
+ # StepFailed provides error and retry details
2768
+ if event.step_failed_details:
2769
+ if event.step_failed_details.error:
2770
+ error_val = event.step_failed_details.error.payload
2771
+ if event.step_failed_details.retry_details:
2772
+ attempt = event.step_failed_details.retry_details.current_attempt
2773
+ if (
2774
+ event.step_failed_details.retry_details.next_attempt_delay_seconds
2775
+ is not None
2776
+ ):
2777
+ next_attempt_ts = event.event_timestamp + datetime.timedelta(
2778
+ seconds=event.step_failed_details.retry_details.next_attempt_delay_seconds
2779
+ )
2780
+
2781
+ operation = replace(
2782
+ operation,
2783
+ step_details=StepDetails(
2784
+ result=result_val,
2785
+ error=error_val,
2786
+ attempt=attempt,
2787
+ next_attempt_timestamp=next_attempt_ts,
2788
+ ),
2789
+ )
2790
+
2791
+ # WAIT details
2792
+ if operation_type == OperationType.WAIT and event.wait_started_details:
2793
+ operation = replace(
2794
+ operation,
2795
+ wait_details=WaitDetails(
2796
+ scheduled_end_timestamp=event.wait_started_details.scheduled_end_timestamp
2797
+ ),
2798
+ )
2799
+
2800
+ # CONTEXT details - only update if this event type has result data (matching TypeScript hasResult)
2801
+ if operation_type == OperationType.CONTEXT and event_config.has_result:
2802
+ if (
2803
+ event.context_succeeded_details
2804
+ and event.context_succeeded_details.result
2805
+ ):
2806
+ operation = replace(
2807
+ operation,
2808
+ context_details=ContextDetails(
2809
+ result=event.context_succeeded_details.result.payload,
2810
+ error=None,
2811
+ ),
2812
+ )
2813
+ elif event.context_failed_details and event.context_failed_details.error:
2814
+ operation = replace(
2815
+ operation,
2816
+ context_details=ContextDetails(
2817
+ result=None,
2818
+ error=event.context_failed_details.error.payload,
2819
+ ),
2820
+ )
2821
+
2822
+ # CHAINED_INVOKE details - only update if this event type has result data (matching TypeScript hasResult)
2823
+ if operation_type == OperationType.CHAINED_INVOKE and event_config.has_result:
2824
+ if (
2825
+ event.chained_invoke_succeeded_details
2826
+ and event.chained_invoke_succeeded_details.result
2827
+ ):
2828
+ operation = replace(
2829
+ operation,
2830
+ chained_invoke_details=ChainedInvokeDetails(
2831
+ result=event.chained_invoke_succeeded_details.result.payload,
2832
+ error=None,
2833
+ ),
2834
+ )
2835
+ elif (
2836
+ event.chained_invoke_failed_details
2837
+ and event.chained_invoke_failed_details.error
2838
+ ):
2839
+ operation = replace(
2840
+ operation,
2841
+ chained_invoke_details=ChainedInvokeDetails(
2842
+ result=None,
2843
+ error=event.chained_invoke_failed_details.error.payload,
2844
+ ),
2845
+ )
2846
+
2847
+ # Store in map
2848
+ operations_map[event.operation_id] = operation
2849
+
2850
+ return list(operations_map.values())
2851
+
2852
+
2853
+ @dataclass(frozen=True)
2854
+ class GetDurableExecutionHistoryRequest:
2855
+ """Request to get durable execution history."""
2856
+
2857
+ durable_execution_arn: str
2858
+ include_execution_data: bool | None = None
2859
+ reverse_order: bool | None = None
2860
+ marker: str | None = None
2861
+ max_items: int = 0
2862
+
2863
+ @classmethod
2864
+ def from_dict(cls, data: dict) -> GetDurableExecutionHistoryRequest:
2865
+ return cls(
2866
+ durable_execution_arn=data["DurableExecutionArn"],
2867
+ include_execution_data=data.get("IncludeExecutionData"),
2868
+ reverse_order=data.get("ReverseOrder"),
2869
+ marker=data.get("Marker"),
2870
+ max_items=data.get("MaxItems", 0),
2871
+ )
2872
+
2873
+ def to_dict(self) -> dict[str, Any]:
2874
+ result: dict[str, Any] = {"DurableExecutionArn": self.durable_execution_arn}
2875
+ if self.include_execution_data is not None:
2876
+ result["IncludeExecutionData"] = self.include_execution_data
2877
+ if self.reverse_order is not None:
2878
+ result["ReverseOrder"] = self.reverse_order
2879
+ if self.marker is not None:
2880
+ result["Marker"] = self.marker
2881
+ if self.max_items is not None:
2882
+ result["MaxItems"] = self.max_items
2883
+ return result
2884
+
2885
+
2886
+ @dataclass(frozen=True)
2887
+ class GetDurableExecutionHistoryResponse:
2888
+ """Response containing durable execution history events."""
2889
+
2890
+ events: list[Event]
2891
+ next_marker: str | None = None
2892
+
2893
+ @classmethod
2894
+ def from_dict(cls, data: dict) -> GetDurableExecutionHistoryResponse:
2895
+ events = [Event.from_dict(event_data) for event_data in data.get("Events", [])]
2896
+ return cls(
2897
+ events=events,
2898
+ next_marker=data.get("NextMarker"),
2899
+ )
2900
+
2901
+ def to_dict(self) -> dict[str, Any]:
2902
+ result: dict[str, Any] = {"Events": [event.to_dict() for event in self.events]}
2903
+ if self.next_marker is not None:
2904
+ result["NextMarker"] = self.next_marker
2905
+ return result
2906
+
2907
+
2908
+ @dataclass(frozen=True)
2909
+ class ListDurableExecutionsByFunctionRequest:
2910
+ """Request to list durable executions by function."""
2911
+
2912
+ function_name: str
2913
+ qualifier: str | None = None
2914
+ durable_execution_name: str | None = None
2915
+ status_filter: list[str] | None = None
2916
+ started_after: str | None = None
2917
+ started_before: str | None = None
2918
+ marker: str | None = None
2919
+ max_items: int = 0
2920
+ reverse_order: bool | None = None
2921
+
2922
+ @classmethod
2923
+ def from_dict(cls, data: dict) -> ListDurableExecutionsByFunctionRequest:
2924
+ # Handle query parameters that may be lists
2925
+ function_name = data.get("FunctionName")
2926
+ if isinstance(function_name, list):
2927
+ function_name = function_name[0] if function_name else ""
2928
+ elif not function_name:
2929
+ function_name = ""
2930
+
2931
+ qualifier = data.get("Qualifier") or data.get("functionVersion")
2932
+ if isinstance(qualifier, list):
2933
+ qualifier = qualifier[0] if qualifier else None
2934
+
2935
+ durable_execution_name = data.get("DurableExecutionName") or data.get(
2936
+ "executionName"
2937
+ )
2938
+ if isinstance(durable_execution_name, list):
2939
+ durable_execution_name = (
2940
+ durable_execution_name[0] if durable_execution_name else None
2941
+ )
2942
+
2943
+ status_filter = data.get("StatusFilter") or data.get("statusFilter")
2944
+ if isinstance(status_filter, list):
2945
+ status_filter = status_filter if status_filter else None
2946
+ elif status_filter:
2947
+ status_filter = [status_filter]
2948
+
2949
+ started_after = data.get("StartedAfter") or data.get("startedAfter")
2950
+ if isinstance(started_after, list):
2951
+ started_after = started_after[0] if started_after else None
2952
+
2953
+ started_before = data.get("StartedBefore") or data.get("startedBefore")
2954
+ if isinstance(started_before, list):
2955
+ started_before = started_before[0] if started_before else None
2956
+
2957
+ marker = data.get("Marker") or data.get("marker")
2958
+ if isinstance(marker, list):
2959
+ marker = marker[0] if marker else None
2960
+
2961
+ max_items = data.get("MaxItems") or data.get("maxItems", 0)
2962
+ if isinstance(max_items, list):
2963
+ max_items = int(max_items[0]) if max_items else 0
2964
+
2965
+ reverse_order = data.get("ReverseOrder") or data.get("reverseOrder")
2966
+ if isinstance(reverse_order, list):
2967
+ reverse_order = (
2968
+ reverse_order[0].lower() in ("true", "1", "yes")
2969
+ if reverse_order
2970
+ else None
2971
+ )
2972
+ elif isinstance(reverse_order, str):
2973
+ reverse_order = reverse_order.lower() in ("true", "1", "yes")
2974
+
2975
+ return cls(
2976
+ function_name=function_name,
2977
+ qualifier=qualifier,
2978
+ durable_execution_name=durable_execution_name,
2979
+ status_filter=status_filter,
2980
+ started_after=started_after,
2981
+ started_before=started_before,
2982
+ marker=marker,
2983
+ max_items=max_items,
2984
+ reverse_order=reverse_order,
2985
+ )
2986
+
2987
+ def to_dict(self) -> dict[str, Any]:
2988
+ result: dict[str, Any] = {"FunctionName": self.function_name}
2989
+ if self.qualifier is not None:
2990
+ result["Qualifier"] = self.qualifier
2991
+ if self.durable_execution_name is not None:
2992
+ result["DurableExecutionName"] = self.durable_execution_name
2993
+ if self.status_filter is not None:
2994
+ result["StatusFilter"] = self.status_filter
2995
+ if self.started_after is not None:
2996
+ result["StartedAfter"] = self.started_after
2997
+ if self.started_before is not None:
2998
+ result["StartedBefore"] = self.started_before
2999
+ if self.marker is not None:
3000
+ result["Marker"] = self.marker
3001
+ if self.max_items is not None:
3002
+ result["MaxItems"] = self.max_items
3003
+ if self.reverse_order is not None:
3004
+ result["ReverseOrder"] = self.reverse_order
3005
+ return result
3006
+
3007
+
3008
+ @dataclass(frozen=True)
3009
+ class ListDurableExecutionsByFunctionResponse:
3010
+ """Response containing list of durable executions by function."""
3011
+
3012
+ durable_executions: list[Execution]
3013
+ next_marker: str | None = None
3014
+
3015
+ @classmethod
3016
+ def from_dict(cls, data: dict) -> ListDurableExecutionsByFunctionResponse:
3017
+ executions = [
3018
+ Execution.from_dict(exec_data)
3019
+ for exec_data in data.get("DurableExecutions", [])
3020
+ ]
3021
+ return cls(
3022
+ durable_executions=executions,
3023
+ next_marker=data.get("NextMarker"),
3024
+ )
3025
+
3026
+ def to_dict(self) -> dict[str, Any]:
3027
+ result: dict[str, Any] = {
3028
+ "DurableExecutions": [exe.to_dict() for exe in self.durable_executions]
3029
+ }
3030
+ if self.next_marker is not None:
3031
+ result["NextMarker"] = self.next_marker
3032
+ return result
3033
+
3034
+
3035
+ # endregion history_models
3036
+
3037
+
3038
+ # region callback_models
3039
+ # Callback-related models
3040
+ @dataclass(frozen=True)
3041
+ class SendDurableExecutionCallbackSuccessRequest:
3042
+ """Request to send callback success."""
3043
+
3044
+ callback_id: str
3045
+ result: bytes | None = None
3046
+
3047
+ @classmethod
3048
+ def from_dict(cls, data: dict) -> SendDurableExecutionCallbackSuccessRequest:
3049
+ return cls(
3050
+ callback_id=data["CallbackId"],
3051
+ result=data.get("Result"),
3052
+ )
3053
+
3054
+ def to_dict(self) -> dict[str, Any]:
3055
+ result: dict[str, Any] = {"CallbackId": self.callback_id}
3056
+ if self.result is not None:
3057
+ result["Result"] = self.result
3058
+ return result
3059
+
3060
+
3061
+ @dataclass(frozen=True)
3062
+ class SendDurableExecutionCallbackSuccessResponse:
3063
+ """Response from sending callback success."""
3064
+
3065
+
3066
+ @dataclass(frozen=True)
3067
+ class SendDurableExecutionCallbackFailureRequest:
3068
+ """Request to send callback failure."""
3069
+
3070
+ callback_id: str
3071
+ error: ErrorObject | None = None
3072
+
3073
+ @classmethod
3074
+ def from_dict(
3075
+ cls, data: dict, callback_id: str
3076
+ ) -> SendDurableExecutionCallbackFailureRequest:
3077
+ error = ErrorObject.from_dict(data) if data else None
3078
+
3079
+ return cls(
3080
+ callback_id=callback_id,
3081
+ error=error,
3082
+ )
3083
+
3084
+ def to_dict(self) -> dict[str, Any]:
3085
+ result: dict[str, Any] = {"CallbackId": self.callback_id}
3086
+ if self.error is not None:
3087
+ result["Error"] = self.error.to_dict()
3088
+ return result
3089
+
3090
+
3091
+ @dataclass(frozen=True)
3092
+ class SendDurableExecutionCallbackFailureResponse:
3093
+ """Response from sending callback failure."""
3094
+
3095
+
3096
+ @dataclass(frozen=True)
3097
+ class SendDurableExecutionCallbackHeartbeatRequest:
3098
+ """Request to send callback heartbeat."""
3099
+
3100
+ callback_id: str
3101
+
3102
+ @classmethod
3103
+ def from_dict(cls, data: dict) -> SendDurableExecutionCallbackHeartbeatRequest:
3104
+ return cls(callback_id=data["CallbackId"])
3105
+
3106
+ def to_dict(self) -> dict[str, Any]:
3107
+ return {"CallbackId": self.callback_id}
3108
+
3109
+
3110
+ @dataclass(frozen=True)
3111
+ class SendDurableExecutionCallbackHeartbeatResponse:
3112
+ """Response from sending callback heartbeat."""
3113
+
3114
+
3115
+ # endregion callback_models
3116
+
3117
+
3118
+ # region checkpoint_models
3119
+ # Checkpoint-related models
3120
+ @dataclass(frozen=True)
3121
+ class CheckpointUpdatedExecutionState:
3122
+ """Updated execution state from checkpoint."""
3123
+
3124
+ operations: list[Operation]
3125
+ next_marker: str | None = None
3126
+
3127
+ @classmethod
3128
+ def from_dict(cls, data: dict) -> CheckpointUpdatedExecutionState:
3129
+ operations = [
3130
+ Operation.from_dict(op_data) for op_data in data.get("Operations", [])
3131
+ ]
3132
+ return cls(
3133
+ operations=operations,
3134
+ next_marker=data.get("NextMarker"),
3135
+ )
3136
+
3137
+ def to_dict(self) -> dict[str, Any]:
3138
+ result: dict[str, Any] = {
3139
+ "Operations": [op.to_dict() for op in self.operations]
3140
+ }
3141
+ if self.next_marker is not None:
3142
+ result["NextMarker"] = self.next_marker
3143
+ return result
3144
+
3145
+
3146
+ @dataclass(frozen=True)
3147
+ class CheckpointDurableExecutionRequest:
3148
+ """Request to checkpoint a durable execution."""
3149
+
3150
+ durable_execution_arn: str
3151
+ checkpoint_token: str
3152
+ updates: list[OperationUpdate] | None = None
3153
+ client_token: str | None = None
3154
+
3155
+ @classmethod
3156
+ def from_dict(
3157
+ cls, data: dict, durable_execution_arn: str
3158
+ ) -> CheckpointDurableExecutionRequest:
3159
+ updates = None
3160
+ if updates_data := data.get("Updates"):
3161
+ updates = []
3162
+ for update_data in updates_data:
3163
+ # Map dictionary fields to OperationUpdate constructor parameters
3164
+ operation_update = OperationUpdate(
3165
+ operation_id=update_data["Id"],
3166
+ operation_type=OperationType(update_data["Type"]),
3167
+ action=OperationAction(update_data["Action"]),
3168
+ parent_id=update_data.get("ParentId"),
3169
+ name=update_data.get("Name"),
3170
+ sub_type=OperationSubType(update_data["SubType"])
3171
+ if update_data.get("SubType")
3172
+ else None,
3173
+ payload=update_data.get("Payload"),
3174
+ error=ErrorObject.from_dict(update_data["Error"])
3175
+ if update_data.get("Error")
3176
+ else None,
3177
+ context_options=ContextOptions.from_dict(
3178
+ update_data["ContextOptions"]
3179
+ )
3180
+ if update_data.get("ContextOptions")
3181
+ else None,
3182
+ step_options=StepOptions.from_dict(update_data["StepOptions"])
3183
+ if update_data.get("StepOptions")
3184
+ else None,
3185
+ wait_options=WaitOptions.from_dict(update_data["WaitOptions"])
3186
+ if update_data.get("WaitOptions")
3187
+ else None,
3188
+ callback_options=CallbackOptions.from_dict(
3189
+ update_data["CallbackOptions"]
3190
+ )
3191
+ if update_data.get("CallbackOptions")
3192
+ else None,
3193
+ chained_invoke_options=ChainedInvokeOptions.from_dict(
3194
+ update_data["ChainedInvokeOptions"]
3195
+ )
3196
+ if update_data.get("ChainedInvokeOptions")
3197
+ else None,
3198
+ )
3199
+ updates.append(operation_update)
3200
+
3201
+ return cls(
3202
+ durable_execution_arn=durable_execution_arn,
3203
+ checkpoint_token=data["CheckpointToken"],
3204
+ updates=updates,
3205
+ client_token=data.get("ClientToken"),
3206
+ )
3207
+
3208
+ def to_dict(self) -> dict[str, Any]:
3209
+ result: dict[str, Any] = {
3210
+ "DurableExecutionArn": self.durable_execution_arn,
3211
+ "CheckpointToken": self.checkpoint_token,
3212
+ }
3213
+ if self.updates is not None:
3214
+ result["Updates"] = [update.to_dict() for update in self.updates]
3215
+ if self.client_token is not None:
3216
+ result["ClientToken"] = self.client_token
3217
+ return result
3218
+
3219
+
3220
+ @dataclass(frozen=True)
3221
+ class CheckpointDurableExecutionResponse:
3222
+ """Response from checkpointing a durable execution."""
3223
+
3224
+ checkpoint_token: str
3225
+ new_execution_state: CheckpointUpdatedExecutionState | None = None
3226
+
3227
+ @classmethod
3228
+ def from_dict(cls, data: dict) -> CheckpointDurableExecutionResponse:
3229
+ new_execution_state = None
3230
+ if state_data := data.get("NewExecutionState"):
3231
+ new_execution_state = CheckpointUpdatedExecutionState.from_dict(state_data)
3232
+
3233
+ return cls(
3234
+ checkpoint_token=data["CheckpointToken"],
3235
+ new_execution_state=new_execution_state,
3236
+ )
3237
+
3238
+ def to_dict(self) -> dict[str, Any]:
3239
+ result: dict[str, Any] = {"CheckpointToken": self.checkpoint_token}
3240
+ if self.new_execution_state is not None:
3241
+ result["NewExecutionState"] = self.new_execution_state.to_dict()
3242
+ return result
3243
+
3244
+
3245
+ # endregion checkpoint_models
3246
+
3247
+
3248
+ # region error_models
3249
+ # Error response structure for consistent error handling
3250
+ @dataclass(frozen=True)
3251
+ class ErrorResponse:
3252
+ """Structured error response for web service operations."""
3253
+
3254
+ error_type: str
3255
+ error_message: str
3256
+ error_code: str | None = None
3257
+ request_id: str | None = None
3258
+
3259
+ @classmethod
3260
+ def from_dict(cls, data: dict) -> ErrorResponse:
3261
+ """Create ErrorResponse from dictionary.
3262
+
3263
+ Args:
3264
+ data: Dictionary containing error data
3265
+
3266
+ Returns:
3267
+ ErrorResponse: The error response object
3268
+ """
3269
+ error_data = data.get("error", data) # Support both nested and flat structures
3270
+ return cls(
3271
+ error_type=error_data["type"],
3272
+ error_message=error_data["message"],
3273
+ error_code=error_data.get("code"),
3274
+ request_id=error_data.get("requestId"),
3275
+ )
3276
+
3277
+ def to_dict(self) -> dict[str, Any]:
3278
+ """Convert ErrorResponse to dictionary.
3279
+
3280
+ Returns:
3281
+ dict: Dictionary representation of the error response
3282
+ """
3283
+ error_data: dict[str, Any] = {
3284
+ "type": self.error_type,
3285
+ "message": self.error_message,
3286
+ }
3287
+
3288
+ if self.error_code is not None:
3289
+ error_data["code"] = self.error_code
3290
+ if self.request_id is not None:
3291
+ error_data["requestId"] = self.request_id
3292
+
3293
+ return {"error": error_data}
3294
+
3295
+
3296
+ # endregion error_models