pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

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