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
@@ -73,6 +73,32 @@ def storage_to_config(storage: StorageBackend | None) -> dict[str, Any] | None:
73
73
  "region": getattr(storage, "region", "us-east-1"),
74
74
  "endpoint_url": getattr(storage, "endpoint_url", None),
75
75
  }
76
+ elif class_name == "CassandraStorageBackend":
77
+ return {
78
+ "type": "cassandra",
79
+ "contact_points": getattr(storage, "contact_points", ["localhost"]),
80
+ "port": getattr(storage, "port", 9042),
81
+ "keyspace": getattr(storage, "keyspace", "pyworkflow"),
82
+ "username": getattr(storage, "username", None),
83
+ "password": getattr(storage, "password", None),
84
+ "read_consistency": getattr(storage, "read_consistency", "LOCAL_QUORUM"),
85
+ "write_consistency": getattr(storage, "write_consistency", "LOCAL_QUORUM"),
86
+ "replication_strategy": getattr(storage, "replication_strategy", "SimpleStrategy"),
87
+ "replication_factor": getattr(storage, "replication_factor", 3),
88
+ "datacenter": getattr(storage, "datacenter", None),
89
+ }
90
+ elif class_name == "MySQLStorageBackend":
91
+ config = {"type": "mysql"}
92
+ dsn = getattr(storage, "dsn", None)
93
+ if dsn:
94
+ config["dsn"] = dsn
95
+ else:
96
+ config["host"] = getattr(storage, "host", "localhost")
97
+ config["port"] = getattr(storage, "port", 3306)
98
+ config["user"] = getattr(storage, "user", "pyworkflow")
99
+ config["password"] = getattr(storage, "password", "")
100
+ config["database"] = getattr(storage, "database", "pyworkflow")
101
+ return config
76
102
  else:
77
103
  # Unknown backend - return minimal config
78
104
  return {"type": "unknown"}
@@ -181,5 +207,48 @@ def config_to_storage(config: dict[str, Any] | None = None) -> StorageBackend:
181
207
  endpoint_url=config.get("endpoint_url"),
182
208
  )
183
209
 
210
+ elif storage_type == "cassandra":
211
+ try:
212
+ from pyworkflow.storage.cassandra import CassandraStorageBackend
213
+ except ImportError:
214
+ raise ValueError(
215
+ "Cassandra storage backend is not available. "
216
+ "Please install the required dependencies with: pip install 'pyworkflow[cassandra]'"
217
+ )
218
+
219
+ return CassandraStorageBackend(
220
+ contact_points=config.get("contact_points", ["localhost"]),
221
+ port=config.get("port", 9042),
222
+ keyspace=config.get("keyspace", "pyworkflow"),
223
+ username=config.get("username"),
224
+ password=config.get("password"),
225
+ read_consistency=config.get("read_consistency", "LOCAL_QUORUM"),
226
+ write_consistency=config.get("write_consistency", "LOCAL_QUORUM"),
227
+ replication_strategy=config.get("replication_strategy", "SimpleStrategy"),
228
+ replication_factor=config.get("replication_factor", 3),
229
+ datacenter=config.get("datacenter"),
230
+ )
231
+
232
+ elif storage_type == "mysql":
233
+ try:
234
+ from pyworkflow.storage.mysql import MySQLStorageBackend
235
+ except ImportError:
236
+ raise ValueError(
237
+ "MySQL storage backend is not available. "
238
+ "Please install the required dependencies with: pip install 'pyworkflow[mysql]'"
239
+ )
240
+
241
+ # Support both DSN and individual parameters
242
+ if "dsn" in config:
243
+ return MySQLStorageBackend(dsn=config["dsn"])
244
+ else:
245
+ return MySQLStorageBackend(
246
+ host=config.get("host", "localhost"),
247
+ port=config.get("port", 3306),
248
+ user=config.get("user", "pyworkflow"),
249
+ password=config.get("password", ""),
250
+ database=config.get("database", "pyworkflow"),
251
+ )
252
+
184
253
  else:
185
254
  raise ValueError(f"Unknown storage type: {storage_type}")
@@ -234,7 +234,7 @@ class DynamoDBStorageBackend(StorageBackend):
234
234
  "error": run.error,
235
235
  "idempotency_key": run.idempotency_key,
236
236
  "max_duration": run.max_duration,
237
- "metadata": json.dumps(run.metadata),
237
+ "context": json.dumps(run.context),
238
238
  "recovery_attempts": run.recovery_attempts,
239
239
  "max_recovery_attempts": run.max_recovery_attempts,
240
240
  "recover_on_worker_loss": run.recover_on_worker_loss,
@@ -362,6 +362,34 @@ class DynamoDBStorageBackend(StorageBackend):
362
362
  },
363
363
  )
364
364
 
365
+ async def update_run_context(
366
+ self,
367
+ run_id: str,
368
+ context: dict,
369
+ ) -> None:
370
+ """Update the step context for a workflow run."""
371
+ async with self._get_client() as client:
372
+ await client.update_item(
373
+ TableName=self.table_name,
374
+ Key={
375
+ "PK": {"S": f"RUN#{run_id}"},
376
+ "SK": {"S": "#METADATA"},
377
+ },
378
+ UpdateExpression="SET #ctx = :ctx, updated_at = :now",
379
+ ExpressionAttributeNames={
380
+ "#ctx": "context",
381
+ },
382
+ ExpressionAttributeValues={
383
+ ":ctx": {"S": json.dumps(context)},
384
+ ":now": {"S": datetime.now(UTC).isoformat()},
385
+ },
386
+ )
387
+
388
+ async def get_run_context(self, run_id: str) -> dict:
389
+ """Get the current step context for a workflow run."""
390
+ run = await self.get_run(run_id)
391
+ return run.context if run else {}
392
+
365
393
  async def list_runs(
366
394
  self,
367
395
  query: str | None = None,
@@ -1221,7 +1249,8 @@ class DynamoDBStorageBackend(StorageBackend):
1221
1249
  error=item.get("error"),
1222
1250
  idempotency_key=item.get("idempotency_key"),
1223
1251
  max_duration=item.get("max_duration"),
1224
- metadata=json.loads(item.get("metadata", "{}")),
1252
+ # Support both 'context' (new) and 'metadata' (legacy) keys
1253
+ context=json.loads(item.get("context", item.get("metadata", "{}"))),
1225
1254
  recovery_attempts=item.get("recovery_attempts", 0),
1226
1255
  max_recovery_attempts=item.get("max_recovery_attempts", 3),
1227
1256
  recover_on_worker_loss=item.get("recover_on_worker_loss", True),
@@ -190,6 +190,34 @@ class FileStorageBackend(StorageBackend):
190
190
 
191
191
  await asyncio.to_thread(_update)
192
192
 
193
+ async def update_run_context(
194
+ self,
195
+ run_id: str,
196
+ context: dict,
197
+ ) -> None:
198
+ """Update the step context for a workflow run."""
199
+ run_file = self.runs_dir / f"{run_id}.json"
200
+
201
+ if not run_file.exists():
202
+ raise ValueError(f"Workflow run {run_id} not found")
203
+
204
+ lock_file = self.locks_dir / f"{run_id}.lock"
205
+ lock = FileLock(str(lock_file))
206
+
207
+ def _update() -> None:
208
+ with lock:
209
+ data = json.loads(run_file.read_text())
210
+ data["context"] = context
211
+ data["updated_at"] = datetime.now(UTC).isoformat()
212
+ run_file.write_text(json.dumps(data, indent=2))
213
+
214
+ await asyncio.to_thread(_update)
215
+
216
+ async def get_run_context(self, run_id: str) -> dict:
217
+ """Get the current step context for a workflow run."""
218
+ run = await self.get_run(run_id)
219
+ return run.context if run else {}
220
+
193
221
  async def list_runs(
194
222
  self,
195
223
  query: str | None = None,
@@ -109,6 +109,24 @@ class InMemoryStorageBackend(StorageBackend):
109
109
  run.recovery_attempts = recovery_attempts
110
110
  run.updated_at = datetime.now(UTC)
111
111
 
112
+ async def update_run_context(
113
+ self,
114
+ run_id: str,
115
+ context: dict,
116
+ ) -> None:
117
+ """Update the step context for a workflow run."""
118
+ with self._lock:
119
+ run = self._runs.get(run_id)
120
+ if run:
121
+ run.context = context
122
+ run.updated_at = datetime.now(UTC)
123
+
124
+ async def get_run_context(self, run_id: str) -> dict:
125
+ """Get the current step context for a workflow run."""
126
+ with self._lock:
127
+ run = self._runs.get(run_id)
128
+ return run.context if run else {}
129
+
112
130
  async def list_runs(
113
131
  self,
114
132
  query: str | None = None,