pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.9__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 (145) hide show
  1. pyworkflow/__init__.py +10 -1
  2. pyworkflow/celery/tasks.py +272 -24
  3. pyworkflow/cli/__init__.py +4 -1
  4. pyworkflow/cli/commands/runs.py +4 -4
  5. pyworkflow/cli/commands/setup.py +203 -4
  6. pyworkflow/cli/utils/config_generator.py +76 -3
  7. pyworkflow/cli/utils/docker_manager.py +232 -0
  8. pyworkflow/context/__init__.py +13 -0
  9. pyworkflow/context/base.py +26 -0
  10. pyworkflow/context/local.py +80 -0
  11. pyworkflow/context/step_context.py +295 -0
  12. pyworkflow/core/registry.py +6 -1
  13. pyworkflow/core/step.py +141 -0
  14. pyworkflow/core/workflow.py +56 -0
  15. pyworkflow/engine/events.py +30 -0
  16. pyworkflow/engine/replay.py +39 -0
  17. pyworkflow/primitives/child_workflow.py +1 -1
  18. pyworkflow/runtime/local.py +1 -1
  19. pyworkflow/storage/__init__.py +14 -0
  20. pyworkflow/storage/base.py +35 -0
  21. pyworkflow/storage/cassandra.py +1747 -0
  22. pyworkflow/storage/config.py +69 -0
  23. pyworkflow/storage/dynamodb.py +31 -2
  24. pyworkflow/storage/file.py +28 -0
  25. pyworkflow/storage/memory.py +18 -0
  26. pyworkflow/storage/mysql.py +1159 -0
  27. pyworkflow/storage/postgres.py +27 -2
  28. pyworkflow/storage/schemas.py +4 -3
  29. pyworkflow/storage/sqlite.py +25 -2
  30. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/METADATA +7 -4
  31. pyworkflow_engine-0.1.9.dist-info/RECORD +91 -0
  32. pyworkflow_engine-0.1.9.dist-info/top_level.txt +1 -0
  33. dashboard/backend/app/__init__.py +0 -1
  34. dashboard/backend/app/config.py +0 -32
  35. dashboard/backend/app/controllers/__init__.py +0 -6
  36. dashboard/backend/app/controllers/run_controller.py +0 -86
  37. dashboard/backend/app/controllers/workflow_controller.py +0 -33
  38. dashboard/backend/app/dependencies/__init__.py +0 -5
  39. dashboard/backend/app/dependencies/storage.py +0 -50
  40. dashboard/backend/app/repositories/__init__.py +0 -6
  41. dashboard/backend/app/repositories/run_repository.py +0 -80
  42. dashboard/backend/app/repositories/workflow_repository.py +0 -27
  43. dashboard/backend/app/rest/__init__.py +0 -8
  44. dashboard/backend/app/rest/v1/__init__.py +0 -12
  45. dashboard/backend/app/rest/v1/health.py +0 -33
  46. dashboard/backend/app/rest/v1/runs.py +0 -133
  47. dashboard/backend/app/rest/v1/workflows.py +0 -41
  48. dashboard/backend/app/schemas/__init__.py +0 -23
  49. dashboard/backend/app/schemas/common.py +0 -16
  50. dashboard/backend/app/schemas/event.py +0 -24
  51. dashboard/backend/app/schemas/hook.py +0 -25
  52. dashboard/backend/app/schemas/run.py +0 -54
  53. dashboard/backend/app/schemas/step.py +0 -28
  54. dashboard/backend/app/schemas/workflow.py +0 -31
  55. dashboard/backend/app/server.py +0 -87
  56. dashboard/backend/app/services/__init__.py +0 -6
  57. dashboard/backend/app/services/run_service.py +0 -240
  58. dashboard/backend/app/services/workflow_service.py +0 -155
  59. dashboard/backend/main.py +0 -18
  60. docs/concepts/cancellation.mdx +0 -362
  61. docs/concepts/continue-as-new.mdx +0 -434
  62. docs/concepts/events.mdx +0 -266
  63. docs/concepts/fault-tolerance.mdx +0 -370
  64. docs/concepts/hooks.mdx +0 -552
  65. docs/concepts/limitations.mdx +0 -167
  66. docs/concepts/schedules.mdx +0 -775
  67. docs/concepts/sleep.mdx +0 -312
  68. docs/concepts/steps.mdx +0 -301
  69. docs/concepts/workflows.mdx +0 -255
  70. docs/guides/cli.mdx +0 -942
  71. docs/guides/configuration.mdx +0 -560
  72. docs/introduction.mdx +0 -155
  73. docs/quickstart.mdx +0 -279
  74. examples/__init__.py +0 -1
  75. examples/celery/__init__.py +0 -1
  76. examples/celery/durable/docker-compose.yml +0 -55
  77. examples/celery/durable/pyworkflow.config.yaml +0 -12
  78. examples/celery/durable/workflows/__init__.py +0 -122
  79. examples/celery/durable/workflows/basic.py +0 -87
  80. examples/celery/durable/workflows/batch_processing.py +0 -102
  81. examples/celery/durable/workflows/cancellation.py +0 -273
  82. examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
  83. examples/celery/durable/workflows/child_workflows.py +0 -202
  84. examples/celery/durable/workflows/continue_as_new.py +0 -260
  85. examples/celery/durable/workflows/fault_tolerance.py +0 -210
  86. examples/celery/durable/workflows/hooks.py +0 -211
  87. examples/celery/durable/workflows/idempotency.py +0 -112
  88. examples/celery/durable/workflows/long_running.py +0 -99
  89. examples/celery/durable/workflows/retries.py +0 -101
  90. examples/celery/durable/workflows/schedules.py +0 -209
  91. examples/celery/transient/01_basic_workflow.py +0 -91
  92. examples/celery/transient/02_fault_tolerance.py +0 -257
  93. examples/celery/transient/__init__.py +0 -20
  94. examples/celery/transient/pyworkflow.config.yaml +0 -25
  95. examples/local/__init__.py +0 -1
  96. examples/local/durable/01_basic_workflow.py +0 -94
  97. examples/local/durable/02_file_storage.py +0 -132
  98. examples/local/durable/03_retries.py +0 -169
  99. examples/local/durable/04_long_running.py +0 -119
  100. examples/local/durable/05_event_log.py +0 -145
  101. examples/local/durable/06_idempotency.py +0 -148
  102. examples/local/durable/07_hooks.py +0 -334
  103. examples/local/durable/08_cancellation.py +0 -233
  104. examples/local/durable/09_child_workflows.py +0 -198
  105. examples/local/durable/10_child_workflow_patterns.py +0 -265
  106. examples/local/durable/11_continue_as_new.py +0 -249
  107. examples/local/durable/12_schedules.py +0 -198
  108. examples/local/durable/__init__.py +0 -1
  109. examples/local/transient/01_quick_tasks.py +0 -87
  110. examples/local/transient/02_retries.py +0 -130
  111. examples/local/transient/03_sleep.py +0 -141
  112. examples/local/transient/__init__.py +0 -1
  113. pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
  114. pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
  115. tests/examples/__init__.py +0 -0
  116. tests/integration/__init__.py +0 -0
  117. tests/integration/test_cancellation.py +0 -330
  118. tests/integration/test_child_workflows.py +0 -439
  119. tests/integration/test_continue_as_new.py +0 -428
  120. tests/integration/test_dynamodb_storage.py +0 -1146
  121. tests/integration/test_fault_tolerance.py +0 -369
  122. tests/integration/test_schedule_storage.py +0 -484
  123. tests/unit/__init__.py +0 -0
  124. tests/unit/backends/__init__.py +0 -1
  125. tests/unit/backends/test_dynamodb_storage.py +0 -1554
  126. tests/unit/backends/test_postgres_storage.py +0 -1281
  127. tests/unit/backends/test_sqlite_storage.py +0 -1460
  128. tests/unit/conftest.py +0 -41
  129. tests/unit/test_cancellation.py +0 -364
  130. tests/unit/test_child_workflows.py +0 -680
  131. tests/unit/test_continue_as_new.py +0 -441
  132. tests/unit/test_event_limits.py +0 -316
  133. tests/unit/test_executor.py +0 -320
  134. tests/unit/test_fault_tolerance.py +0 -334
  135. tests/unit/test_hooks.py +0 -495
  136. tests/unit/test_registry.py +0 -261
  137. tests/unit/test_replay.py +0 -420
  138. tests/unit/test_schedule_schemas.py +0 -285
  139. tests/unit/test_schedule_utils.py +0 -286
  140. tests/unit/test_scheduled_workflow.py +0 -274
  141. tests/unit/test_step.py +0 -353
  142. tests/unit/test_workflow.py +0 -243
  143. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/WHEEL +0 -0
  144. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/entry_points.txt +0 -0
  145. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/licenses/LICENSE +0 -0
pyworkflow/__init__.py CHANGED
@@ -29,7 +29,7 @@ Quick Start:
29
29
  >>> run_id = await start(my_workflow, "Alice")
30
30
  """
31
31
 
32
- __version__ = "0.1.7"
32
+ __version__ = "0.1.9"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -44,11 +44,15 @@ from pyworkflow.config import (
44
44
  from pyworkflow.context import (
45
45
  LocalContext,
46
46
  MockContext,
47
+ StepContext,
47
48
  WorkflowContext,
48
49
  get_context,
50
+ get_step_context,
49
51
  has_context,
52
+ has_step_context,
50
53
  reset_context,
51
54
  set_context,
55
+ set_step_context,
52
56
  )
53
57
 
54
58
  # Exceptions
@@ -224,6 +228,11 @@ __all__ = [
224
228
  "has_context",
225
229
  "set_context",
226
230
  "reset_context",
231
+ # Step context for distributed execution
232
+ "StepContext",
233
+ "get_step_context",
234
+ "has_step_context",
235
+ "set_step_context",
227
236
  # Registry
228
237
  "list_workflows",
229
238
  "get_workflow",
@@ -13,7 +13,10 @@ import asyncio
13
13
  import uuid
14
14
  from collections.abc import Callable
15
15
  from datetime import UTC, datetime
16
- from typing import Any
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from pyworkflow.context.step_context import StepContext
17
20
 
18
21
  from celery import Task
19
22
  from celery.exceptions import WorkerLostError
@@ -103,11 +106,16 @@ def execute_step_task(
103
106
  step_id: str,
104
107
  max_retries: int = 3,
105
108
  storage_config: dict[str, Any] | None = None,
109
+ context_data: dict[str, Any] | None = None,
110
+ context_class_name: str | None = None,
106
111
  ) -> Any:
107
112
  """
108
- Execute a workflow step in a Celery worker.
113
+ Execute a workflow step on a Celery worker.
109
114
 
110
- This task runs a single step and handles retries automatically.
115
+ This task:
116
+ 1. Executes the step function
117
+ 2. Records STEP_COMPLETED/STEP_FAILED event in storage
118
+ 3. Triggers workflow resumption via resume_workflow_task
111
119
 
112
120
  Args:
113
121
  step_name: Name of the step function
@@ -117,6 +125,8 @@ def execute_step_task(
117
125
  step_id: Step execution ID
118
126
  max_retries: Maximum retry attempts
119
127
  storage_config: Storage backend configuration
128
+ context_data: Optional step context data (from workflow)
129
+ context_class_name: Optional fully qualified context class name
120
130
 
121
131
  Returns:
122
132
  Step result (serialized)
@@ -128,7 +138,7 @@ def execute_step_task(
128
138
  from pyworkflow.core.registry import _registry
129
139
 
130
140
  logger.info(
131
- f"Executing step: {step_name}",
141
+ f"Executing dispatched step: {step_name}",
132
142
  run_id=run_id,
133
143
  step_id=step_id,
134
144
  attempt=self.request.retries + 1,
@@ -137,12 +147,49 @@ def execute_step_task(
137
147
  # Get step metadata
138
148
  step_meta = _registry.get_step(step_name)
139
149
  if not step_meta:
150
+ # Record failure and resume workflow
151
+ asyncio.run(
152
+ _record_step_failure_and_resume(
153
+ storage_config=storage_config,
154
+ run_id=run_id,
155
+ step_id=step_id,
156
+ step_name=step_name,
157
+ error=f"Step '{step_name}' not found in registry",
158
+ error_type="FatalError",
159
+ is_retryable=False,
160
+ )
161
+ )
140
162
  raise FatalError(f"Step '{step_name}' not found in registry")
141
163
 
142
164
  # Deserialize arguments
143
165
  args = deserialize_args(args_json)
144
166
  kwargs = deserialize_kwargs(kwargs_json)
145
167
 
168
+ # Set up step context if provided (read-only mode)
169
+ step_context_token = None
170
+ readonly_token = None
171
+
172
+ if context_data and context_class_name:
173
+ try:
174
+ from pyworkflow.context.step_context import (
175
+ _set_step_context_internal,
176
+ _set_step_context_readonly,
177
+ )
178
+
179
+ # Import context class dynamically
180
+ context_class = _resolve_context_class(context_class_name)
181
+ if context_class is not None:
182
+ step_ctx = context_class.from_dict(context_data)
183
+ step_context_token = _set_step_context_internal(step_ctx)
184
+ # Set readonly mode to prevent mutation in steps
185
+ readonly_token = _set_step_context_readonly(True)
186
+ except Exception as e:
187
+ logger.warning(
188
+ f"Failed to load step context: {e}",
189
+ run_id=run_id,
190
+ step_id=step_id,
191
+ )
192
+
146
193
  # Execute step function
147
194
  try:
148
195
  # Get the original function (unwrapped from decorator)
@@ -160,32 +207,225 @@ def execute_step_task(
160
207
  step_id=step_id,
161
208
  )
162
209
 
210
+ # Record STEP_COMPLETED event and trigger workflow resumption
211
+ asyncio.run(
212
+ _record_step_completion_and_resume(
213
+ storage_config=storage_config,
214
+ run_id=run_id,
215
+ step_id=step_id,
216
+ step_name=step_name,
217
+ result=result,
218
+ )
219
+ )
220
+
163
221
  return result
164
222
 
165
- except FatalError:
223
+ except FatalError as e:
166
224
  logger.error(f"Step failed (fatal): {step_name}", run_id=run_id, step_id=step_id)
225
+ # Record failure and resume workflow (workflow will fail on replay)
226
+ asyncio.run(
227
+ _record_step_failure_and_resume(
228
+ storage_config=storage_config,
229
+ run_id=run_id,
230
+ step_id=step_id,
231
+ step_name=step_name,
232
+ error=str(e),
233
+ error_type=type(e).__name__,
234
+ is_retryable=False,
235
+ )
236
+ )
167
237
  raise
168
238
 
169
239
  except RetryableError as e:
170
- logger.warning(
171
- f"Step failed (retriable): {step_name}",
172
- run_id=run_id,
173
- step_id=step_id,
174
- retry_after=e.retry_after,
175
- )
176
- # Let Celery handle the retry
177
- raise self.retry(exc=e, countdown=e.get_retry_delay_seconds() or 60)
240
+ # Check if we have retries left
241
+ if self.request.retries < max_retries:
242
+ logger.warning(
243
+ f"Step failed (retriable): {step_name}, retrying...",
244
+ run_id=run_id,
245
+ step_id=step_id,
246
+ retry_after=e.retry_after,
247
+ attempt=self.request.retries + 1,
248
+ max_retries=max_retries,
249
+ )
250
+ # Let Celery handle the retry - don't resume workflow yet
251
+ raise self.retry(exc=e, countdown=e.get_retry_delay_seconds() or 60)
252
+ else:
253
+ # Max retries exhausted - record failure and resume workflow
254
+ logger.error(
255
+ f"Step failed after {max_retries + 1} attempts: {step_name}",
256
+ run_id=run_id,
257
+ step_id=step_id,
258
+ )
259
+ asyncio.run(
260
+ _record_step_failure_and_resume(
261
+ storage_config=storage_config,
262
+ run_id=run_id,
263
+ step_id=step_id,
264
+ step_name=step_name,
265
+ error=str(e),
266
+ error_type=type(e).__name__,
267
+ is_retryable=False, # Mark as not retryable since we exhausted retries
268
+ )
269
+ )
270
+ raise
178
271
 
179
272
  except Exception as e:
180
- logger.error(
181
- f"Step failed (unexpected): {step_name}",
182
- run_id=run_id,
183
- step_id=step_id,
184
- error=str(e),
185
- exc_info=True,
186
- )
187
- # Treat unexpected errors as retriable
188
- raise self.retry(exc=RetryableError(str(e)), countdown=60)
273
+ # Check if we have retries left
274
+ if self.request.retries < max_retries:
275
+ logger.warning(
276
+ f"Step failed (unexpected): {step_name}, retrying...",
277
+ run_id=run_id,
278
+ step_id=step_id,
279
+ error=str(e),
280
+ attempt=self.request.retries + 1,
281
+ )
282
+ # Treat unexpected errors as retriable
283
+ raise self.retry(exc=RetryableError(str(e)), countdown=60)
284
+ else:
285
+ # Max retries exhausted
286
+ logger.error(
287
+ f"Step failed after {max_retries + 1} attempts: {step_name}",
288
+ run_id=run_id,
289
+ step_id=step_id,
290
+ error=str(e),
291
+ exc_info=True,
292
+ )
293
+ asyncio.run(
294
+ _record_step_failure_and_resume(
295
+ storage_config=storage_config,
296
+ run_id=run_id,
297
+ step_id=step_id,
298
+ step_name=step_name,
299
+ error=str(e),
300
+ error_type=type(e).__name__,
301
+ is_retryable=False,
302
+ )
303
+ )
304
+ raise
305
+
306
+ finally:
307
+ # Clean up step context
308
+ if readonly_token is not None:
309
+ from pyworkflow.context.step_context import _reset_step_context_readonly
310
+
311
+ _reset_step_context_readonly(readonly_token)
312
+ if step_context_token is not None:
313
+ from pyworkflow.context.step_context import _reset_step_context
314
+
315
+ _reset_step_context(step_context_token)
316
+
317
+
318
+ async def _record_step_completion_and_resume(
319
+ storage_config: dict[str, Any] | None,
320
+ run_id: str,
321
+ step_id: str,
322
+ step_name: str,
323
+ result: Any,
324
+ ) -> None:
325
+ """
326
+ Record STEP_COMPLETED event and trigger workflow resumption.
327
+
328
+ Called by execute_step_task after successful step execution.
329
+ """
330
+ from pyworkflow.engine.events import create_step_completed_event
331
+ from pyworkflow.serialization.encoder import serialize
332
+
333
+ # Get storage backend
334
+ storage = _get_storage_backend(storage_config)
335
+
336
+ # Ensure storage is connected
337
+ if hasattr(storage, "connect"):
338
+ await storage.connect()
339
+
340
+ # Record STEP_COMPLETED event
341
+ completion_event = create_step_completed_event(
342
+ run_id=run_id,
343
+ step_id=step_id,
344
+ result=serialize(result),
345
+ step_name=step_name,
346
+ )
347
+ await storage.record_event(completion_event)
348
+
349
+ # Schedule workflow resumption immediately
350
+ schedule_workflow_resumption(run_id, datetime.now(UTC), storage_config)
351
+
352
+ logger.info(
353
+ "Step completed and workflow resumption scheduled",
354
+ run_id=run_id,
355
+ step_id=step_id,
356
+ step_name=step_name,
357
+ )
358
+
359
+
360
+ async def _record_step_failure_and_resume(
361
+ storage_config: dict[str, Any] | None,
362
+ run_id: str,
363
+ step_id: str,
364
+ step_name: str,
365
+ error: str,
366
+ error_type: str,
367
+ is_retryable: bool,
368
+ ) -> None:
369
+ """
370
+ Record STEP_FAILED event and trigger workflow resumption.
371
+
372
+ Called by execute_step_task after step failure (when retries are exhausted).
373
+ The workflow will fail when it replays and sees the failure event.
374
+ """
375
+ from pyworkflow.engine.events import create_step_failed_event
376
+
377
+ # Get storage backend
378
+ storage = _get_storage_backend(storage_config)
379
+
380
+ # Ensure storage is connected
381
+ if hasattr(storage, "connect"):
382
+ await storage.connect()
383
+
384
+ # Record STEP_FAILED event
385
+ failure_event = create_step_failed_event(
386
+ run_id=run_id,
387
+ step_id=step_id,
388
+ error=error,
389
+ error_type=error_type,
390
+ is_retryable=is_retryable,
391
+ attempt=1, # Final attempt
392
+ )
393
+ await storage.record_event(failure_event)
394
+
395
+ # Schedule workflow resumption - workflow will fail on replay
396
+ schedule_workflow_resumption(run_id, datetime.now(UTC), storage_config)
397
+
398
+ logger.info(
399
+ "Step failed and workflow resumption scheduled",
400
+ run_id=run_id,
401
+ step_id=step_id,
402
+ step_name=step_name,
403
+ error=error,
404
+ )
405
+
406
+
407
+ def _resolve_context_class(class_name: str) -> type["StepContext"] | None:
408
+ """
409
+ Resolve a context class from its fully qualified name.
410
+
411
+ Args:
412
+ class_name: Fully qualified class name (e.g., "myapp.contexts.OrderContext")
413
+
414
+ Returns:
415
+ The class type, or None if resolution fails
416
+ """
417
+ try:
418
+ import importlib
419
+
420
+ parts = class_name.rsplit(".", 1)
421
+ if len(parts) == 2:
422
+ module_name, cls_name = parts
423
+ module = importlib.import_module(module_name)
424
+ return getattr(module, cls_name, None)
425
+ # Simple class name - try to get from globals
426
+ return None
427
+ except Exception:
428
+ return None
189
429
 
190
430
 
191
431
  @celery_app.task(
@@ -365,6 +605,8 @@ async def _execute_child_workflow_on_worker(
365
605
  storage=storage,
366
606
  durable=True,
367
607
  event_log=None, # Fresh execution
608
+ runtime="celery",
609
+ storage_config=storage_config,
368
610
  )
369
611
 
370
612
  # Update status to COMPLETED
@@ -714,6 +956,8 @@ async def _recover_workflow_on_worker(
714
956
  args=args,
715
957
  kwargs=kwargs,
716
958
  event_log=events,
959
+ runtime="celery",
960
+ storage_config=storage_config,
717
961
  )
718
962
 
719
963
  # Update run status to completed
@@ -949,7 +1193,7 @@ async def _start_workflow_on_worker(
949
1193
  input_kwargs=serialize_kwargs(**kwargs),
950
1194
  idempotency_key=idempotency_key,
951
1195
  max_duration=workflow_meta.max_duration,
952
- metadata={}, # Run-level metadata (not from decorator)
1196
+ context={}, # Step context (not from decorator)
953
1197
  recovery_attempts=0,
954
1198
  max_recovery_attempts=max_recovery_attempts,
955
1199
  recover_on_worker_loss=recover_on_worker_loss,
@@ -977,6 +1221,8 @@ async def _start_workflow_on_worker(
977
1221
  storage=storage,
978
1222
  args=args,
979
1223
  kwargs=kwargs,
1224
+ runtime="celery",
1225
+ storage_config=storage_config,
980
1226
  )
981
1227
 
982
1228
  # Update run status to completed
@@ -1231,7 +1477,7 @@ async def _execute_scheduled_workflow(
1231
1477
  kwargs_json=kwargs_json,
1232
1478
  run_id=run_id,
1233
1479
  storage_config=storage_config,
1234
- metadata={"schedule_id": schedule_id, "scheduled_time": scheduled_time.isoformat()},
1480
+ # Note: context data is passed through for scheduled workflows to include schedule info
1235
1481
  )
1236
1482
 
1237
1483
  # Record trigger event - use schedule_id as run_id since workflow run may not exist yet
@@ -1381,6 +1627,8 @@ async def _resume_workflow_on_worker(
1381
1627
  kwargs=kwargs,
1382
1628
  event_log=events,
1383
1629
  cancellation_requested=cancellation_requested,
1630
+ runtime="celery",
1631
+ storage_config=storage_config,
1384
1632
  )
1385
1633
 
1386
1634
  # Update run status to completed
@@ -27,7 +27,10 @@ from pyworkflow.cli.utils.storage import create_storage
27
27
  )
28
28
  @click.option(
29
29
  "--storage",
30
- type=click.Choice(["file", "memory", "sqlite", "dynamodb"], case_sensitive=False),
30
+ type=click.Choice(
31
+ ["file", "memory", "sqlite", "postgres", "mysql", "dynamodb", "cassandra"],
32
+ case_sensitive=False,
33
+ ),
31
34
  envvar="PYWORKFLOW_STORAGE_BACKEND",
32
35
  help="Storage backend type (default: file)",
33
36
  )
@@ -227,7 +227,7 @@ async def run_status(ctx: click.Context, run_id: str) -> None:
227
227
  "input_kwargs": json.loads(run.input_kwargs) if run.input_kwargs else None,
228
228
  "result": json.loads(run.result) if run.result else None,
229
229
  "error": run.error,
230
- "metadata": run.metadata,
230
+ "context": run.context,
231
231
  }
232
232
  format_json(data)
233
233
 
@@ -266,9 +266,9 @@ async def run_status(ctx: click.Context, run_id: str) -> None:
266
266
  if run.error:
267
267
  data["Error"] = run.error
268
268
 
269
- # Add metadata if present
270
- if run.metadata:
271
- data["Metadata"] = json.dumps(run.metadata, indent=2)
269
+ # Add context if present
270
+ if run.context:
271
+ data["Context"] = json.dumps(run.context, indent=2)
272
272
 
273
273
  format_key_value(data, title=f"Workflow Run: {run_id}")
274
274