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
@@ -21,7 +21,9 @@ from pyworkflow.cli.utils.config_generator import (
21
21
  from pyworkflow.cli.utils.docker_manager import (
22
22
  check_docker_available,
23
23
  check_service_health,
24
+ generate_cassandra_docker_compose_content,
24
25
  generate_docker_compose_content,
26
+ generate_mysql_docker_compose_content,
25
27
  generate_postgres_docker_compose_content,
26
28
  run_docker_command,
27
29
  write_docker_compose,
@@ -87,7 +89,10 @@ def _flatten_yaml_config(nested_config: dict) -> dict:
87
89
  )
88
90
  @click.option(
89
91
  "--storage",
90
- type=click.Choice(["file", "memory", "sqlite", "postgres", "dynamodb"], case_sensitive=False),
92
+ type=click.Choice(
93
+ ["file", "memory", "sqlite", "postgres", "mysql", "dynamodb", "cassandra"],
94
+ case_sensitive=False,
95
+ ),
91
96
  help="Storage backend type",
92
97
  )
93
98
  @click.option(
@@ -140,7 +145,7 @@ def setup(
140
145
  sys.exit(1)
141
146
  except Exception as e:
142
147
  print_error(f"\nSetup failed: {str(e)}")
143
- if ctx.obj.get("verbose"):
148
+ if ctx.obj and ctx.obj.get("verbose"):
144
149
  raise
145
150
  sys.exit(1)
146
151
 
@@ -239,6 +244,13 @@ def _run_setup(
239
244
 
240
245
  # 6. Write configuration file
241
246
  print_info("\nGenerating configuration...")
247
+
248
+ # Parse Cassandra contact points from comma-separated string to list
249
+ cassandra_contact_points = None
250
+ if config_data.get("cassandra_contact_points"):
251
+ contact_points_str = config_data["cassandra_contact_points"]
252
+ cassandra_contact_points = [cp.strip() for cp in contact_points_str.split(",")]
253
+
242
254
  yaml_content = generate_yaml_config(
243
255
  module=config_data.get("module"),
244
256
  runtime=config_data["runtime"],
@@ -251,9 +263,19 @@ def _run_setup(
251
263
  postgres_user=config_data.get("postgres_user"),
252
264
  postgres_password=config_data.get("postgres_password"),
253
265
  postgres_database=config_data.get("postgres_database"),
266
+ mysql_host=config_data.get("mysql_host"),
267
+ mysql_port=config_data.get("mysql_port"),
268
+ mysql_user=config_data.get("mysql_user"),
269
+ mysql_password=config_data.get("mysql_password"),
270
+ mysql_database=config_data.get("mysql_database"),
254
271
  dynamodb_table_name=config_data.get("dynamodb_table_name"),
255
272
  dynamodb_region=config_data.get("dynamodb_region"),
256
273
  dynamodb_endpoint_url=config_data.get("dynamodb_endpoint_url"),
274
+ cassandra_contact_points=cassandra_contact_points,
275
+ cassandra_port=config_data.get("cassandra_port"),
276
+ cassandra_keyspace=config_data.get("cassandra_keyspace"),
277
+ cassandra_user=config_data.get("cassandra_user"),
278
+ cassandra_password=config_data.get("cassandra_password"),
257
279
  )
258
280
 
259
281
  config_file_path = write_yaml_config(yaml_content, config_path, backup=True)
@@ -313,6 +335,36 @@ def _check_postgres_available() -> bool:
313
335
  return False
314
336
 
315
337
 
338
+ def _check_cassandra_available() -> bool:
339
+ """
340
+ Check if cassandra-driver is installed for Cassandra support.
341
+
342
+ Returns:
343
+ True if cassandra-driver is available, False otherwise
344
+ """
345
+ try:
346
+ from cassandra.cluster import Cluster # noqa: F401
347
+
348
+ return True
349
+ except ImportError:
350
+ return False
351
+
352
+
353
+ def _check_mysql_available() -> bool:
354
+ """
355
+ Check if aiomysql is installed for MySQL support.
356
+
357
+ Returns:
358
+ True if aiomysql is available, False otherwise
359
+ """
360
+ try:
361
+ import aiomysql # noqa: F401
362
+
363
+ return True
364
+ except ImportError:
365
+ return False
366
+
367
+
316
368
  def _run_interactive_configuration(
317
369
  non_interactive: bool,
318
370
  module_override: str | None,
@@ -346,9 +398,11 @@ def _run_interactive_configuration(
346
398
  config_data["result_backend"] = "redis://localhost:6379/1"
347
399
  print_info("✓ Broker: Redis (will be started via Docker)")
348
400
 
349
- # Check if SQLite and PostgreSQL are available
401
+ # Check if SQLite, PostgreSQL, Cassandra and MySQL are available
350
402
  sqlite_available = _check_sqlite_available()
351
403
  postgres_available = _check_postgres_available()
404
+ cassandra_available = _check_cassandra_available()
405
+ mysql_available = _check_mysql_available()
352
406
 
353
407
  # Storage backend
354
408
  if storage_override:
@@ -378,6 +432,27 @@ def _run_interactive_configuration(
378
432
  print_info("")
379
433
  print_info("Or choose a different storage backend: --storage sqlite")
380
434
  raise click.Abort()
435
+ # Validate if cassandra was requested but not available
436
+ if storage_type == "cassandra" and not cassandra_available:
437
+ print_error("\nCassandra storage backend is not available!")
438
+ print_info("\ncassandra-driver package is not installed.")
439
+ print_info("To fix this, install cassandra-driver:")
440
+ print_info("")
441
+ print_info(" pip install cassandra-driver")
442
+ print_info("")
443
+ print_info("Or choose a different storage backend: --storage sqlite")
444
+ raise click.Abort()
445
+ # Validate if mysql was requested but not available
446
+ mysql_available = _check_mysql_available()
447
+ if storage_type == "mysql" and not mysql_available:
448
+ print_error("\nMySQL storage backend is not available!")
449
+ print_info("\naiomysql package is not installed.")
450
+ print_info("To fix this, install aiomysql:")
451
+ print_info("")
452
+ print_info(" pip install aiomysql")
453
+ print_info("")
454
+ print_info("Or choose a different storage backend: --storage sqlite")
455
+ raise click.Abort()
381
456
  elif non_interactive:
382
457
  if sqlite_available:
383
458
  storage_type = "sqlite"
@@ -407,6 +482,14 @@ def _run_interactive_configuration(
407
482
  choices.append(
408
483
  {"name": "PostgreSQL - Scalable production database", "value": "postgres"}
409
484
  )
485
+ if cassandra_available:
486
+ choices.append(
487
+ {"name": "Cassandra - Distributed NoSQL database (scalable)", "value": "cassandra"}
488
+ )
489
+ if mysql_available:
490
+ choices.append(
491
+ {"name": "MySQL - Popular open-source relational database", "value": "mysql"}
492
+ )
410
493
  choices.extend(
411
494
  [
412
495
  {
@@ -428,6 +511,14 @@ def _run_interactive_configuration(
428
511
  print_info("Note: PostgreSQL backend available after: pip install asyncpg")
429
512
  print_info("")
430
513
 
514
+ if not cassandra_available:
515
+ print_info("Note: Cassandra backend available after: pip install cassandra-driver")
516
+ print_info("")
517
+
518
+ if not mysql_available:
519
+ print_info("Note: MySQL backend available after: pip install aiomysql")
520
+ print_info("")
521
+
431
522
  storage_type = select(
432
523
  "Choose storage backend:",
433
524
  choices=choices,
@@ -491,6 +582,38 @@ def _run_interactive_configuration(
491
582
  default="pyworkflow",
492
583
  )
493
584
 
585
+ # MySQL connection (for mysql backend)
586
+ elif storage_type == "mysql":
587
+ if non_interactive:
588
+ # Use default connection settings for non-interactive mode
589
+ config_data["mysql_host"] = "localhost"
590
+ config_data["mysql_port"] = "3306"
591
+ config_data["mysql_user"] = "pyworkflow"
592
+ config_data["mysql_password"] = "pyworkflow"
593
+ config_data["mysql_database"] = "pyworkflow"
594
+ else:
595
+ print_info("\nConfigure MySQL connection:")
596
+ config_data["mysql_host"] = input_text(
597
+ "MySQL host:",
598
+ default="localhost",
599
+ )
600
+ config_data["mysql_port"] = input_text(
601
+ "MySQL port:",
602
+ default="3306",
603
+ )
604
+ config_data["mysql_database"] = input_text(
605
+ "Database name:",
606
+ default="pyworkflow",
607
+ )
608
+ config_data["mysql_user"] = input_text(
609
+ "Database user:",
610
+ default="pyworkflow",
611
+ )
612
+ config_data["mysql_password"] = input_text(
613
+ "Database password:",
614
+ default="pyworkflow",
615
+ )
616
+
494
617
  # DynamoDB configuration
495
618
  elif storage_type == "dynamodb":
496
619
  if non_interactive:
@@ -517,6 +640,40 @@ def _run_interactive_configuration(
517
640
  )
518
641
  config_data["dynamodb_endpoint_url"] = endpoint
519
642
 
643
+ # Cassandra configuration
644
+ elif storage_type == "cassandra":
645
+ if non_interactive:
646
+ config_data["cassandra_contact_points"] = "localhost"
647
+ config_data["cassandra_port"] = "9042"
648
+ config_data["cassandra_keyspace"] = "pyworkflow"
649
+ else:
650
+ print_info("\nConfigure Cassandra connection:")
651
+ contact_points = input_text(
652
+ "Cassandra contact points (comma-separated):",
653
+ default="localhost",
654
+ )
655
+ config_data["cassandra_contact_points"] = contact_points
656
+
657
+ config_data["cassandra_port"] = input_text(
658
+ "Cassandra port:",
659
+ default="9042",
660
+ )
661
+ config_data["cassandra_keyspace"] = input_text(
662
+ "Keyspace name:",
663
+ default="pyworkflow",
664
+ )
665
+
666
+ # Optional authentication
667
+ if confirm("Use Cassandra authentication?", default=False):
668
+ config_data["cassandra_user"] = input_text(
669
+ "Cassandra user:",
670
+ default="cassandra",
671
+ )
672
+ config_data["cassandra_password"] = input_text(
673
+ "Cassandra password:",
674
+ default="cassandra",
675
+ )
676
+
520
677
  return config_data
521
678
 
522
679
 
@@ -543,6 +700,22 @@ def _setup_docker_infrastructure(
543
700
  postgres_password=config_data.get("postgres_password", "pyworkflow"),
544
701
  postgres_database=config_data.get("postgres_database", "pyworkflow"),
545
702
  )
703
+ elif storage_type == "cassandra":
704
+ compose_content = generate_cassandra_docker_compose_content(
705
+ cassandra_host="cassandra",
706
+ cassandra_port=int(config_data.get("cassandra_port", "9042")),
707
+ cassandra_keyspace=config_data.get("cassandra_keyspace", "pyworkflow"),
708
+ cassandra_user=config_data.get("cassandra_user"),
709
+ cassandra_password=config_data.get("cassandra_password"),
710
+ )
711
+ elif storage_type == "mysql":
712
+ compose_content = generate_mysql_docker_compose_content(
713
+ mysql_host="mysql",
714
+ mysql_port=int(config_data.get("mysql_port", "3306")),
715
+ mysql_user=config_data.get("mysql_user", "pyworkflow"),
716
+ mysql_password=config_data.get("mysql_password", "pyworkflow"),
717
+ mysql_database=config_data.get("mysql_database", "pyworkflow"),
718
+ )
546
719
  else:
547
720
  compose_content = generate_docker_compose_content(
548
721
  storage_type=storage_type,
@@ -574,10 +747,14 @@ def _setup_docker_infrastructure(
574
747
  print_info("\n Starting services...")
575
748
  print_info("")
576
749
 
577
- # Include postgres in services to start if using postgres storage
750
+ # Include postgres/cassandra/mysql in services to start if using those storage types
578
751
  services_to_start = ["redis"]
579
752
  if storage_type == "postgres":
580
753
  services_to_start.insert(0, "postgres")
754
+ elif storage_type == "cassandra":
755
+ services_to_start.insert(0, "cassandra")
756
+ elif storage_type == "mysql":
757
+ services_to_start.insert(0, "mysql")
581
758
  if dashboard_available:
582
759
  services_to_start.extend(["dashboard-backend", "dashboard-frontend"])
583
760
 
@@ -594,6 +771,12 @@ def _setup_docker_infrastructure(
594
771
  if storage_type == "postgres":
595
772
  postgres_port = config_data.get("postgres_port", "5432")
596
773
  ports_in_use = f"{postgres_port}, {ports_in_use}"
774
+ elif storage_type == "cassandra":
775
+ cassandra_port = config_data.get("cassandra_port", "9042")
776
+ ports_in_use = f"{cassandra_port}, {ports_in_use}"
777
+ elif storage_type == "mysql":
778
+ mysql_port = config_data.get("mysql_port", "3306")
779
+ ports_in_use = f"{mysql_port}, {ports_in_use}"
597
780
  print_info(f" • Check if ports {ports_in_use} are already in use")
598
781
  print_info(" • View logs: docker compose logs")
599
782
  print_info(" • Try: docker compose down && docker compose up -d")
@@ -612,6 +795,16 @@ def _setup_docker_infrastructure(
612
795
  pg_port = int(config_data.get("postgres_port", "5432"))
613
796
  health_checks["PostgreSQL"] = {"type": "tcp", "host": "localhost", "port": pg_port}
614
797
 
798
+ # Add Cassandra health check if using cassandra storage
799
+ if storage_type == "cassandra":
800
+ cass_port = int(config_data.get("cassandra_port", "9042"))
801
+ health_checks["Cassandra"] = {"type": "tcp", "host": "localhost", "port": cass_port}
802
+
803
+ # Add MySQL health check if using mysql storage
804
+ if storage_type == "mysql":
805
+ mysql_port_num = int(config_data.get("mysql_port", "3306"))
806
+ health_checks["MySQL"] = {"type": "tcp", "host": "localhost", "port": mysql_port_num}
807
+
615
808
  # Only check dashboard health if it was started
616
809
  if dashboard_available:
617
810
  health_checks["Dashboard Backend"] = {
@@ -672,6 +865,12 @@ def _show_next_steps(
672
865
  if config_data.get("storage_type") == "postgres":
673
866
  postgres_port = config_data.get("postgres_port", "5432")
674
867
  print_info(f" • PostgreSQL: localhost:{postgres_port}")
868
+ elif config_data.get("storage_type") == "cassandra":
869
+ cassandra_port = config_data.get("cassandra_port", "9042")
870
+ print_info(f" • Cassandra: localhost:{cassandra_port}")
871
+ elif config_data.get("storage_type") == "mysql":
872
+ mysql_port = config_data.get("mysql_port", "3306")
873
+ print_info(f" • MySQL: localhost:{mysql_port}")
675
874
  print_info(" • Redis: redis://localhost:6379")
676
875
  if dashboard_available:
677
876
  print_info(" • Dashboard: http://localhost:5173")
@@ -24,9 +24,19 @@ def generate_yaml_config(
24
24
  postgres_user: str | None = None,
25
25
  postgres_password: str | None = None,
26
26
  postgres_database: str | None = None,
27
+ mysql_host: str | None = None,
28
+ mysql_port: str | None = None,
29
+ mysql_user: str | None = None,
30
+ mysql_password: str | None = None,
31
+ mysql_database: str | None = None,
27
32
  dynamodb_table_name: str | None = None,
28
33
  dynamodb_region: str | None = None,
29
34
  dynamodb_endpoint_url: str | None = None,
35
+ cassandra_contact_points: list[str] | None = None,
36
+ cassandra_port: str | None = None,
37
+ cassandra_keyspace: str | None = None,
38
+ cassandra_user: str | None = None,
39
+ cassandra_password: str | None = None,
30
40
  ) -> str:
31
41
  """
32
42
  Generate YAML configuration content.
@@ -34,7 +44,8 @@ def generate_yaml_config(
34
44
  Args:
35
45
  module: Optional workflow module path (e.g., "myapp.workflows")
36
46
  runtime: Runtime type (e.g., "celery", "local")
37
- storage_type: Storage backend type (e.g., "sqlite", "file", "memory", "postgres")
47
+ storage_type: Storage backend type (e.g., "sqlite", "file", "memory", "postgres",
48
+ "cassandra")
38
49
  storage_path: Optional storage path for file/sqlite backends
39
50
  broker_url: Celery broker URL
40
51
  result_backend: Celery result backend URL
@@ -46,6 +57,11 @@ def generate_yaml_config(
46
57
  dynamodb_table_name: Optional DynamoDB table name
47
58
  dynamodb_region: Optional AWS region for DynamoDB
48
59
  dynamodb_endpoint_url: Optional local DynamoDB endpoint URL
60
+ cassandra_contact_points: List of Cassandra contact points (for cassandra backend)
61
+ cassandra_port: Cassandra CQL native transport port (for cassandra backend)
62
+ cassandra_keyspace: Cassandra keyspace name (for cassandra backend)
63
+ cassandra_user: Optional Cassandra user (for cassandra backend)
64
+ cassandra_password: Optional Cassandra password (for cassandra backend)
49
65
 
50
66
  Returns:
51
67
  YAML configuration as string
@@ -84,6 +100,17 @@ def generate_yaml_config(
84
100
  storage_config["password"] = postgres_password
85
101
  if postgres_database:
86
102
  storage_config["database"] = postgres_database
103
+ elif storage_type == "mysql":
104
+ if mysql_host:
105
+ storage_config["host"] = mysql_host
106
+ if mysql_port:
107
+ storage_config["port"] = int(mysql_port)
108
+ if mysql_user:
109
+ storage_config["user"] = mysql_user
110
+ if mysql_password:
111
+ storage_config["password"] = mysql_password
112
+ if mysql_database:
113
+ storage_config["database"] = mysql_database
87
114
  elif storage_type == "dynamodb":
88
115
  if dynamodb_table_name:
89
116
  storage_config["table_name"] = dynamodb_table_name
@@ -91,6 +118,17 @@ def generate_yaml_config(
91
118
  storage_config["region"] = dynamodb_region
92
119
  if dynamodb_endpoint_url:
93
120
  storage_config["endpoint_url"] = dynamodb_endpoint_url
121
+ elif storage_type == "cassandra":
122
+ if cassandra_contact_points:
123
+ storage_config["contact_points"] = cassandra_contact_points
124
+ if cassandra_port:
125
+ storage_config["port"] = int(cassandra_port)
126
+ if cassandra_keyspace:
127
+ storage_config["keyspace"] = cassandra_keyspace
128
+ if cassandra_user:
129
+ storage_config["username"] = cassandra_user
130
+ if cassandra_password:
131
+ storage_config["password"] = cassandra_password
94
132
  config["storage"] = storage_config
95
133
 
96
134
  # Celery configuration (only for celery runtime)
@@ -180,6 +218,13 @@ def load_yaml_config(path: Path | None = None) -> dict[str, Any]:
180
218
  with open(path) as f:
181
219
  config = yaml.safe_load(f)
182
220
 
221
+ # Handle empty config file (yaml.safe_load returns None for empty files)
222
+ if config is None:
223
+ raise ValueError(
224
+ "Config file is empty or contains only comments. "
225
+ "Please add valid configuration or delete the file to create a new one."
226
+ )
227
+
183
228
  # Ensure it's a dict
184
229
  if not isinstance(config, dict):
185
230
  raise ValueError("Config file must contain a YAML dictionary")
@@ -258,6 +303,14 @@ def display_config_summary(config: dict[str, Any]) -> list[str]:
258
303
  user = storage.get("user", "pyworkflow")
259
304
  lines.append(f" PostgreSQL: {user}@{host}:{port}/{database}")
260
305
 
306
+ # MySQL-specific config
307
+ if storage_type == "mysql":
308
+ host = storage.get("host", "localhost")
309
+ port = storage.get("port", 3306)
310
+ database = storage.get("database", "pyworkflow")
311
+ user = storage.get("user", "pyworkflow")
312
+ lines.append(f" MySQL: {user}@{host}:{port}/{database}")
313
+
261
314
  # DynamoDB-specific config
262
315
  if storage_type == "dynamodb":
263
316
  if "table_name" in storage:
@@ -267,6 +320,16 @@ def display_config_summary(config: dict[str, Any]) -> list[str]:
267
320
  if "endpoint_url" in storage:
268
321
  lines.append(f" Endpoint URL: {storage['endpoint_url']}")
269
322
 
323
+ # Cassandra-specific config
324
+ if storage_type == "cassandra":
325
+ contact_points = storage.get("contact_points", ["localhost"])
326
+ port = storage.get("port", 9042)
327
+ keyspace = storage.get("keyspace", "pyworkflow")
328
+ hosts = ", ".join(contact_points) if isinstance(contact_points, list) else contact_points
329
+ lines.append(f" Cassandra: {hosts}:{port}/{keyspace}")
330
+ if "username" in storage:
331
+ lines.append(f" Cassandra User: {storage['username']}")
332
+
270
333
  # Celery (if applicable)
271
334
  if runtime == "celery" and "celery" in config:
272
335
  celery = config["celery"]
@@ -314,10 +377,20 @@ def validate_config(config: dict[str, Any]) -> tuple[bool, list[str]]:
314
377
  storage_type = storage.get("type")
315
378
  if not storage_type:
316
379
  errors.append("Missing storage 'type'")
317
- elif storage_type not in ["file", "memory", "sqlite", "redis", "postgres", "dynamodb"]:
380
+ elif storage_type not in [
381
+ "file",
382
+ "memory",
383
+ "sqlite",
384
+ "redis",
385
+ "postgres",
386
+ "mysql",
387
+ "dynamodb",
388
+ "cassandra",
389
+ ]:
318
390
  errors.append(
319
391
  f"Invalid storage type: {storage_type}. "
320
- "Must be 'file', 'memory', 'sqlite', 'redis', 'postgres' or 'dynamodb'"
392
+ "Must be 'file', 'memory', 'sqlite', 'redis', 'postgres', 'mysql', 'dynamodb' "
393
+ "or 'cassandra'"
321
394
  )
322
395
 
323
396
  # Check Celery config if using celery runtime