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,328 @@
1
+ """
2
+ @scheduled_workflow decorator for defining workflows with built-in scheduling.
3
+
4
+ This decorator combines the @workflow decorator with schedule configuration,
5
+ enabling workflows to be automatically scheduled based on cron expressions,
6
+ intervals, or calendar specifications.
7
+
8
+ Examples:
9
+ # Cron-based schedule (every day at 9 AM)
10
+ @scheduled_workflow(cron="0 9 * * *")
11
+ async def daily_report():
12
+ return await generate_report()
13
+
14
+ # Interval-based schedule (every 5 minutes)
15
+ @scheduled_workflow(interval="5m")
16
+ async def health_check():
17
+ return await check_system_health()
18
+
19
+ # Combined with workflow options
20
+ @scheduled_workflow(
21
+ cron="0 0 * * 0",
22
+ name="weekly_cleanup",
23
+ overlap_policy=OverlapPolicy.SKIP,
24
+ timezone="America/New_York",
25
+ )
26
+ async def weekly_cleanup():
27
+ return await cleanup_old_data()
28
+ """
29
+
30
+ import functools
31
+ from collections.abc import Callable
32
+ from dataclasses import dataclass
33
+ from datetime import datetime
34
+ from typing import Any
35
+
36
+ from pyworkflow.core.registry import register_workflow
37
+ from pyworkflow.storage.schemas import CalendarSpec, OverlapPolicy, ScheduleSpec
38
+
39
+
40
+ @dataclass
41
+ class ScheduledWorkflowMetadata:
42
+ """Metadata for a scheduled workflow."""
43
+
44
+ workflow_name: str
45
+ spec: ScheduleSpec
46
+ overlap_policy: OverlapPolicy
47
+ func: Callable[..., Any]
48
+
49
+
50
+ # Registry for scheduled workflows (separate from main workflow registry)
51
+ _scheduled_workflows: dict[str, ScheduledWorkflowMetadata] = {}
52
+
53
+
54
+ def scheduled_workflow(
55
+ cron: str | None = None,
56
+ interval: str | None = None,
57
+ calendar: list[CalendarSpec] | None = None,
58
+ timezone: str = "UTC",
59
+ start_at: datetime | None = None,
60
+ end_at: datetime | None = None,
61
+ jitter: str | None = None,
62
+ overlap_policy: OverlapPolicy = OverlapPolicy.SKIP,
63
+ name: str | None = None,
64
+ durable: bool | None = None,
65
+ max_duration: str | None = None,
66
+ recover_on_worker_loss: bool | None = None,
67
+ max_recovery_attempts: int | None = None,
68
+ ) -> Callable:
69
+ """
70
+ Decorator to define a workflow with built-in scheduling configuration.
71
+
72
+ This decorator combines @workflow functionality with schedule specification,
73
+ allowing the workflow to be automatically scheduled when registered with
74
+ a scheduler.
75
+
76
+ Args:
77
+ cron: Cron expression for scheduling (e.g., "0 9 * * *" for daily at 9 AM)
78
+ interval: Interval duration (e.g., "5m", "1h", "30s")
79
+ calendar: List of CalendarSpec for calendar-based scheduling
80
+ timezone: Timezone for schedule (default: "UTC")
81
+ start_at: Optional start time for the schedule
82
+ end_at: Optional end time for the schedule
83
+ jitter: Random delay to prevent thundering herd (e.g., "30s")
84
+ overlap_policy: How to handle overlapping runs (default: SKIP)
85
+ name: Optional workflow name (defaults to function name)
86
+ durable: Whether workflow is durable (None = use configured default)
87
+ max_duration: Optional max duration (e.g., "1h", "30m")
88
+ recover_on_worker_loss: Whether to auto-recover on worker failure
89
+ max_recovery_attempts: Max recovery attempts on worker failure
90
+
91
+ Returns:
92
+ Decorated workflow function with schedule metadata
93
+
94
+ Raises:
95
+ ValueError: If no schedule specification provided
96
+
97
+ Examples:
98
+ # Every day at 9 AM
99
+ @scheduled_workflow(cron="0 9 * * *")
100
+ async def daily_report():
101
+ return await generate_report()
102
+
103
+ # Every 5 minutes with skip policy
104
+ @scheduled_workflow(
105
+ interval="5m",
106
+ overlap_policy=OverlapPolicy.SKIP,
107
+ )
108
+ async def health_check():
109
+ return await check_system_health()
110
+
111
+ # First of every month at midnight UTC
112
+ @scheduled_workflow(
113
+ calendar=[CalendarSpec(day_of_month=1, hour=0, minute=0)],
114
+ timezone="UTC",
115
+ )
116
+ async def monthly_billing():
117
+ return await process_billing()
118
+
119
+ # Complex schedule with workflow options
120
+ @scheduled_workflow(
121
+ cron="0 */4 * * *", # Every 4 hours
122
+ name="sync_data",
123
+ overlap_policy=OverlapPolicy.BUFFER_ONE,
124
+ recover_on_worker_loss=True,
125
+ max_recovery_attempts=5,
126
+ )
127
+ async def sync_external_data():
128
+ return await sync_data()
129
+ """
130
+ # Validate at least one schedule type is provided
131
+ if not cron and not interval and not calendar:
132
+ raise ValueError("scheduled_workflow requires at least one of: cron, interval, or calendar")
133
+
134
+ def decorator(func: Callable) -> Callable:
135
+ workflow_name = name or func.__name__
136
+
137
+ @functools.wraps(func)
138
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
139
+ return await func(*args, **kwargs)
140
+
141
+ # Register as a regular workflow first
142
+ register_workflow(
143
+ name=workflow_name,
144
+ func=wrapper,
145
+ original_func=func,
146
+ max_duration=max_duration,
147
+ )
148
+
149
+ # Store standard workflow metadata on wrapper
150
+ wrapper.__workflow__ = True # type: ignore[attr-defined]
151
+ wrapper.__workflow_name__ = workflow_name # type: ignore[attr-defined]
152
+ wrapper.__workflow_durable__ = durable # type: ignore[attr-defined]
153
+ wrapper.__workflow_max_duration__ = max_duration # type: ignore[attr-defined]
154
+ wrapper.__workflow_recover_on_worker_loss__ = recover_on_worker_loss # type: ignore[attr-defined]
155
+ wrapper.__workflow_max_recovery_attempts__ = max_recovery_attempts # type: ignore[attr-defined]
156
+
157
+ # Create schedule spec
158
+ spec = ScheduleSpec(
159
+ cron=cron,
160
+ interval=interval,
161
+ calendar=calendar,
162
+ timezone=timezone,
163
+ start_at=start_at,
164
+ end_at=end_at,
165
+ jitter=jitter,
166
+ )
167
+
168
+ # Store schedule metadata on wrapper
169
+ wrapper.__scheduled__ = True # type: ignore[attr-defined]
170
+ wrapper.__schedule_spec__ = spec # type: ignore[attr-defined]
171
+ wrapper.__overlap_policy__ = overlap_policy # type: ignore[attr-defined]
172
+
173
+ # Register in scheduled workflows registry
174
+ scheduled_meta = ScheduledWorkflowMetadata(
175
+ workflow_name=workflow_name,
176
+ spec=spec,
177
+ overlap_policy=overlap_policy,
178
+ func=wrapper,
179
+ )
180
+ _scheduled_workflows[workflow_name] = scheduled_meta
181
+
182
+ return wrapper
183
+
184
+ return decorator
185
+
186
+
187
+ def get_scheduled_workflow(name: str) -> ScheduledWorkflowMetadata | None:
188
+ """
189
+ Get scheduled workflow metadata by name.
190
+
191
+ Args:
192
+ name: Workflow name
193
+
194
+ Returns:
195
+ ScheduledWorkflowMetadata if found, None otherwise
196
+ """
197
+ return _scheduled_workflows.get(name)
198
+
199
+
200
+ def list_scheduled_workflows() -> dict[str, ScheduledWorkflowMetadata]:
201
+ """
202
+ List all registered scheduled workflows.
203
+
204
+ Returns:
205
+ Dictionary mapping workflow names to their schedule metadata
206
+ """
207
+ return _scheduled_workflows.copy()
208
+
209
+
210
+ def register_scheduled_workflow(
211
+ workflow_name: str,
212
+ spec: ScheduleSpec,
213
+ overlap_policy: OverlapPolicy,
214
+ func: Callable[..., Any],
215
+ ) -> None:
216
+ """
217
+ Manually register a scheduled workflow.
218
+
219
+ This is useful when you want to add scheduling to an existing workflow
220
+ without using the @scheduled_workflow decorator.
221
+
222
+ Args:
223
+ workflow_name: Name of the workflow to schedule
224
+ spec: Schedule specification
225
+ overlap_policy: How to handle overlapping runs
226
+ func: The workflow function
227
+
228
+ Examples:
229
+ from pyworkflow import workflow
230
+ from pyworkflow.core.scheduled import register_scheduled_workflow
231
+
232
+ @workflow
233
+ async def my_workflow():
234
+ pass
235
+
236
+ # Add scheduling later
237
+ register_scheduled_workflow(
238
+ "my_workflow",
239
+ ScheduleSpec(cron="0 9 * * *"),
240
+ OverlapPolicy.SKIP,
241
+ my_workflow,
242
+ )
243
+ """
244
+ scheduled_meta = ScheduledWorkflowMetadata(
245
+ workflow_name=workflow_name,
246
+ spec=spec,
247
+ overlap_policy=overlap_policy,
248
+ func=func,
249
+ )
250
+ _scheduled_workflows[workflow_name] = scheduled_meta
251
+
252
+
253
+ def unregister_scheduled_workflow(workflow_name: str) -> bool:
254
+ """
255
+ Unregister a scheduled workflow.
256
+
257
+ Args:
258
+ workflow_name: Name of the workflow to unregister
259
+
260
+ Returns:
261
+ True if workflow was unregistered, False if not found
262
+ """
263
+ if workflow_name in _scheduled_workflows:
264
+ del _scheduled_workflows[workflow_name]
265
+ return True
266
+ return False
267
+
268
+
269
+ def clear_scheduled_workflows() -> None:
270
+ """Clear all scheduled workflow registrations (useful for testing)."""
271
+ _scheduled_workflows.clear()
272
+
273
+
274
+ async def activate_scheduled_workflows(
275
+ storage: Any = None,
276
+ schedule_id_prefix: str = "auto_",
277
+ ) -> list[str]:
278
+ """
279
+ Activate all registered scheduled workflows by creating schedules in storage.
280
+
281
+ This function takes all workflows decorated with @scheduled_workflow and
282
+ creates corresponding schedules in the storage backend.
283
+
284
+ Args:
285
+ storage: Storage backend (uses global config if not provided)
286
+ schedule_id_prefix: Prefix for generated schedule IDs
287
+
288
+ Returns:
289
+ List of created schedule IDs
290
+
291
+ Examples:
292
+ from pyworkflow.core.scheduled import activate_scheduled_workflows
293
+
294
+ # Activate all @scheduled_workflow decorated functions
295
+ schedule_ids = await activate_scheduled_workflows()
296
+ print(f"Created {len(schedule_ids)} schedules")
297
+ """
298
+ if storage is None:
299
+ from pyworkflow.config import get_config
300
+
301
+ storage = get_config().storage
302
+
303
+ if storage is None:
304
+ raise ValueError("Storage backend required to activate scheduled workflows")
305
+
306
+ from pyworkflow.primitives.schedule import create_schedule
307
+
308
+ created_ids: list[str] = []
309
+
310
+ for workflow_name, meta in _scheduled_workflows.items():
311
+ schedule_id = f"{schedule_id_prefix}{workflow_name}"
312
+
313
+ # Check if schedule already exists
314
+ existing = await storage.get_schedule(schedule_id)
315
+ if existing:
316
+ # Skip if already exists
317
+ continue
318
+
319
+ schedule = await create_schedule(
320
+ workflow_name=workflow_name,
321
+ spec=meta.spec,
322
+ overlap_policy=meta.overlap_policy,
323
+ schedule_id=schedule_id,
324
+ storage=storage,
325
+ )
326
+ created_ids.append(schedule.schedule_id)
327
+
328
+ return created_ids