pyworkflow-engine 0.1.7__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 (196) hide show
  1. dashboard/backend/app/__init__.py +1 -0
  2. dashboard/backend/app/config.py +32 -0
  3. dashboard/backend/app/controllers/__init__.py +6 -0
  4. dashboard/backend/app/controllers/run_controller.py +86 -0
  5. dashboard/backend/app/controllers/workflow_controller.py +33 -0
  6. dashboard/backend/app/dependencies/__init__.py +5 -0
  7. dashboard/backend/app/dependencies/storage.py +50 -0
  8. dashboard/backend/app/repositories/__init__.py +6 -0
  9. dashboard/backend/app/repositories/run_repository.py +80 -0
  10. dashboard/backend/app/repositories/workflow_repository.py +27 -0
  11. dashboard/backend/app/rest/__init__.py +8 -0
  12. dashboard/backend/app/rest/v1/__init__.py +12 -0
  13. dashboard/backend/app/rest/v1/health.py +33 -0
  14. dashboard/backend/app/rest/v1/runs.py +133 -0
  15. dashboard/backend/app/rest/v1/workflows.py +41 -0
  16. dashboard/backend/app/schemas/__init__.py +23 -0
  17. dashboard/backend/app/schemas/common.py +16 -0
  18. dashboard/backend/app/schemas/event.py +24 -0
  19. dashboard/backend/app/schemas/hook.py +25 -0
  20. dashboard/backend/app/schemas/run.py +54 -0
  21. dashboard/backend/app/schemas/step.py +28 -0
  22. dashboard/backend/app/schemas/workflow.py +31 -0
  23. dashboard/backend/app/server.py +87 -0
  24. dashboard/backend/app/services/__init__.py +6 -0
  25. dashboard/backend/app/services/run_service.py +240 -0
  26. dashboard/backend/app/services/workflow_service.py +155 -0
  27. dashboard/backend/main.py +18 -0
  28. docs/concepts/cancellation.mdx +362 -0
  29. docs/concepts/continue-as-new.mdx +434 -0
  30. docs/concepts/events.mdx +266 -0
  31. docs/concepts/fault-tolerance.mdx +370 -0
  32. docs/concepts/hooks.mdx +552 -0
  33. docs/concepts/limitations.mdx +167 -0
  34. docs/concepts/schedules.mdx +775 -0
  35. docs/concepts/sleep.mdx +312 -0
  36. docs/concepts/steps.mdx +301 -0
  37. docs/concepts/workflows.mdx +255 -0
  38. docs/guides/cli.mdx +942 -0
  39. docs/guides/configuration.mdx +560 -0
  40. docs/introduction.mdx +155 -0
  41. docs/quickstart.mdx +279 -0
  42. examples/__init__.py +1 -0
  43. examples/celery/__init__.py +1 -0
  44. examples/celery/durable/docker-compose.yml +55 -0
  45. examples/celery/durable/pyworkflow.config.yaml +12 -0
  46. examples/celery/durable/workflows/__init__.py +122 -0
  47. examples/celery/durable/workflows/basic.py +87 -0
  48. examples/celery/durable/workflows/batch_processing.py +102 -0
  49. examples/celery/durable/workflows/cancellation.py +273 -0
  50. examples/celery/durable/workflows/child_workflow_patterns.py +240 -0
  51. examples/celery/durable/workflows/child_workflows.py +202 -0
  52. examples/celery/durable/workflows/continue_as_new.py +260 -0
  53. examples/celery/durable/workflows/fault_tolerance.py +210 -0
  54. examples/celery/durable/workflows/hooks.py +211 -0
  55. examples/celery/durable/workflows/idempotency.py +112 -0
  56. examples/celery/durable/workflows/long_running.py +99 -0
  57. examples/celery/durable/workflows/retries.py +101 -0
  58. examples/celery/durable/workflows/schedules.py +209 -0
  59. examples/celery/transient/01_basic_workflow.py +91 -0
  60. examples/celery/transient/02_fault_tolerance.py +257 -0
  61. examples/celery/transient/__init__.py +20 -0
  62. examples/celery/transient/pyworkflow.config.yaml +25 -0
  63. examples/local/__init__.py +1 -0
  64. examples/local/durable/01_basic_workflow.py +94 -0
  65. examples/local/durable/02_file_storage.py +132 -0
  66. examples/local/durable/03_retries.py +169 -0
  67. examples/local/durable/04_long_running.py +119 -0
  68. examples/local/durable/05_event_log.py +145 -0
  69. examples/local/durable/06_idempotency.py +148 -0
  70. examples/local/durable/07_hooks.py +334 -0
  71. examples/local/durable/08_cancellation.py +233 -0
  72. examples/local/durable/09_child_workflows.py +198 -0
  73. examples/local/durable/10_child_workflow_patterns.py +265 -0
  74. examples/local/durable/11_continue_as_new.py +249 -0
  75. examples/local/durable/12_schedules.py +198 -0
  76. examples/local/durable/__init__.py +1 -0
  77. examples/local/transient/01_quick_tasks.py +87 -0
  78. examples/local/transient/02_retries.py +130 -0
  79. examples/local/transient/03_sleep.py +141 -0
  80. examples/local/transient/__init__.py +1 -0
  81. pyworkflow/__init__.py +256 -0
  82. pyworkflow/aws/__init__.py +68 -0
  83. pyworkflow/aws/context.py +234 -0
  84. pyworkflow/aws/handler.py +184 -0
  85. pyworkflow/aws/testing.py +310 -0
  86. pyworkflow/celery/__init__.py +41 -0
  87. pyworkflow/celery/app.py +198 -0
  88. pyworkflow/celery/scheduler.py +315 -0
  89. pyworkflow/celery/tasks.py +1746 -0
  90. pyworkflow/cli/__init__.py +132 -0
  91. pyworkflow/cli/__main__.py +6 -0
  92. pyworkflow/cli/commands/__init__.py +1 -0
  93. pyworkflow/cli/commands/hooks.py +640 -0
  94. pyworkflow/cli/commands/quickstart.py +495 -0
  95. pyworkflow/cli/commands/runs.py +773 -0
  96. pyworkflow/cli/commands/scheduler.py +130 -0
  97. pyworkflow/cli/commands/schedules.py +794 -0
  98. pyworkflow/cli/commands/setup.py +703 -0
  99. pyworkflow/cli/commands/worker.py +413 -0
  100. pyworkflow/cli/commands/workflows.py +1257 -0
  101. pyworkflow/cli/output/__init__.py +1 -0
  102. pyworkflow/cli/output/formatters.py +321 -0
  103. pyworkflow/cli/output/styles.py +121 -0
  104. pyworkflow/cli/utils/__init__.py +1 -0
  105. pyworkflow/cli/utils/async_helpers.py +30 -0
  106. pyworkflow/cli/utils/config.py +130 -0
  107. pyworkflow/cli/utils/config_generator.py +344 -0
  108. pyworkflow/cli/utils/discovery.py +53 -0
  109. pyworkflow/cli/utils/docker_manager.py +651 -0
  110. pyworkflow/cli/utils/interactive.py +364 -0
  111. pyworkflow/cli/utils/storage.py +115 -0
  112. pyworkflow/config.py +329 -0
  113. pyworkflow/context/__init__.py +63 -0
  114. pyworkflow/context/aws.py +230 -0
  115. pyworkflow/context/base.py +416 -0
  116. pyworkflow/context/local.py +930 -0
  117. pyworkflow/context/mock.py +381 -0
  118. pyworkflow/core/__init__.py +0 -0
  119. pyworkflow/core/exceptions.py +353 -0
  120. pyworkflow/core/registry.py +313 -0
  121. pyworkflow/core/scheduled.py +328 -0
  122. pyworkflow/core/step.py +494 -0
  123. pyworkflow/core/workflow.py +294 -0
  124. pyworkflow/discovery.py +248 -0
  125. pyworkflow/engine/__init__.py +0 -0
  126. pyworkflow/engine/events.py +879 -0
  127. pyworkflow/engine/executor.py +682 -0
  128. pyworkflow/engine/replay.py +273 -0
  129. pyworkflow/observability/__init__.py +19 -0
  130. pyworkflow/observability/logging.py +234 -0
  131. pyworkflow/primitives/__init__.py +33 -0
  132. pyworkflow/primitives/child_handle.py +174 -0
  133. pyworkflow/primitives/child_workflow.py +372 -0
  134. pyworkflow/primitives/continue_as_new.py +101 -0
  135. pyworkflow/primitives/define_hook.py +150 -0
  136. pyworkflow/primitives/hooks.py +97 -0
  137. pyworkflow/primitives/resume_hook.py +210 -0
  138. pyworkflow/primitives/schedule.py +545 -0
  139. pyworkflow/primitives/shield.py +96 -0
  140. pyworkflow/primitives/sleep.py +100 -0
  141. pyworkflow/runtime/__init__.py +21 -0
  142. pyworkflow/runtime/base.py +179 -0
  143. pyworkflow/runtime/celery.py +310 -0
  144. pyworkflow/runtime/factory.py +101 -0
  145. pyworkflow/runtime/local.py +706 -0
  146. pyworkflow/scheduler/__init__.py +9 -0
  147. pyworkflow/scheduler/local.py +248 -0
  148. pyworkflow/serialization/__init__.py +0 -0
  149. pyworkflow/serialization/decoder.py +146 -0
  150. pyworkflow/serialization/encoder.py +162 -0
  151. pyworkflow/storage/__init__.py +54 -0
  152. pyworkflow/storage/base.py +612 -0
  153. pyworkflow/storage/config.py +185 -0
  154. pyworkflow/storage/dynamodb.py +1315 -0
  155. pyworkflow/storage/file.py +827 -0
  156. pyworkflow/storage/memory.py +549 -0
  157. pyworkflow/storage/postgres.py +1161 -0
  158. pyworkflow/storage/schemas.py +486 -0
  159. pyworkflow/storage/sqlite.py +1136 -0
  160. pyworkflow/utils/__init__.py +0 -0
  161. pyworkflow/utils/duration.py +177 -0
  162. pyworkflow/utils/schedule.py +391 -0
  163. pyworkflow_engine-0.1.7.dist-info/METADATA +687 -0
  164. pyworkflow_engine-0.1.7.dist-info/RECORD +196 -0
  165. pyworkflow_engine-0.1.7.dist-info/WHEEL +5 -0
  166. pyworkflow_engine-0.1.7.dist-info/entry_points.txt +2 -0
  167. pyworkflow_engine-0.1.7.dist-info/licenses/LICENSE +21 -0
  168. pyworkflow_engine-0.1.7.dist-info/top_level.txt +5 -0
  169. tests/examples/__init__.py +0 -0
  170. tests/integration/__init__.py +0 -0
  171. tests/integration/test_cancellation.py +330 -0
  172. tests/integration/test_child_workflows.py +439 -0
  173. tests/integration/test_continue_as_new.py +428 -0
  174. tests/integration/test_dynamodb_storage.py +1146 -0
  175. tests/integration/test_fault_tolerance.py +369 -0
  176. tests/integration/test_schedule_storage.py +484 -0
  177. tests/unit/__init__.py +0 -0
  178. tests/unit/backends/__init__.py +1 -0
  179. tests/unit/backends/test_dynamodb_storage.py +1554 -0
  180. tests/unit/backends/test_postgres_storage.py +1281 -0
  181. tests/unit/backends/test_sqlite_storage.py +1460 -0
  182. tests/unit/conftest.py +41 -0
  183. tests/unit/test_cancellation.py +364 -0
  184. tests/unit/test_child_workflows.py +680 -0
  185. tests/unit/test_continue_as_new.py +441 -0
  186. tests/unit/test_event_limits.py +316 -0
  187. tests/unit/test_executor.py +320 -0
  188. tests/unit/test_fault_tolerance.py +334 -0
  189. tests/unit/test_hooks.py +495 -0
  190. tests/unit/test_registry.py +261 -0
  191. tests/unit/test_replay.py +420 -0
  192. tests/unit/test_schedule_schemas.py +285 -0
  193. tests/unit/test_schedule_utils.py +286 -0
  194. tests/unit/test_scheduled_workflow.py +274 -0
  195. tests/unit/test_step.py +353 -0
  196. tests/unit/test_workflow.py +243 -0
@@ -0,0 +1,879 @@
1
+ """
2
+ Event types and schemas for event sourcing.
3
+
4
+ All workflow state changes are recorded as events in an append-only log.
5
+ Events enable deterministic replay for fault tolerance and resumption.
6
+ """
7
+
8
+ import uuid
9
+ from dataclasses import dataclass, field
10
+ from datetime import UTC, datetime
11
+ from enum import Enum
12
+ from typing import Any
13
+
14
+
15
+ class EventType(Enum):
16
+ """All possible event types in the workflow system."""
17
+
18
+ # Workflow lifecycle events
19
+ WORKFLOW_STARTED = "workflow.started"
20
+ WORKFLOW_COMPLETED = "workflow.completed"
21
+ WORKFLOW_FAILED = "workflow.failed"
22
+ WORKFLOW_INTERRUPTED = "workflow.interrupted" # Infrastructure failure (worker loss)
23
+ WORKFLOW_CANCELLED = "workflow.cancelled"
24
+ WORKFLOW_PAUSED = "workflow.paused"
25
+ WORKFLOW_RESUMED = "workflow.resumed"
26
+ WORKFLOW_CONTINUED_AS_NEW = "workflow.continued_as_new" # Workflow continued with fresh history
27
+
28
+ # Step lifecycle events
29
+ STEP_STARTED = "step.started"
30
+ STEP_COMPLETED = "step.completed"
31
+ STEP_FAILED = "step.failed"
32
+ STEP_RETRYING = "step.retrying"
33
+ STEP_CANCELLED = "step.cancelled"
34
+
35
+ # Sleep/wait events
36
+ SLEEP_STARTED = "sleep.started"
37
+ SLEEP_COMPLETED = "sleep.completed"
38
+
39
+ # Hook/webhook events
40
+ HOOK_CREATED = "hook.created"
41
+ HOOK_RECEIVED = "hook.received"
42
+ HOOK_EXPIRED = "hook.expired"
43
+ HOOK_DISPOSED = "hook.disposed"
44
+
45
+ # Cancellation events
46
+ CANCELLATION_REQUESTED = "cancellation.requested"
47
+
48
+ # Child workflow events
49
+ CHILD_WORKFLOW_STARTED = "child_workflow.started"
50
+ CHILD_WORKFLOW_COMPLETED = "child_workflow.completed"
51
+ CHILD_WORKFLOW_FAILED = "child_workflow.failed"
52
+ CHILD_WORKFLOW_CANCELLED = "child_workflow.cancelled"
53
+
54
+ # Schedule events
55
+ SCHEDULE_CREATED = "schedule.created"
56
+ SCHEDULE_UPDATED = "schedule.updated"
57
+ SCHEDULE_PAUSED = "schedule.paused"
58
+ SCHEDULE_RESUMED = "schedule.resumed"
59
+ SCHEDULE_DELETED = "schedule.deleted"
60
+ SCHEDULE_TRIGGERED = "schedule.triggered"
61
+ SCHEDULE_SKIPPED = "schedule.skipped"
62
+ SCHEDULE_BACKFILL_STARTED = "schedule.backfill_started"
63
+ SCHEDULE_BACKFILL_COMPLETED = "schedule.backfill_completed"
64
+
65
+
66
+ @dataclass
67
+ class Event:
68
+ """
69
+ Base event structure for all workflow events.
70
+
71
+ Events are immutable records of state changes, stored in an append-only log.
72
+ The sequence number is assigned by the storage layer to ensure ordering.
73
+ """
74
+
75
+ event_id: str = field(default_factory=lambda: f"evt_{uuid.uuid4().hex[:16]}")
76
+ run_id: str = ""
77
+ type: EventType = EventType.WORKFLOW_STARTED
78
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
79
+ data: dict[str, Any] = field(default_factory=dict)
80
+ sequence: int | None = None # Assigned by storage layer
81
+
82
+ def __post_init__(self) -> None:
83
+ """Validate event after initialization."""
84
+ if not self.run_id:
85
+ raise ValueError("Event must have a run_id")
86
+ if not isinstance(self.type, EventType):
87
+ raise TypeError(f"Event type must be EventType enum, got {type(self.type)}")
88
+
89
+
90
+ # Event creation helpers for common event types
91
+
92
+
93
+ def create_workflow_started_event(
94
+ run_id: str,
95
+ workflow_name: str,
96
+ args: Any,
97
+ kwargs: Any,
98
+ metadata: dict[str, Any] | None = None,
99
+ ) -> Event:
100
+ """Create a workflow started event."""
101
+ return Event(
102
+ run_id=run_id,
103
+ type=EventType.WORKFLOW_STARTED,
104
+ data={
105
+ "workflow_name": workflow_name,
106
+ "args": args,
107
+ "kwargs": kwargs,
108
+ "metadata": metadata or {},
109
+ },
110
+ )
111
+
112
+
113
+ def create_workflow_completed_event(run_id: str, result: Any, workflow_name: str) -> Event:
114
+ """Create a workflow completed event."""
115
+ return Event(
116
+ run_id=run_id,
117
+ type=EventType.WORKFLOW_COMPLETED,
118
+ data={"result": result, "workflow_name": workflow_name},
119
+ )
120
+
121
+
122
+ def create_workflow_failed_event(
123
+ run_id: str, error: str, error_type: str, traceback: str | None = None
124
+ ) -> Event:
125
+ """Create a workflow failed event."""
126
+ return Event(
127
+ run_id=run_id,
128
+ type=EventType.WORKFLOW_FAILED,
129
+ data={
130
+ "error": error,
131
+ "error_type": error_type,
132
+ "traceback": traceback,
133
+ },
134
+ )
135
+
136
+
137
+ def create_workflow_continued_as_new_event(
138
+ run_id: str,
139
+ new_run_id: str,
140
+ args: str,
141
+ kwargs: str,
142
+ reason: str | None = None,
143
+ ) -> Event:
144
+ """
145
+ Create a workflow continued as new event.
146
+
147
+ This event is recorded when a workflow completes by calling
148
+ continue_as_new(), indicating this run is complete and a new
149
+ run has been started with fresh event history.
150
+
151
+ Args:
152
+ run_id: The current workflow run ID
153
+ new_run_id: The new workflow run ID
154
+ args: Serialized positional arguments for new workflow
155
+ kwargs: Serialized keyword arguments for new workflow
156
+ reason: Optional reason for continuation
157
+
158
+ Returns:
159
+ Event: The workflow continued as new event
160
+ """
161
+ return Event(
162
+ run_id=run_id,
163
+ type=EventType.WORKFLOW_CONTINUED_AS_NEW,
164
+ data={
165
+ "new_run_id": new_run_id,
166
+ "args": args,
167
+ "kwargs": kwargs,
168
+ "reason": reason,
169
+ "continued_at": datetime.now(UTC).isoformat(),
170
+ },
171
+ )
172
+
173
+
174
+ def create_workflow_interrupted_event(
175
+ run_id: str,
176
+ reason: str,
177
+ worker_id: str | None = None,
178
+ last_event_sequence: int | None = None,
179
+ error: str | None = None,
180
+ recovery_attempt: int = 1,
181
+ recoverable: bool = True,
182
+ ) -> Event:
183
+ """
184
+ Create a workflow interrupted event.
185
+
186
+ This event is recorded when a workflow is interrupted due to infrastructure
187
+ failures (e.g., worker crash, timeout, signal) rather than application errors.
188
+
189
+ Args:
190
+ run_id: The workflow run ID
191
+ reason: Interruption reason (e.g., "worker_lost", "timeout", "signal")
192
+ worker_id: ID of the worker that was handling the task
193
+ last_event_sequence: Sequence number of the last recorded event
194
+ error: Optional error message
195
+ recovery_attempt: Current recovery attempt number
196
+ recoverable: Whether the workflow can be recovered
197
+
198
+ Returns:
199
+ Event: The workflow interrupted event
200
+ """
201
+ return Event(
202
+ run_id=run_id,
203
+ type=EventType.WORKFLOW_INTERRUPTED,
204
+ data={
205
+ "reason": reason,
206
+ "worker_id": worker_id,
207
+ "last_event_sequence": last_event_sequence,
208
+ "error": error,
209
+ "recovery_attempt": recovery_attempt,
210
+ "recoverable": recoverable,
211
+ },
212
+ )
213
+
214
+
215
+ def create_step_started_event(
216
+ run_id: str,
217
+ step_id: str,
218
+ step_name: str,
219
+ args: Any,
220
+ kwargs: Any,
221
+ attempt: int = 1,
222
+ ) -> Event:
223
+ """Create a step started event."""
224
+ return Event(
225
+ run_id=run_id,
226
+ type=EventType.STEP_STARTED,
227
+ data={
228
+ "step_id": step_id,
229
+ "step_name": step_name,
230
+ "args": args,
231
+ "kwargs": kwargs,
232
+ "attempt": attempt,
233
+ },
234
+ )
235
+
236
+
237
+ def create_step_completed_event(run_id: str, step_id: str, result: Any, step_name: str) -> Event:
238
+ """Create a step completed event."""
239
+ return Event(
240
+ run_id=run_id,
241
+ type=EventType.STEP_COMPLETED,
242
+ data={
243
+ "step_id": step_id,
244
+ "result": result,
245
+ "step_name": step_name,
246
+ },
247
+ )
248
+
249
+
250
+ def create_step_failed_event(
251
+ run_id: str,
252
+ step_id: str,
253
+ error: str,
254
+ error_type: str,
255
+ is_retryable: bool,
256
+ attempt: int,
257
+ traceback: str | None = None,
258
+ ) -> Event:
259
+ """Create a step failed event."""
260
+ return Event(
261
+ run_id=run_id,
262
+ type=EventType.STEP_FAILED,
263
+ data={
264
+ "step_id": step_id,
265
+ "error": error,
266
+ "error_type": error_type,
267
+ "is_retryable": is_retryable,
268
+ "attempt": attempt,
269
+ "traceback": traceback,
270
+ },
271
+ )
272
+
273
+
274
+ def create_step_retrying_event(
275
+ run_id: str,
276
+ step_id: str,
277
+ attempt: int,
278
+ retry_after: str | None = None,
279
+ error: str | None = None,
280
+ ) -> Event:
281
+ """Create a step retrying event."""
282
+ return Event(
283
+ run_id=run_id,
284
+ type=EventType.STEP_RETRYING,
285
+ data={
286
+ "step_id": step_id,
287
+ "attempt": attempt,
288
+ "retry_after": retry_after,
289
+ "error": error,
290
+ },
291
+ )
292
+
293
+
294
+ def create_sleep_started_event(
295
+ run_id: str,
296
+ sleep_id: str,
297
+ duration_seconds: int,
298
+ resume_at: datetime,
299
+ name: str | None = None,
300
+ ) -> Event:
301
+ """Create a sleep started event."""
302
+ return Event(
303
+ run_id=run_id,
304
+ type=EventType.SLEEP_STARTED,
305
+ data={
306
+ "sleep_id": sleep_id,
307
+ "duration_seconds": duration_seconds,
308
+ "resume_at": resume_at.isoformat(),
309
+ "name": name,
310
+ },
311
+ )
312
+
313
+
314
+ def create_sleep_completed_event(run_id: str, sleep_id: str) -> Event:
315
+ """Create a sleep completed event."""
316
+ return Event(
317
+ run_id=run_id,
318
+ type=EventType.SLEEP_COMPLETED,
319
+ data={"sleep_id": sleep_id},
320
+ )
321
+
322
+
323
+ def create_hook_created_event(
324
+ run_id: str,
325
+ hook_id: str,
326
+ token: str = "",
327
+ url: str = "",
328
+ expires_at: datetime | None = None,
329
+ name: str | None = None,
330
+ hook_name: str | None = None,
331
+ timeout_seconds: int | None = None,
332
+ ) -> Event:
333
+ """
334
+ Create a hook created event.
335
+
336
+ Args:
337
+ run_id: Workflow run ID
338
+ hook_id: Unique hook identifier
339
+ token: Security token for resuming the hook
340
+ url: Optional webhook URL
341
+ expires_at: Optional expiration datetime
342
+ name: Optional hook name (alias: hook_name)
343
+ hook_name: Alias for name (for backwards compatibility)
344
+ timeout_seconds: Alternative to expires_at (converted internally)
345
+ """
346
+ # Handle aliases
347
+ actual_name = name or hook_name
348
+
349
+ # Convert timeout_seconds to expires_at if provided
350
+ actual_expires_at = expires_at
351
+ if timeout_seconds and not expires_at:
352
+ from datetime import UTC, timedelta
353
+
354
+ actual_expires_at = datetime.now(UTC) + timedelta(seconds=timeout_seconds)
355
+
356
+ return Event(
357
+ run_id=run_id,
358
+ type=EventType.HOOK_CREATED,
359
+ data={
360
+ "hook_id": hook_id,
361
+ "url": url,
362
+ "token": token,
363
+ "expires_at": actual_expires_at.isoformat() if actual_expires_at else None,
364
+ "name": actual_name,
365
+ },
366
+ )
367
+
368
+
369
+ def create_hook_received_event(run_id: str, hook_id: str, payload: Any) -> Event:
370
+ """Create a hook received event."""
371
+ return Event(
372
+ run_id=run_id,
373
+ type=EventType.HOOK_RECEIVED,
374
+ data={
375
+ "hook_id": hook_id,
376
+ "payload": payload,
377
+ },
378
+ )
379
+
380
+
381
+ def create_hook_expired_event(run_id: str, hook_id: str) -> Event:
382
+ """Create a hook expired event."""
383
+ return Event(
384
+ run_id=run_id,
385
+ type=EventType.HOOK_EXPIRED,
386
+ data={"hook_id": hook_id},
387
+ )
388
+
389
+
390
+ def create_cancellation_requested_event(
391
+ run_id: str,
392
+ reason: str | None = None,
393
+ requested_by: str | None = None,
394
+ ) -> Event:
395
+ """
396
+ Create a cancellation requested event.
397
+
398
+ This event is recorded when cancellation is requested for a workflow.
399
+ It signals that the workflow should terminate gracefully.
400
+
401
+ Args:
402
+ run_id: The workflow run ID
403
+ reason: Optional reason for cancellation (e.g., "user_requested", "timeout")
404
+ requested_by: Optional identifier of who/what requested the cancellation
405
+
406
+ Returns:
407
+ Event: The cancellation requested event
408
+ """
409
+ return Event(
410
+ run_id=run_id,
411
+ type=EventType.CANCELLATION_REQUESTED,
412
+ data={
413
+ "reason": reason,
414
+ "requested_by": requested_by,
415
+ "requested_at": datetime.now(UTC).isoformat(),
416
+ },
417
+ )
418
+
419
+
420
+ def create_workflow_cancelled_event(
421
+ run_id: str,
422
+ reason: str | None = None,
423
+ cleanup_completed: bool = False,
424
+ ) -> Event:
425
+ """
426
+ Create a workflow cancelled event.
427
+
428
+ This event is recorded when a workflow has been successfully cancelled,
429
+ optionally after cleanup operations have completed.
430
+
431
+ Args:
432
+ run_id: The workflow run ID
433
+ reason: Optional reason for cancellation
434
+ cleanup_completed: Whether cleanup operations completed successfully
435
+
436
+ Returns:
437
+ Event: The workflow cancelled event
438
+ """
439
+ return Event(
440
+ run_id=run_id,
441
+ type=EventType.WORKFLOW_CANCELLED,
442
+ data={
443
+ "reason": reason,
444
+ "cleanup_completed": cleanup_completed,
445
+ "cancelled_at": datetime.now(UTC).isoformat(),
446
+ },
447
+ )
448
+
449
+
450
+ def create_step_cancelled_event(
451
+ run_id: str,
452
+ step_id: str,
453
+ step_name: str,
454
+ reason: str | None = None,
455
+ ) -> Event:
456
+ """
457
+ Create a step cancelled event.
458
+
459
+ This event is recorded when a step is cancelled, either because the
460
+ workflow was cancelled or the step was explicitly terminated.
461
+
462
+ Args:
463
+ run_id: The workflow run ID
464
+ step_id: The unique step identifier
465
+ step_name: The name of the step
466
+ reason: Optional reason for cancellation
467
+
468
+ Returns:
469
+ Event: The step cancelled event
470
+ """
471
+ return Event(
472
+ run_id=run_id,
473
+ type=EventType.STEP_CANCELLED,
474
+ data={
475
+ "step_id": step_id,
476
+ "step_name": step_name,
477
+ "reason": reason,
478
+ "cancelled_at": datetime.now(UTC).isoformat(),
479
+ },
480
+ )
481
+
482
+
483
+ # Child workflow event creation helpers
484
+
485
+
486
+ def create_child_workflow_started_event(
487
+ run_id: str,
488
+ child_id: str,
489
+ child_run_id: str,
490
+ child_workflow_name: str,
491
+ args: Any,
492
+ kwargs: Any,
493
+ wait_for_completion: bool,
494
+ ) -> Event:
495
+ """
496
+ Create a child workflow started event.
497
+
498
+ This event is recorded in the parent workflow's event log when a child
499
+ workflow is spawned.
500
+
501
+ Args:
502
+ run_id: The parent workflow run ID
503
+ child_id: Deterministic child identifier (for replay)
504
+ child_run_id: The child workflow's unique run ID
505
+ child_workflow_name: The name of the child workflow
506
+ args: Serialized positional arguments for child workflow
507
+ kwargs: Serialized keyword arguments for child workflow
508
+ wait_for_completion: Whether parent is waiting for child to complete
509
+
510
+ Returns:
511
+ Event: The child workflow started event
512
+ """
513
+ return Event(
514
+ run_id=run_id,
515
+ type=EventType.CHILD_WORKFLOW_STARTED,
516
+ data={
517
+ "child_id": child_id,
518
+ "child_run_id": child_run_id,
519
+ "child_workflow_name": child_workflow_name,
520
+ "args": args,
521
+ "kwargs": kwargs,
522
+ "wait_for_completion": wait_for_completion,
523
+ "started_at": datetime.now(UTC).isoformat(),
524
+ },
525
+ )
526
+
527
+
528
+ def create_child_workflow_completed_event(
529
+ run_id: str,
530
+ child_id: str,
531
+ child_run_id: str,
532
+ result: Any,
533
+ ) -> Event:
534
+ """
535
+ Create a child workflow completed event.
536
+
537
+ This event is recorded in the parent workflow's event log when a child
538
+ workflow completes successfully.
539
+
540
+ Args:
541
+ run_id: The parent workflow run ID
542
+ child_id: Deterministic child identifier (for replay)
543
+ child_run_id: The child workflow's run ID
544
+ result: Serialized result from the child workflow
545
+
546
+ Returns:
547
+ Event: The child workflow completed event
548
+ """
549
+ return Event(
550
+ run_id=run_id,
551
+ type=EventType.CHILD_WORKFLOW_COMPLETED,
552
+ data={
553
+ "child_id": child_id,
554
+ "child_run_id": child_run_id,
555
+ "result": result,
556
+ "completed_at": datetime.now(UTC).isoformat(),
557
+ },
558
+ )
559
+
560
+
561
+ def create_child_workflow_failed_event(
562
+ run_id: str,
563
+ child_id: str,
564
+ child_run_id: str,
565
+ error: str,
566
+ error_type: str,
567
+ ) -> Event:
568
+ """
569
+ Create a child workflow failed event.
570
+
571
+ This event is recorded in the parent workflow's event log when a child
572
+ workflow fails.
573
+
574
+ Args:
575
+ run_id: The parent workflow run ID
576
+ child_id: Deterministic child identifier (for replay)
577
+ child_run_id: The child workflow's run ID
578
+ error: Error message from the child workflow
579
+ error_type: The exception type that caused the failure
580
+
581
+ Returns:
582
+ Event: The child workflow failed event
583
+ """
584
+ return Event(
585
+ run_id=run_id,
586
+ type=EventType.CHILD_WORKFLOW_FAILED,
587
+ data={
588
+ "child_id": child_id,
589
+ "child_run_id": child_run_id,
590
+ "error": error,
591
+ "error_type": error_type,
592
+ "failed_at": datetime.now(UTC).isoformat(),
593
+ },
594
+ )
595
+
596
+
597
+ def create_child_workflow_cancelled_event(
598
+ run_id: str,
599
+ child_id: str,
600
+ child_run_id: str,
601
+ reason: str | None = None,
602
+ ) -> Event:
603
+ """
604
+ Create a child workflow cancelled event.
605
+
606
+ This event is recorded in the parent workflow's event log when a child
607
+ workflow is cancelled (typically due to parent completion or explicit cancel).
608
+
609
+ Args:
610
+ run_id: The parent workflow run ID
611
+ child_id: Deterministic child identifier (for replay)
612
+ child_run_id: The child workflow's run ID
613
+ reason: Optional reason for cancellation
614
+
615
+ Returns:
616
+ Event: The child workflow cancelled event
617
+ """
618
+ return Event(
619
+ run_id=run_id,
620
+ type=EventType.CHILD_WORKFLOW_CANCELLED,
621
+ data={
622
+ "child_id": child_id,
623
+ "child_run_id": child_run_id,
624
+ "reason": reason,
625
+ "cancelled_at": datetime.now(UTC).isoformat(),
626
+ },
627
+ )
628
+
629
+
630
+ # Schedule event creation helpers
631
+
632
+
633
+ def create_schedule_created_event(
634
+ run_id: str,
635
+ schedule_id: str,
636
+ workflow_name: str,
637
+ spec: dict[str, Any],
638
+ overlap_policy: str,
639
+ ) -> Event:
640
+ """
641
+ Create a schedule created event.
642
+
643
+ This event is recorded when a new schedule is created.
644
+
645
+ Args:
646
+ run_id: The run ID (use schedule_id for schedule-level events)
647
+ schedule_id: The schedule identifier
648
+ workflow_name: Name of the workflow being scheduled
649
+ spec: The schedule specification (as dict)
650
+ overlap_policy: The overlap policy for the schedule
651
+
652
+ Returns:
653
+ Event: The schedule created event
654
+ """
655
+ return Event(
656
+ run_id=run_id,
657
+ type=EventType.SCHEDULE_CREATED,
658
+ data={
659
+ "schedule_id": schedule_id,
660
+ "workflow_name": workflow_name,
661
+ "spec": spec,
662
+ "overlap_policy": overlap_policy,
663
+ "created_at": datetime.now(UTC).isoformat(),
664
+ },
665
+ )
666
+
667
+
668
+ def create_schedule_triggered_event(
669
+ run_id: str,
670
+ schedule_id: str,
671
+ scheduled_time: datetime,
672
+ actual_time: datetime,
673
+ workflow_run_id: str,
674
+ ) -> Event:
675
+ """
676
+ Create a schedule triggered event.
677
+
678
+ This event is recorded when a schedule triggers a workflow execution.
679
+
680
+ Args:
681
+ run_id: The workflow run ID being created
682
+ schedule_id: The schedule identifier
683
+ scheduled_time: The time the schedule was supposed to trigger
684
+ actual_time: The actual time the trigger occurred
685
+ workflow_run_id: The ID of the workflow run being created
686
+
687
+ Returns:
688
+ Event: The schedule triggered event
689
+ """
690
+ return Event(
691
+ run_id=run_id,
692
+ type=EventType.SCHEDULE_TRIGGERED,
693
+ data={
694
+ "schedule_id": schedule_id,
695
+ "scheduled_time": scheduled_time.isoformat(),
696
+ "actual_time": actual_time.isoformat(),
697
+ "workflow_run_id": workflow_run_id,
698
+ },
699
+ )
700
+
701
+
702
+ def create_schedule_skipped_event(
703
+ run_id: str,
704
+ schedule_id: str,
705
+ reason: str,
706
+ scheduled_time: datetime,
707
+ overlap_policy: str | None = None,
708
+ ) -> Event:
709
+ """
710
+ Create a schedule skipped event.
711
+
712
+ This event is recorded when a scheduled execution is skipped,
713
+ typically due to overlap policy or schedule being paused.
714
+
715
+ Args:
716
+ run_id: The run ID (use schedule_id for schedule-level events)
717
+ schedule_id: The schedule identifier
718
+ reason: The reason for skipping
719
+ scheduled_time: The time the schedule was supposed to trigger
720
+ overlap_policy: The overlap policy that caused the skip
721
+
722
+ Returns:
723
+ Event: The schedule skipped event
724
+ """
725
+ return Event(
726
+ run_id=run_id,
727
+ type=EventType.SCHEDULE_SKIPPED,
728
+ data={
729
+ "schedule_id": schedule_id,
730
+ "reason": reason,
731
+ "scheduled_time": scheduled_time.isoformat(),
732
+ "overlap_policy": overlap_policy,
733
+ "skipped_at": datetime.now(UTC).isoformat(),
734
+ },
735
+ )
736
+
737
+
738
+ def create_schedule_paused_event(
739
+ run_id: str,
740
+ schedule_id: str,
741
+ reason: str | None = None,
742
+ ) -> Event:
743
+ """
744
+ Create a schedule paused event.
745
+
746
+ Args:
747
+ run_id: The run ID (use schedule_id for schedule-level events)
748
+ schedule_id: The schedule identifier
749
+ reason: Optional reason for pausing
750
+
751
+ Returns:
752
+ Event: The schedule paused event
753
+ """
754
+ return Event(
755
+ run_id=run_id,
756
+ type=EventType.SCHEDULE_PAUSED,
757
+ data={
758
+ "schedule_id": schedule_id,
759
+ "reason": reason,
760
+ "paused_at": datetime.now(UTC).isoformat(),
761
+ },
762
+ )
763
+
764
+
765
+ def create_schedule_resumed_event(
766
+ run_id: str,
767
+ schedule_id: str,
768
+ next_run_time: datetime | None = None,
769
+ ) -> Event:
770
+ """
771
+ Create a schedule resumed event.
772
+
773
+ Args:
774
+ run_id: The run ID (use schedule_id for schedule-level events)
775
+ schedule_id: The schedule identifier
776
+ next_run_time: The next scheduled run time after resumption
777
+
778
+ Returns:
779
+ Event: The schedule resumed event
780
+ """
781
+ return Event(
782
+ run_id=run_id,
783
+ type=EventType.SCHEDULE_RESUMED,
784
+ data={
785
+ "schedule_id": schedule_id,
786
+ "next_run_time": next_run_time.isoformat() if next_run_time else None,
787
+ "resumed_at": datetime.now(UTC).isoformat(),
788
+ },
789
+ )
790
+
791
+
792
+ def create_schedule_deleted_event(
793
+ run_id: str,
794
+ schedule_id: str,
795
+ reason: str | None = None,
796
+ ) -> Event:
797
+ """
798
+ Create a schedule deleted event.
799
+
800
+ Args:
801
+ run_id: The run ID (use schedule_id for schedule-level events)
802
+ schedule_id: The schedule identifier
803
+ reason: Optional reason for deletion
804
+
805
+ Returns:
806
+ Event: The schedule deleted event
807
+ """
808
+ return Event(
809
+ run_id=run_id,
810
+ type=EventType.SCHEDULE_DELETED,
811
+ data={
812
+ "schedule_id": schedule_id,
813
+ "reason": reason,
814
+ "deleted_at": datetime.now(UTC).isoformat(),
815
+ },
816
+ )
817
+
818
+
819
+ def create_schedule_backfill_started_event(
820
+ run_id: str,
821
+ schedule_id: str,
822
+ start_time: datetime,
823
+ end_time: datetime,
824
+ expected_runs: int,
825
+ ) -> Event:
826
+ """
827
+ Create a schedule backfill started event.
828
+
829
+ Args:
830
+ run_id: The run ID (use schedule_id for schedule-level events)
831
+ schedule_id: The schedule identifier
832
+ start_time: Start of the backfill period
833
+ end_time: End of the backfill period
834
+ expected_runs: Expected number of runs to create
835
+
836
+ Returns:
837
+ Event: The schedule backfill started event
838
+ """
839
+ return Event(
840
+ run_id=run_id,
841
+ type=EventType.SCHEDULE_BACKFILL_STARTED,
842
+ data={
843
+ "schedule_id": schedule_id,
844
+ "start_time": start_time.isoformat(),
845
+ "end_time": end_time.isoformat(),
846
+ "expected_runs": expected_runs,
847
+ "started_at": datetime.now(UTC).isoformat(),
848
+ },
849
+ )
850
+
851
+
852
+ def create_schedule_backfill_completed_event(
853
+ run_id: str,
854
+ schedule_id: str,
855
+ runs_created: int,
856
+ run_ids: list[str],
857
+ ) -> Event:
858
+ """
859
+ Create a schedule backfill completed event.
860
+
861
+ Args:
862
+ run_id: The run ID (use schedule_id for schedule-level events)
863
+ schedule_id: The schedule identifier
864
+ runs_created: Number of runs actually created
865
+ run_ids: List of created run IDs
866
+
867
+ Returns:
868
+ Event: The schedule backfill completed event
869
+ """
870
+ return Event(
871
+ run_id=run_id,
872
+ type=EventType.SCHEDULE_BACKFILL_COMPLETED,
873
+ data={
874
+ "schedule_id": schedule_id,
875
+ "runs_created": runs_created,
876
+ "run_ids": run_ids,
877
+ "completed_at": datetime.now(UTC).isoformat(),
878
+ },
879
+ )