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.
- pyworkflow/__init__.py +10 -1
- pyworkflow/celery/tasks.py +272 -24
- pyworkflow/cli/__init__.py +4 -1
- pyworkflow/cli/commands/runs.py +4 -4
- pyworkflow/cli/commands/setup.py +203 -4
- pyworkflow/cli/utils/config_generator.py +76 -3
- pyworkflow/cli/utils/docker_manager.py +232 -0
- pyworkflow/config.py +94 -17
- pyworkflow/context/__init__.py +13 -0
- pyworkflow/context/base.py +26 -0
- pyworkflow/context/local.py +80 -0
- pyworkflow/context/step_context.py +295 -0
- pyworkflow/core/registry.py +6 -1
- pyworkflow/core/step.py +141 -0
- pyworkflow/core/workflow.py +56 -0
- pyworkflow/engine/events.py +30 -0
- pyworkflow/engine/replay.py +39 -0
- pyworkflow/primitives/child_workflow.py +1 -1
- pyworkflow/runtime/local.py +1 -1
- pyworkflow/storage/__init__.py +14 -0
- pyworkflow/storage/base.py +35 -0
- pyworkflow/storage/cassandra.py +1747 -0
- pyworkflow/storage/config.py +69 -0
- pyworkflow/storage/dynamodb.py +31 -2
- pyworkflow/storage/file.py +28 -0
- pyworkflow/storage/memory.py +18 -0
- pyworkflow/storage/mysql.py +1159 -0
- pyworkflow/storage/postgres.py +27 -2
- pyworkflow/storage/schemas.py +4 -3
- pyworkflow/storage/sqlite.py +25 -2
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/METADATA +7 -4
- pyworkflow_engine-0.1.10.dist-info/RECORD +91 -0
- pyworkflow_engine-0.1.10.dist-info/top_level.txt +1 -0
- dashboard/backend/app/__init__.py +0 -1
- dashboard/backend/app/config.py +0 -32
- dashboard/backend/app/controllers/__init__.py +0 -6
- dashboard/backend/app/controllers/run_controller.py +0 -86
- dashboard/backend/app/controllers/workflow_controller.py +0 -33
- dashboard/backend/app/dependencies/__init__.py +0 -5
- dashboard/backend/app/dependencies/storage.py +0 -50
- dashboard/backend/app/repositories/__init__.py +0 -6
- dashboard/backend/app/repositories/run_repository.py +0 -80
- dashboard/backend/app/repositories/workflow_repository.py +0 -27
- dashboard/backend/app/rest/__init__.py +0 -8
- dashboard/backend/app/rest/v1/__init__.py +0 -12
- dashboard/backend/app/rest/v1/health.py +0 -33
- dashboard/backend/app/rest/v1/runs.py +0 -133
- dashboard/backend/app/rest/v1/workflows.py +0 -41
- dashboard/backend/app/schemas/__init__.py +0 -23
- dashboard/backend/app/schemas/common.py +0 -16
- dashboard/backend/app/schemas/event.py +0 -24
- dashboard/backend/app/schemas/hook.py +0 -25
- dashboard/backend/app/schemas/run.py +0 -54
- dashboard/backend/app/schemas/step.py +0 -28
- dashboard/backend/app/schemas/workflow.py +0 -31
- dashboard/backend/app/server.py +0 -87
- dashboard/backend/app/services/__init__.py +0 -6
- dashboard/backend/app/services/run_service.py +0 -240
- dashboard/backend/app/services/workflow_service.py +0 -155
- dashboard/backend/main.py +0 -18
- docs/concepts/cancellation.mdx +0 -362
- docs/concepts/continue-as-new.mdx +0 -434
- docs/concepts/events.mdx +0 -266
- docs/concepts/fault-tolerance.mdx +0 -370
- docs/concepts/hooks.mdx +0 -552
- docs/concepts/limitations.mdx +0 -167
- docs/concepts/schedules.mdx +0 -775
- docs/concepts/sleep.mdx +0 -312
- docs/concepts/steps.mdx +0 -301
- docs/concepts/workflows.mdx +0 -255
- docs/guides/cli.mdx +0 -942
- docs/guides/configuration.mdx +0 -560
- docs/introduction.mdx +0 -155
- docs/quickstart.mdx +0 -279
- examples/__init__.py +0 -1
- examples/celery/__init__.py +0 -1
- examples/celery/durable/docker-compose.yml +0 -55
- examples/celery/durable/pyworkflow.config.yaml +0 -12
- examples/celery/durable/workflows/__init__.py +0 -122
- examples/celery/durable/workflows/basic.py +0 -87
- examples/celery/durable/workflows/batch_processing.py +0 -102
- examples/celery/durable/workflows/cancellation.py +0 -273
- examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
- examples/celery/durable/workflows/child_workflows.py +0 -202
- examples/celery/durable/workflows/continue_as_new.py +0 -260
- examples/celery/durable/workflows/fault_tolerance.py +0 -210
- examples/celery/durable/workflows/hooks.py +0 -211
- examples/celery/durable/workflows/idempotency.py +0 -112
- examples/celery/durable/workflows/long_running.py +0 -99
- examples/celery/durable/workflows/retries.py +0 -101
- examples/celery/durable/workflows/schedules.py +0 -209
- examples/celery/transient/01_basic_workflow.py +0 -91
- examples/celery/transient/02_fault_tolerance.py +0 -257
- examples/celery/transient/__init__.py +0 -20
- examples/celery/transient/pyworkflow.config.yaml +0 -25
- examples/local/__init__.py +0 -1
- examples/local/durable/01_basic_workflow.py +0 -94
- examples/local/durable/02_file_storage.py +0 -132
- examples/local/durable/03_retries.py +0 -169
- examples/local/durable/04_long_running.py +0 -119
- examples/local/durable/05_event_log.py +0 -145
- examples/local/durable/06_idempotency.py +0 -148
- examples/local/durable/07_hooks.py +0 -334
- examples/local/durable/08_cancellation.py +0 -233
- examples/local/durable/09_child_workflows.py +0 -198
- examples/local/durable/10_child_workflow_patterns.py +0 -265
- examples/local/durable/11_continue_as_new.py +0 -249
- examples/local/durable/12_schedules.py +0 -198
- examples/local/durable/__init__.py +0 -1
- examples/local/transient/01_quick_tasks.py +0 -87
- examples/local/transient/02_retries.py +0 -130
- examples/local/transient/03_sleep.py +0 -141
- examples/local/transient/__init__.py +0 -1
- pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +0 -330
- tests/integration/test_child_workflows.py +0 -439
- tests/integration/test_continue_as_new.py +0 -428
- tests/integration/test_dynamodb_storage.py +0 -1146
- tests/integration/test_fault_tolerance.py +0 -369
- tests/integration/test_schedule_storage.py +0 -484
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +0 -1
- tests/unit/backends/test_dynamodb_storage.py +0 -1554
- tests/unit/backends/test_postgres_storage.py +0 -1281
- tests/unit/backends/test_sqlite_storage.py +0 -1460
- tests/unit/conftest.py +0 -41
- tests/unit/test_cancellation.py +0 -364
- tests/unit/test_child_workflows.py +0 -680
- tests/unit/test_continue_as_new.py +0 -441
- tests/unit/test_event_limits.py +0 -316
- tests/unit/test_executor.py +0 -320
- tests/unit/test_fault_tolerance.py +0 -334
- tests/unit/test_hooks.py +0 -495
- tests/unit/test_registry.py +0 -261
- tests/unit/test_replay.py +0 -420
- tests/unit/test_schedule_schemas.py +0 -285
- tests/unit/test_schedule_utils.py +0 -286
- tests/unit/test_scheduled_workflow.py +0 -274
- tests/unit/test_step.py +0 -353
- tests/unit/test_workflow.py +0 -243
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/WHEEL +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/entry_points.txt +0 -0
- {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.
|
|
9
|
-
3.
|
|
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
|
|
105
|
-
"""
|
|
106
|
-
|
|
171
|
+
def _config_from_env_and_yaml() -> PyWorkflowConfig:
|
|
172
|
+
"""
|
|
173
|
+
Create a PyWorkflowConfig from environment variables and YAML file.
|
|
107
174
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
116
|
-
|
|
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
|
-
#
|
|
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
|
|
292
|
-
|
|
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
|
-
#
|
|
300
|
-
_config =
|
|
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
|
|
pyworkflow/context/__init__.py
CHANGED
|
@@ -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",
|
pyworkflow/context/base.py
CHANGED
|
@@ -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
|
pyworkflow/context/local.py
CHANGED
|
@@ -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
|
# =========================================================================
|