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
@@ -195,6 +195,131 @@ volumes:
195
195
  return template
196
196
 
197
197
 
198
+ def generate_cassandra_docker_compose_content(
199
+ cassandra_host: str = "cassandra",
200
+ cassandra_port: int = 9042,
201
+ cassandra_keyspace: str = "pyworkflow",
202
+ cassandra_user: str | None = None,
203
+ cassandra_password: str | None = None,
204
+ ) -> str:
205
+ """
206
+ Generate docker-compose.yml content for Cassandra-based PyWorkflow services.
207
+
208
+ Args:
209
+ cassandra_host: Cassandra host (container name for docker networking)
210
+ cassandra_port: Cassandra CQL native transport port
211
+ cassandra_keyspace: Cassandra keyspace name
212
+ cassandra_user: Optional Cassandra user (for authentication)
213
+ cassandra_password: Optional Cassandra password (for authentication)
214
+
215
+ Returns:
216
+ docker-compose.yml content as string
217
+
218
+ Example:
219
+ >>> compose_content = generate_cassandra_docker_compose_content()
220
+ """
221
+ # Build environment variables for dashboard
222
+ dashboard_env = f""" - DASHBOARD_STORAGE_TYPE=cassandra
223
+ - DASHBOARD_CASSANDRA_HOST={cassandra_host}
224
+ - DASHBOARD_CASSANDRA_PORT=9042
225
+ - DASHBOARD_CASSANDRA_KEYSPACE={cassandra_keyspace}"""
226
+
227
+ if cassandra_user and cassandra_password:
228
+ dashboard_env += f"""
229
+ - DASHBOARD_CASSANDRA_USER={cassandra_user}
230
+ - DASHBOARD_CASSANDRA_PASSWORD={cassandra_password}"""
231
+
232
+ # Cassandra environment for authentication
233
+ cassandra_env = ""
234
+ if cassandra_user and cassandra_password:
235
+ cassandra_env = f"""
236
+ - CASSANDRA_AUTHENTICATOR=PasswordAuthenticator
237
+ - CASSANDRA_AUTHORIZER=CassandraAuthorizer
238
+ - CASSANDRA_USER={cassandra_user}
239
+ - CASSANDRA_PASSWORD={cassandra_password}"""
240
+
241
+ template = f"""services:
242
+ cassandra:
243
+ image: cassandra:4.1
244
+ container_name: pyworkflow-cassandra
245
+ ports:
246
+ - "{cassandra_port}:9042"
247
+ environment:
248
+ - CASSANDRA_CLUSTER_NAME=pyworkflow-cluster
249
+ - CASSANDRA_DC=dc1
250
+ - CASSANDRA_RACK=rack1
251
+ - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch
252
+ - MAX_HEAP_SIZE=512M
253
+ - HEAP_NEWSIZE=128M{cassandra_env}
254
+ volumes:
255
+ - cassandra_data:/var/lib/cassandra
256
+ healthcheck:
257
+ test: ["CMD-SHELL", "cqlsh -e 'describe cluster' || exit 1"]
258
+ interval: 15s
259
+ timeout: 10s
260
+ retries: 10
261
+ start_period: 60s
262
+ restart: unless-stopped
263
+
264
+ redis:
265
+ image: redis:7-alpine
266
+ container_name: pyworkflow-redis
267
+ ports:
268
+ - "6379:6379"
269
+ volumes:
270
+ - redis_data:/data
271
+ healthcheck:
272
+ test: ["CMD", "redis-cli", "ping"]
273
+ interval: 5s
274
+ timeout: 3s
275
+ retries: 5
276
+ restart: unless-stopped
277
+
278
+ dashboard-backend:
279
+ image: yashabro/pyworkflow-dashboard-backend:latest
280
+ container_name: pyworkflow-dashboard-backend
281
+ working_dir: /app/project
282
+ ports:
283
+ - "8585:8585"
284
+ environment:
285
+ - DASHBOARD_PYWORKFLOW_CONFIG_PATH=/app/project/pyworkflow.config.yaml
286
+ {dashboard_env}
287
+ - DASHBOARD_HOST=0.0.0.0
288
+ - DASHBOARD_PORT=8585
289
+ - DASHBOARD_CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
290
+ - PYWORKFLOW_CELERY_BROKER=redis://redis:6379/0
291
+ - PYWORKFLOW_CELERY_RESULT_BACKEND=redis://redis:6379/1
292
+ - PYTHONPATH=/app/project
293
+ volumes:
294
+ - .:/app/project:ro
295
+ depends_on:
296
+ cassandra:
297
+ condition: service_healthy
298
+ redis:
299
+ condition: service_healthy
300
+ restart: unless-stopped
301
+
302
+ dashboard-frontend:
303
+ image: yashabro/pyworkflow-dashboard-frontend:latest
304
+ container_name: pyworkflow-dashboard-frontend
305
+ ports:
306
+ - "5173:80"
307
+ environment:
308
+ - VITE_API_URL=http://localhost:8585
309
+ depends_on:
310
+ - dashboard-backend
311
+ restart: unless-stopped
312
+
313
+ volumes:
314
+ cassandra_data:
315
+ driver: local
316
+ redis_data:
317
+ driver: local
318
+ """
319
+
320
+ return template
321
+
322
+
198
323
  def generate_postgres_docker_compose_content(
199
324
  postgres_host: str = "postgres",
200
325
  postgres_port: int = 5432,
@@ -301,6 +426,113 @@ volumes:
301
426
  return template
302
427
 
303
428
 
429
+ def generate_mysql_docker_compose_content(
430
+ mysql_host: str = "mysql",
431
+ mysql_port: int = 3306,
432
+ mysql_user: str = "pyworkflow",
433
+ mysql_password: str = "pyworkflow",
434
+ mysql_database: str = "pyworkflow",
435
+ ) -> str:
436
+ """
437
+ Generate docker-compose.yml content for MySQL-based PyWorkflow services.
438
+
439
+ Args:
440
+ mysql_host: MySQL host (container name for docker networking)
441
+ mysql_port: MySQL port
442
+ mysql_user: MySQL user
443
+ mysql_password: MySQL password
444
+ mysql_database: MySQL database name
445
+
446
+ Returns:
447
+ docker-compose.yml content as string
448
+
449
+ Example:
450
+ >>> compose_content = generate_mysql_docker_compose_content()
451
+ """
452
+ template = f"""services:
453
+ mysql:
454
+ image: mysql:8.0
455
+ container_name: pyworkflow-mysql
456
+ ports:
457
+ - "{mysql_port}:3306"
458
+ environment:
459
+ - MYSQL_ROOT_PASSWORD={mysql_password}
460
+ - MYSQL_USER={mysql_user}
461
+ - MYSQL_PASSWORD={mysql_password}
462
+ - MYSQL_DATABASE={mysql_database}
463
+ volumes:
464
+ - mysql_data:/var/lib/mysql
465
+ healthcheck:
466
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "{mysql_user}", "-p{mysql_password}"]
467
+ interval: 5s
468
+ timeout: 3s
469
+ retries: 5
470
+ restart: unless-stopped
471
+
472
+ redis:
473
+ image: redis:7-alpine
474
+ container_name: pyworkflow-redis
475
+ ports:
476
+ - "6379:6379"
477
+ volumes:
478
+ - redis_data:/data
479
+ healthcheck:
480
+ test: ["CMD", "redis-cli", "ping"]
481
+ interval: 5s
482
+ timeout: 3s
483
+ retries: 5
484
+ restart: unless-stopped
485
+
486
+ dashboard-backend:
487
+ image: yashabro/pyworkflow-dashboard-backend:latest
488
+ container_name: pyworkflow-dashboard-backend
489
+ working_dir: /app/project
490
+ ports:
491
+ - "8585:8585"
492
+ environment:
493
+ - DASHBOARD_PYWORKFLOW_CONFIG_PATH=/app/project/pyworkflow.config.yaml
494
+ - DASHBOARD_STORAGE_TYPE=mysql
495
+ - DASHBOARD_MYSQL_HOST={mysql_host}
496
+ - DASHBOARD_MYSQL_PORT=3306
497
+ - DASHBOARD_MYSQL_USER={mysql_user}
498
+ - DASHBOARD_MYSQL_PASSWORD={mysql_password}
499
+ - DASHBOARD_MYSQL_DATABASE={mysql_database}
500
+ - DASHBOARD_HOST=0.0.0.0
501
+ - DASHBOARD_PORT=8585
502
+ - DASHBOARD_CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
503
+ - PYWORKFLOW_CELERY_BROKER=redis://redis:6379/0
504
+ - PYWORKFLOW_CELERY_RESULT_BACKEND=redis://redis:6379/1
505
+ - PYTHONPATH=/app/project
506
+ volumes:
507
+ - .:/app/project:ro
508
+ depends_on:
509
+ mysql:
510
+ condition: service_healthy
511
+ redis:
512
+ condition: service_healthy
513
+ restart: unless-stopped
514
+
515
+ dashboard-frontend:
516
+ image: yashabro/pyworkflow-dashboard-frontend:latest
517
+ container_name: pyworkflow-dashboard-frontend
518
+ ports:
519
+ - "5173:80"
520
+ environment:
521
+ - VITE_API_URL=http://localhost:8585
522
+ depends_on:
523
+ - dashboard-backend
524
+ restart: unless-stopped
525
+
526
+ volumes:
527
+ mysql_data:
528
+ driver: local
529
+ redis_data:
530
+ driver: local
531
+ """
532
+
533
+ return template
534
+
535
+
304
536
  def write_docker_compose(content: str, path: Path) -> None:
305
537
  """
306
538
  Write docker-compose.yml to file.
pyworkflow/config.py CHANGED
@@ -5,8 +5,9 @@ Provides global configuration for runtime, storage, and default settings.
5
5
 
6
6
  Configuration is loaded in this priority order:
7
7
  1. Values set via pyworkflow.configure() (highest priority)
8
- 2. Values from pyworkflow.config.yaml in current directory
9
- 3. Default values
8
+ 2. Environment variables (PYWORKFLOW_*)
9
+ 3. Values from pyworkflow.config.yaml in current directory
10
+ 4. Default values
10
11
 
11
12
  Usage:
12
13
  >>> import pyworkflow
@@ -15,8 +16,26 @@ Usage:
15
16
  ... default_durable=False,
16
17
  ... storage=InMemoryStorageBackend(),
17
18
  ... )
19
+
20
+ Environment Variables:
21
+ PYWORKFLOW_STORAGE_TYPE: Storage backend type (file, memory, sqlite, postgres, mysql)
22
+ PYWORKFLOW_STORAGE_PATH: Path for file/sqlite backends
23
+ PYWORKFLOW_POSTGRES_HOST: PostgreSQL host
24
+ PYWORKFLOW_POSTGRES_PORT: PostgreSQL port
25
+ PYWORKFLOW_POSTGRES_USER: PostgreSQL user
26
+ PYWORKFLOW_POSTGRES_PASSWORD: PostgreSQL password
27
+ PYWORKFLOW_POSTGRES_DATABASE: PostgreSQL database
28
+ PYWORKFLOW_MYSQL_HOST: MySQL host
29
+ PYWORKFLOW_MYSQL_PORT: MySQL port
30
+ PYWORKFLOW_MYSQL_USER: MySQL user
31
+ PYWORKFLOW_MYSQL_PASSWORD: MySQL password
32
+ PYWORKFLOW_MYSQL_DATABASE: MySQL database
33
+ PYWORKFLOW_CELERY_BROKER: Celery broker URL
34
+ PYWORKFLOW_CELERY_RESULT_BACKEND: Celery result backend URL
35
+ PYWORKFLOW_RUNTIME: Default runtime (local, celery)
18
36
  """
19
37
 
38
+ import os
20
39
  import warnings
21
40
  from dataclasses import dataclass
22
41
  from pathlib import Path
@@ -26,6 +45,54 @@ if TYPE_CHECKING:
26
45
  from pyworkflow.storage.base import StorageBackend
27
46
 
28
47
 
48
+ def _load_env_storage_config() -> dict[str, Any] | None:
49
+ """
50
+ Load storage configuration from environment variables.
51
+
52
+ Returns:
53
+ Storage configuration dict if PYWORKFLOW_STORAGE_TYPE is set, None otherwise
54
+ """
55
+ storage_type = os.getenv("PYWORKFLOW_STORAGE_TYPE") or os.getenv("PYWORKFLOW_STORAGE_BACKEND")
56
+ if not storage_type:
57
+ return None
58
+
59
+ storage_type = storage_type.lower()
60
+
61
+ if storage_type == "postgres":
62
+ return {
63
+ "type": "postgres",
64
+ "host": os.getenv("PYWORKFLOW_POSTGRES_HOST", "localhost"),
65
+ "port": int(os.getenv("PYWORKFLOW_POSTGRES_PORT", "5432")),
66
+ "user": os.getenv("PYWORKFLOW_POSTGRES_USER", "pyworkflow"),
67
+ "password": os.getenv("PYWORKFLOW_POSTGRES_PASSWORD", ""),
68
+ "database": os.getenv("PYWORKFLOW_POSTGRES_DATABASE", "pyworkflow"),
69
+ }
70
+ elif storage_type == "mysql":
71
+ return {
72
+ "type": "mysql",
73
+ "host": os.getenv("PYWORKFLOW_MYSQL_HOST", "localhost"),
74
+ "port": int(os.getenv("PYWORKFLOW_MYSQL_PORT", "3306")),
75
+ "user": os.getenv("PYWORKFLOW_MYSQL_USER", "pyworkflow"),
76
+ "password": os.getenv("PYWORKFLOW_MYSQL_PASSWORD", ""),
77
+ "database": os.getenv("PYWORKFLOW_MYSQL_DATABASE", "pyworkflow"),
78
+ }
79
+ elif storage_type == "sqlite":
80
+ return {
81
+ "type": "sqlite",
82
+ "base_path": os.getenv("PYWORKFLOW_STORAGE_PATH", "./pyworkflow_data/pyworkflow.db"),
83
+ }
84
+ elif storage_type == "memory":
85
+ return {"type": "memory"}
86
+ elif storage_type == "file":
87
+ return {
88
+ "type": "file",
89
+ "base_path": os.getenv("PYWORKFLOW_STORAGE_PATH", "./pyworkflow_data"),
90
+ }
91
+ else:
92
+ # Unknown type, return as-is and let config_to_storage handle it
93
+ return {"type": storage_type}
94
+
95
+
29
96
  def _load_yaml_config() -> dict[str, Any]:
30
97
  """
31
98
  Load configuration from pyworkflow.config.yaml in current directory.
@@ -101,23 +168,33 @@ class PyWorkflowConfig:
101
168
  event_warning_interval: int = 100 # Log warning every N events after soft limit
102
169
 
103
170
 
104
- def _config_from_yaml() -> PyWorkflowConfig:
105
- """Create a PyWorkflowConfig from YAML file settings."""
106
- yaml_config = _load_yaml_config()
171
+ def _config_from_env_and_yaml() -> PyWorkflowConfig:
172
+ """
173
+ Create a PyWorkflowConfig from environment variables and YAML file.
107
174
 
108
- if not yaml_config:
109
- return PyWorkflowConfig()
175
+ Priority:
176
+ 1. Environment variables (PYWORKFLOW_*)
177
+ 2. YAML config file (pyworkflow.config.yaml)
178
+ 3. Defaults
179
+ """
180
+ yaml_config = _load_yaml_config()
181
+ env_storage_config = _load_env_storage_config()
110
182
 
111
- # Map YAML keys to config attributes
112
- runtime = yaml_config.get("runtime", "local")
183
+ # Runtime: env var > yaml > default
184
+ runtime = os.getenv("PYWORKFLOW_RUNTIME") or yaml_config.get("runtime", "local")
113
185
  durable = runtime == "celery" # Celery runtime defaults to durable
114
186
 
115
- # Create storage from config
116
- storage = _create_storage_from_config(yaml_config.get("storage", {}))
187
+ # Storage: env var > yaml > None
188
+ if env_storage_config:
189
+ storage = _create_storage_from_config(env_storage_config)
190
+ elif yaml_config.get("storage"):
191
+ storage = _create_storage_from_config(yaml_config.get("storage", {}))
192
+ else:
193
+ storage = None
117
194
 
118
- # Get celery broker
195
+ # Celery broker: env var > yaml > None
119
196
  celery_config = yaml_config.get("celery", {})
120
- celery_broker = celery_config.get("broker")
197
+ celery_broker = os.getenv("PYWORKFLOW_CELERY_BROKER") or celery_config.get("broker")
121
198
 
122
199
  return PyWorkflowConfig(
123
200
  default_runtime=runtime,
@@ -288,16 +365,16 @@ def get_config() -> PyWorkflowConfig:
288
365
  """
289
366
  Get the current configuration.
290
367
 
291
- If not yet configured, loads from pyworkflow.config.yaml if present,
292
- otherwise creates default configuration.
368
+ If not yet configured, loads from environment variables and
369
+ pyworkflow.config.yaml (env vars take priority).
293
370
 
294
371
  Returns:
295
372
  Current PyWorkflowConfig instance
296
373
  """
297
374
  global _config, _config_loaded_from_yaml
298
375
  if _config is None:
299
- # Try to load from YAML config file first
300
- _config = _config_from_yaml()
376
+ # Load from env vars and YAML config file
377
+ _config = _config_from_env_and_yaml()
301
378
  _config_loaded_from_yaml = True
302
379
  return _config
303
380
 
@@ -41,6 +41,13 @@ from pyworkflow.context.base import (
41
41
  )
42
42
  from pyworkflow.context.local import LocalContext
43
43
  from pyworkflow.context.mock import MockContext
44
+ from pyworkflow.context.step_context import (
45
+ StepContext,
46
+ get_step_context,
47
+ get_step_context_class,
48
+ has_step_context,
49
+ set_step_context,
50
+ )
44
51
 
45
52
  __all__ = [
46
53
  # Base context and helpers
@@ -49,6 +56,12 @@ __all__ = [
49
56
  "has_context",
50
57
  "set_context",
51
58
  "reset_context",
59
+ # Step context for distributed execution
60
+ "StepContext",
61
+ "get_step_context",
62
+ "has_step_context",
63
+ "set_step_context",
64
+ "get_step_context_class",
52
65
  # Context implementations
53
66
  "LocalContext",
54
67
  "MockContext",
@@ -281,6 +281,32 @@ class WorkflowContext(ABC):
281
281
  """Get the storage backend."""
282
282
  return None
283
283
 
284
+ @property
285
+ def runtime(self) -> str | None:
286
+ """
287
+ Get the runtime environment slug.
288
+
289
+ Returns the runtime identifier (e.g., "celery", "temporal") or None
290
+ for local/inline execution. Used to determine step dispatch behavior.
291
+
292
+ Returns:
293
+ Runtime slug string or None for local execution
294
+ """
295
+ return None # Default: local/inline execution
296
+
297
+ @property
298
+ def storage_config(self) -> dict[str, Any] | None:
299
+ """Get storage configuration for distributed workers."""
300
+ return None
301
+
302
+ def has_step_failed(self, step_id: str) -> bool:
303
+ """Check if a step has a recorded failure."""
304
+ return False # Default: no failures
305
+
306
+ def get_step_failure(self, step_id: str) -> dict[str, Any] | None:
307
+ """Get failure info for a step."""
308
+ return None # Default: no failure info
309
+
284
310
  def should_execute_step(self, step_id: str) -> bool:
285
311
  """Check if step should be executed (not already completed)."""
286
312
  return True # Default: always execute
@@ -97,6 +97,13 @@ class LocalContext(WorkflowContext):
97
97
  self._child_results: dict[str, dict[str, Any]] = {}
98
98
  self._pending_children: dict[str, str] = {} # child_id -> child_run_id
99
99
 
100
+ # Runtime environment (e.g., "celery", "temporal", None for local)
101
+ self._runtime: str | None = None
102
+ self._storage_config: dict[str, Any] | None = None
103
+
104
+ # Step failure tracking (for handling failures during replay)
105
+ self._step_failures: dict[str, dict[str, Any]] = {}
106
+
100
107
  # Replay state if resuming
101
108
  if event_log:
102
109
  self._is_replaying = True
@@ -134,6 +141,22 @@ class LocalContext(WorkflowContext):
134
141
  "last_error": event.data.get("error", ""),
135
142
  }
136
143
 
144
+ elif event.type == EventType.STEP_FAILED:
145
+ # Track step failures for distributed step dispatch
146
+ # If a step failed (not retrying), record it for replay detection
147
+ step_id = event.data.get("step_id")
148
+ is_retryable = event.data.get("is_retryable", True)
149
+ # Only track as a final failure if not retryable or if this is
150
+ # the last failure before a STEP_COMPLETED event
151
+ # For now, track all failures - the step decorator will check
152
+ # if there's also a STEP_COMPLETED event
153
+ if step_id and not is_retryable:
154
+ self._step_failures[step_id] = {
155
+ "error": event.data.get("error", "Unknown error"),
156
+ "error_type": event.data.get("error_type", "Exception"),
157
+ "is_retryable": is_retryable,
158
+ }
159
+
137
160
  elif event.type == EventType.CANCELLATION_REQUESTED:
138
161
  self._cancellation_requested = True
139
162
  self._cancellation_reason = event.data.get("reason")
@@ -208,6 +231,28 @@ class LocalContext(WorkflowContext):
208
231
  """Set replay mode."""
209
232
  self._is_replaying = value
210
233
 
234
+ @property
235
+ def runtime(self) -> str | None:
236
+ """
237
+ Get the runtime environment slug.
238
+
239
+ Returns the runtime identifier (e.g., "celery", "temporal") or None
240
+ for local/inline execution. Used to determine step dispatch behavior.
241
+
242
+ Returns:
243
+ Runtime slug string or None for local execution
244
+ """
245
+ return self._runtime
246
+
247
+ @property
248
+ def storage_config(self) -> dict[str, Any] | None:
249
+ """
250
+ Get storage configuration for distributed workers.
251
+
252
+ This is passed to step workers so they can connect to the same storage.
253
+ """
254
+ return self._storage_config
255
+
211
256
  # =========================================================================
212
257
  # Step result caching (for @step decorator compatibility)
213
258
  # =========================================================================
@@ -255,6 +300,41 @@ class LocalContext(WorkflowContext):
255
300
  """Clear retry state for a step."""
256
301
  self._retry_states.pop(step_id, None)
257
302
 
303
+ # =========================================================================
304
+ # Step failure tracking (for distributed step dispatch)
305
+ # =========================================================================
306
+
307
+ def has_step_failed(self, step_id: str) -> bool:
308
+ """
309
+ Check if a step has a recorded failure.
310
+
311
+ Used during replay to detect steps that failed on a remote worker.
312
+ """
313
+ return step_id in self._step_failures
314
+
315
+ def get_step_failure(self, step_id: str) -> dict[str, Any] | None:
316
+ """
317
+ Get step failure info.
318
+
319
+ Returns:
320
+ Dict with 'error', 'error_type', 'is_retryable' or None
321
+ """
322
+ return self._step_failures.get(step_id)
323
+
324
+ def record_step_failure(
325
+ self,
326
+ step_id: str,
327
+ error: str,
328
+ error_type: str,
329
+ is_retryable: bool,
330
+ ) -> None:
331
+ """Record a step failure for replay detection."""
332
+ self._step_failures[step_id] = {
333
+ "error": error,
334
+ "error_type": error_type,
335
+ "is_retryable": is_retryable,
336
+ }
337
+
258
338
  # =========================================================================
259
339
  # Sleep state management (for @step decorator and EventReplayer compatibility)
260
340
  # =========================================================================