pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.10__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 (146) 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/config.py +94 -17
  9. pyworkflow/context/__init__.py +13 -0
  10. pyworkflow/context/base.py +26 -0
  11. pyworkflow/context/local.py +80 -0
  12. pyworkflow/context/step_context.py +295 -0
  13. pyworkflow/core/registry.py +6 -1
  14. pyworkflow/core/step.py +141 -0
  15. pyworkflow/core/workflow.py +56 -0
  16. pyworkflow/engine/events.py +30 -0
  17. pyworkflow/engine/replay.py +39 -0
  18. pyworkflow/primitives/child_workflow.py +1 -1
  19. pyworkflow/runtime/local.py +1 -1
  20. pyworkflow/storage/__init__.py +14 -0
  21. pyworkflow/storage/base.py +35 -0
  22. pyworkflow/storage/cassandra.py +1747 -0
  23. pyworkflow/storage/config.py +69 -0
  24. pyworkflow/storage/dynamodb.py +31 -2
  25. pyworkflow/storage/file.py +28 -0
  26. pyworkflow/storage/memory.py +18 -0
  27. pyworkflow/storage/mysql.py +1159 -0
  28. pyworkflow/storage/postgres.py +27 -2
  29. pyworkflow/storage/schemas.py +4 -3
  30. pyworkflow/storage/sqlite.py +25 -2
  31. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/METADATA +7 -4
  32. pyworkflow_engine-0.1.10.dist-info/RECORD +91 -0
  33. pyworkflow_engine-0.1.10.dist-info/top_level.txt +1 -0
  34. dashboard/backend/app/__init__.py +0 -1
  35. dashboard/backend/app/config.py +0 -32
  36. dashboard/backend/app/controllers/__init__.py +0 -6
  37. dashboard/backend/app/controllers/run_controller.py +0 -86
  38. dashboard/backend/app/controllers/workflow_controller.py +0 -33
  39. dashboard/backend/app/dependencies/__init__.py +0 -5
  40. dashboard/backend/app/dependencies/storage.py +0 -50
  41. dashboard/backend/app/repositories/__init__.py +0 -6
  42. dashboard/backend/app/repositories/run_repository.py +0 -80
  43. dashboard/backend/app/repositories/workflow_repository.py +0 -27
  44. dashboard/backend/app/rest/__init__.py +0 -8
  45. dashboard/backend/app/rest/v1/__init__.py +0 -12
  46. dashboard/backend/app/rest/v1/health.py +0 -33
  47. dashboard/backend/app/rest/v1/runs.py +0 -133
  48. dashboard/backend/app/rest/v1/workflows.py +0 -41
  49. dashboard/backend/app/schemas/__init__.py +0 -23
  50. dashboard/backend/app/schemas/common.py +0 -16
  51. dashboard/backend/app/schemas/event.py +0 -24
  52. dashboard/backend/app/schemas/hook.py +0 -25
  53. dashboard/backend/app/schemas/run.py +0 -54
  54. dashboard/backend/app/schemas/step.py +0 -28
  55. dashboard/backend/app/schemas/workflow.py +0 -31
  56. dashboard/backend/app/server.py +0 -87
  57. dashboard/backend/app/services/__init__.py +0 -6
  58. dashboard/backend/app/services/run_service.py +0 -240
  59. dashboard/backend/app/services/workflow_service.py +0 -155
  60. dashboard/backend/main.py +0 -18
  61. docs/concepts/cancellation.mdx +0 -362
  62. docs/concepts/continue-as-new.mdx +0 -434
  63. docs/concepts/events.mdx +0 -266
  64. docs/concepts/fault-tolerance.mdx +0 -370
  65. docs/concepts/hooks.mdx +0 -552
  66. docs/concepts/limitations.mdx +0 -167
  67. docs/concepts/schedules.mdx +0 -775
  68. docs/concepts/sleep.mdx +0 -312
  69. docs/concepts/steps.mdx +0 -301
  70. docs/concepts/workflows.mdx +0 -255
  71. docs/guides/cli.mdx +0 -942
  72. docs/guides/configuration.mdx +0 -560
  73. docs/introduction.mdx +0 -155
  74. docs/quickstart.mdx +0 -279
  75. examples/__init__.py +0 -1
  76. examples/celery/__init__.py +0 -1
  77. examples/celery/durable/docker-compose.yml +0 -55
  78. examples/celery/durable/pyworkflow.config.yaml +0 -12
  79. examples/celery/durable/workflows/__init__.py +0 -122
  80. examples/celery/durable/workflows/basic.py +0 -87
  81. examples/celery/durable/workflows/batch_processing.py +0 -102
  82. examples/celery/durable/workflows/cancellation.py +0 -273
  83. examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
  84. examples/celery/durable/workflows/child_workflows.py +0 -202
  85. examples/celery/durable/workflows/continue_as_new.py +0 -260
  86. examples/celery/durable/workflows/fault_tolerance.py +0 -210
  87. examples/celery/durable/workflows/hooks.py +0 -211
  88. examples/celery/durable/workflows/idempotency.py +0 -112
  89. examples/celery/durable/workflows/long_running.py +0 -99
  90. examples/celery/durable/workflows/retries.py +0 -101
  91. examples/celery/durable/workflows/schedules.py +0 -209
  92. examples/celery/transient/01_basic_workflow.py +0 -91
  93. examples/celery/transient/02_fault_tolerance.py +0 -257
  94. examples/celery/transient/__init__.py +0 -20
  95. examples/celery/transient/pyworkflow.config.yaml +0 -25
  96. examples/local/__init__.py +0 -1
  97. examples/local/durable/01_basic_workflow.py +0 -94
  98. examples/local/durable/02_file_storage.py +0 -132
  99. examples/local/durable/03_retries.py +0 -169
  100. examples/local/durable/04_long_running.py +0 -119
  101. examples/local/durable/05_event_log.py +0 -145
  102. examples/local/durable/06_idempotency.py +0 -148
  103. examples/local/durable/07_hooks.py +0 -334
  104. examples/local/durable/08_cancellation.py +0 -233
  105. examples/local/durable/09_child_workflows.py +0 -198
  106. examples/local/durable/10_child_workflow_patterns.py +0 -265
  107. examples/local/durable/11_continue_as_new.py +0 -249
  108. examples/local/durable/12_schedules.py +0 -198
  109. examples/local/durable/__init__.py +0 -1
  110. examples/local/transient/01_quick_tasks.py +0 -87
  111. examples/local/transient/02_retries.py +0 -130
  112. examples/local/transient/03_sleep.py +0 -141
  113. examples/local/transient/__init__.py +0 -1
  114. pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
  115. pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
  116. tests/examples/__init__.py +0 -0
  117. tests/integration/__init__.py +0 -0
  118. tests/integration/test_cancellation.py +0 -330
  119. tests/integration/test_child_workflows.py +0 -439
  120. tests/integration/test_continue_as_new.py +0 -428
  121. tests/integration/test_dynamodb_storage.py +0 -1146
  122. tests/integration/test_fault_tolerance.py +0 -369
  123. tests/integration/test_schedule_storage.py +0 -484
  124. tests/unit/__init__.py +0 -0
  125. tests/unit/backends/__init__.py +0 -1
  126. tests/unit/backends/test_dynamodb_storage.py +0 -1554
  127. tests/unit/backends/test_postgres_storage.py +0 -1281
  128. tests/unit/backends/test_sqlite_storage.py +0 -1460
  129. tests/unit/conftest.py +0 -41
  130. tests/unit/test_cancellation.py +0 -364
  131. tests/unit/test_child_workflows.py +0 -680
  132. tests/unit/test_continue_as_new.py +0 -441
  133. tests/unit/test_event_limits.py +0 -316
  134. tests/unit/test_executor.py +0 -320
  135. tests/unit/test_fault_tolerance.py +0 -334
  136. tests/unit/test_hooks.py +0 -495
  137. tests/unit/test_registry.py +0 -261
  138. tests/unit/test_replay.py +0 -420
  139. tests/unit/test_schedule_schemas.py +0 -285
  140. tests/unit/test_schedule_utils.py +0 -286
  141. tests/unit/test_scheduled_workflow.py +0 -274
  142. tests/unit/test_step.py +0 -353
  143. tests/unit/test_workflow.py +0 -243
  144. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/WHEEL +0 -0
  145. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/entry_points.txt +0 -0
  146. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.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,