pyworkflow-engine 0.1.7__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 (196) hide show
  1. dashboard/backend/app/__init__.py +1 -0
  2. dashboard/backend/app/config.py +32 -0
  3. dashboard/backend/app/controllers/__init__.py +6 -0
  4. dashboard/backend/app/controllers/run_controller.py +86 -0
  5. dashboard/backend/app/controllers/workflow_controller.py +33 -0
  6. dashboard/backend/app/dependencies/__init__.py +5 -0
  7. dashboard/backend/app/dependencies/storage.py +50 -0
  8. dashboard/backend/app/repositories/__init__.py +6 -0
  9. dashboard/backend/app/repositories/run_repository.py +80 -0
  10. dashboard/backend/app/repositories/workflow_repository.py +27 -0
  11. dashboard/backend/app/rest/__init__.py +8 -0
  12. dashboard/backend/app/rest/v1/__init__.py +12 -0
  13. dashboard/backend/app/rest/v1/health.py +33 -0
  14. dashboard/backend/app/rest/v1/runs.py +133 -0
  15. dashboard/backend/app/rest/v1/workflows.py +41 -0
  16. dashboard/backend/app/schemas/__init__.py +23 -0
  17. dashboard/backend/app/schemas/common.py +16 -0
  18. dashboard/backend/app/schemas/event.py +24 -0
  19. dashboard/backend/app/schemas/hook.py +25 -0
  20. dashboard/backend/app/schemas/run.py +54 -0
  21. dashboard/backend/app/schemas/step.py +28 -0
  22. dashboard/backend/app/schemas/workflow.py +31 -0
  23. dashboard/backend/app/server.py +87 -0
  24. dashboard/backend/app/services/__init__.py +6 -0
  25. dashboard/backend/app/services/run_service.py +240 -0
  26. dashboard/backend/app/services/workflow_service.py +155 -0
  27. dashboard/backend/main.py +18 -0
  28. docs/concepts/cancellation.mdx +362 -0
  29. docs/concepts/continue-as-new.mdx +434 -0
  30. docs/concepts/events.mdx +266 -0
  31. docs/concepts/fault-tolerance.mdx +370 -0
  32. docs/concepts/hooks.mdx +552 -0
  33. docs/concepts/limitations.mdx +167 -0
  34. docs/concepts/schedules.mdx +775 -0
  35. docs/concepts/sleep.mdx +312 -0
  36. docs/concepts/steps.mdx +301 -0
  37. docs/concepts/workflows.mdx +255 -0
  38. docs/guides/cli.mdx +942 -0
  39. docs/guides/configuration.mdx +560 -0
  40. docs/introduction.mdx +155 -0
  41. docs/quickstart.mdx +279 -0
  42. examples/__init__.py +1 -0
  43. examples/celery/__init__.py +1 -0
  44. examples/celery/durable/docker-compose.yml +55 -0
  45. examples/celery/durable/pyworkflow.config.yaml +12 -0
  46. examples/celery/durable/workflows/__init__.py +122 -0
  47. examples/celery/durable/workflows/basic.py +87 -0
  48. examples/celery/durable/workflows/batch_processing.py +102 -0
  49. examples/celery/durable/workflows/cancellation.py +273 -0
  50. examples/celery/durable/workflows/child_workflow_patterns.py +240 -0
  51. examples/celery/durable/workflows/child_workflows.py +202 -0
  52. examples/celery/durable/workflows/continue_as_new.py +260 -0
  53. examples/celery/durable/workflows/fault_tolerance.py +210 -0
  54. examples/celery/durable/workflows/hooks.py +211 -0
  55. examples/celery/durable/workflows/idempotency.py +112 -0
  56. examples/celery/durable/workflows/long_running.py +99 -0
  57. examples/celery/durable/workflows/retries.py +101 -0
  58. examples/celery/durable/workflows/schedules.py +209 -0
  59. examples/celery/transient/01_basic_workflow.py +91 -0
  60. examples/celery/transient/02_fault_tolerance.py +257 -0
  61. examples/celery/transient/__init__.py +20 -0
  62. examples/celery/transient/pyworkflow.config.yaml +25 -0
  63. examples/local/__init__.py +1 -0
  64. examples/local/durable/01_basic_workflow.py +94 -0
  65. examples/local/durable/02_file_storage.py +132 -0
  66. examples/local/durable/03_retries.py +169 -0
  67. examples/local/durable/04_long_running.py +119 -0
  68. examples/local/durable/05_event_log.py +145 -0
  69. examples/local/durable/06_idempotency.py +148 -0
  70. examples/local/durable/07_hooks.py +334 -0
  71. examples/local/durable/08_cancellation.py +233 -0
  72. examples/local/durable/09_child_workflows.py +198 -0
  73. examples/local/durable/10_child_workflow_patterns.py +265 -0
  74. examples/local/durable/11_continue_as_new.py +249 -0
  75. examples/local/durable/12_schedules.py +198 -0
  76. examples/local/durable/__init__.py +1 -0
  77. examples/local/transient/01_quick_tasks.py +87 -0
  78. examples/local/transient/02_retries.py +130 -0
  79. examples/local/transient/03_sleep.py +141 -0
  80. examples/local/transient/__init__.py +1 -0
  81. pyworkflow/__init__.py +256 -0
  82. pyworkflow/aws/__init__.py +68 -0
  83. pyworkflow/aws/context.py +234 -0
  84. pyworkflow/aws/handler.py +184 -0
  85. pyworkflow/aws/testing.py +310 -0
  86. pyworkflow/celery/__init__.py +41 -0
  87. pyworkflow/celery/app.py +198 -0
  88. pyworkflow/celery/scheduler.py +315 -0
  89. pyworkflow/celery/tasks.py +1746 -0
  90. pyworkflow/cli/__init__.py +132 -0
  91. pyworkflow/cli/__main__.py +6 -0
  92. pyworkflow/cli/commands/__init__.py +1 -0
  93. pyworkflow/cli/commands/hooks.py +640 -0
  94. pyworkflow/cli/commands/quickstart.py +495 -0
  95. pyworkflow/cli/commands/runs.py +773 -0
  96. pyworkflow/cli/commands/scheduler.py +130 -0
  97. pyworkflow/cli/commands/schedules.py +794 -0
  98. pyworkflow/cli/commands/setup.py +703 -0
  99. pyworkflow/cli/commands/worker.py +413 -0
  100. pyworkflow/cli/commands/workflows.py +1257 -0
  101. pyworkflow/cli/output/__init__.py +1 -0
  102. pyworkflow/cli/output/formatters.py +321 -0
  103. pyworkflow/cli/output/styles.py +121 -0
  104. pyworkflow/cli/utils/__init__.py +1 -0
  105. pyworkflow/cli/utils/async_helpers.py +30 -0
  106. pyworkflow/cli/utils/config.py +130 -0
  107. pyworkflow/cli/utils/config_generator.py +344 -0
  108. pyworkflow/cli/utils/discovery.py +53 -0
  109. pyworkflow/cli/utils/docker_manager.py +651 -0
  110. pyworkflow/cli/utils/interactive.py +364 -0
  111. pyworkflow/cli/utils/storage.py +115 -0
  112. pyworkflow/config.py +329 -0
  113. pyworkflow/context/__init__.py +63 -0
  114. pyworkflow/context/aws.py +230 -0
  115. pyworkflow/context/base.py +416 -0
  116. pyworkflow/context/local.py +930 -0
  117. pyworkflow/context/mock.py +381 -0
  118. pyworkflow/core/__init__.py +0 -0
  119. pyworkflow/core/exceptions.py +353 -0
  120. pyworkflow/core/registry.py +313 -0
  121. pyworkflow/core/scheduled.py +328 -0
  122. pyworkflow/core/step.py +494 -0
  123. pyworkflow/core/workflow.py +294 -0
  124. pyworkflow/discovery.py +248 -0
  125. pyworkflow/engine/__init__.py +0 -0
  126. pyworkflow/engine/events.py +879 -0
  127. pyworkflow/engine/executor.py +682 -0
  128. pyworkflow/engine/replay.py +273 -0
  129. pyworkflow/observability/__init__.py +19 -0
  130. pyworkflow/observability/logging.py +234 -0
  131. pyworkflow/primitives/__init__.py +33 -0
  132. pyworkflow/primitives/child_handle.py +174 -0
  133. pyworkflow/primitives/child_workflow.py +372 -0
  134. pyworkflow/primitives/continue_as_new.py +101 -0
  135. pyworkflow/primitives/define_hook.py +150 -0
  136. pyworkflow/primitives/hooks.py +97 -0
  137. pyworkflow/primitives/resume_hook.py +210 -0
  138. pyworkflow/primitives/schedule.py +545 -0
  139. pyworkflow/primitives/shield.py +96 -0
  140. pyworkflow/primitives/sleep.py +100 -0
  141. pyworkflow/runtime/__init__.py +21 -0
  142. pyworkflow/runtime/base.py +179 -0
  143. pyworkflow/runtime/celery.py +310 -0
  144. pyworkflow/runtime/factory.py +101 -0
  145. pyworkflow/runtime/local.py +706 -0
  146. pyworkflow/scheduler/__init__.py +9 -0
  147. pyworkflow/scheduler/local.py +248 -0
  148. pyworkflow/serialization/__init__.py +0 -0
  149. pyworkflow/serialization/decoder.py +146 -0
  150. pyworkflow/serialization/encoder.py +162 -0
  151. pyworkflow/storage/__init__.py +54 -0
  152. pyworkflow/storage/base.py +612 -0
  153. pyworkflow/storage/config.py +185 -0
  154. pyworkflow/storage/dynamodb.py +1315 -0
  155. pyworkflow/storage/file.py +827 -0
  156. pyworkflow/storage/memory.py +549 -0
  157. pyworkflow/storage/postgres.py +1161 -0
  158. pyworkflow/storage/schemas.py +486 -0
  159. pyworkflow/storage/sqlite.py +1136 -0
  160. pyworkflow/utils/__init__.py +0 -0
  161. pyworkflow/utils/duration.py +177 -0
  162. pyworkflow/utils/schedule.py +391 -0
  163. pyworkflow_engine-0.1.7.dist-info/METADATA +687 -0
  164. pyworkflow_engine-0.1.7.dist-info/RECORD +196 -0
  165. pyworkflow_engine-0.1.7.dist-info/WHEEL +5 -0
  166. pyworkflow_engine-0.1.7.dist-info/entry_points.txt +2 -0
  167. pyworkflow_engine-0.1.7.dist-info/licenses/LICENSE +21 -0
  168. pyworkflow_engine-0.1.7.dist-info/top_level.txt +5 -0
  169. tests/examples/__init__.py +0 -0
  170. tests/integration/__init__.py +0 -0
  171. tests/integration/test_cancellation.py +330 -0
  172. tests/integration/test_child_workflows.py +439 -0
  173. tests/integration/test_continue_as_new.py +428 -0
  174. tests/integration/test_dynamodb_storage.py +1146 -0
  175. tests/integration/test_fault_tolerance.py +369 -0
  176. tests/integration/test_schedule_storage.py +484 -0
  177. tests/unit/__init__.py +0 -0
  178. tests/unit/backends/__init__.py +1 -0
  179. tests/unit/backends/test_dynamodb_storage.py +1554 -0
  180. tests/unit/backends/test_postgres_storage.py +1281 -0
  181. tests/unit/backends/test_sqlite_storage.py +1460 -0
  182. tests/unit/conftest.py +41 -0
  183. tests/unit/test_cancellation.py +364 -0
  184. tests/unit/test_child_workflows.py +680 -0
  185. tests/unit/test_continue_as_new.py +441 -0
  186. tests/unit/test_event_limits.py +316 -0
  187. tests/unit/test_executor.py +320 -0
  188. tests/unit/test_fault_tolerance.py +334 -0
  189. tests/unit/test_hooks.py +495 -0
  190. tests/unit/test_registry.py +261 -0
  191. tests/unit/test_replay.py +420 -0
  192. tests/unit/test_schedule_schemas.py +285 -0
  193. tests/unit/test_schedule_utils.py +286 -0
  194. tests/unit/test_scheduled_workflow.py +274 -0
  195. tests/unit/test_step.py +353 -0
  196. tests/unit/test_workflow.py +243 -0
@@ -0,0 +1,344 @@
1
+ """
2
+ Configuration file generation utilities.
3
+
4
+ This module provides functions for generating and managing pyworkflow.config.yaml
5
+ configuration files.
6
+ """
7
+
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import yaml
13
+
14
+
15
+ def generate_yaml_config(
16
+ module: str | None,
17
+ runtime: str,
18
+ storage_type: str,
19
+ storage_path: str | None,
20
+ broker_url: str,
21
+ result_backend: str,
22
+ postgres_host: str | None = None,
23
+ postgres_port: str | None = None,
24
+ postgres_user: str | None = None,
25
+ postgres_password: str | None = None,
26
+ postgres_database: str | None = None,
27
+ dynamodb_table_name: str | None = None,
28
+ dynamodb_region: str | None = None,
29
+ dynamodb_endpoint_url: str | None = None,
30
+ ) -> str:
31
+ """
32
+ Generate YAML configuration content.
33
+
34
+ Args:
35
+ module: Optional workflow module path (e.g., "myapp.workflows")
36
+ runtime: Runtime type (e.g., "celery", "local")
37
+ storage_type: Storage backend type (e.g., "sqlite", "file", "memory", "postgres")
38
+ storage_path: Optional storage path for file/sqlite backends
39
+ broker_url: Celery broker URL
40
+ result_backend: Celery result backend URL
41
+ postgres_host: PostgreSQL host (for postgres backend)
42
+ postgres_port: PostgreSQL port (for postgres backend)
43
+ postgres_user: PostgreSQL user (for postgres backend)
44
+ postgres_password: PostgreSQL password (for postgres backend)
45
+ postgres_database: PostgreSQL database name (for postgres backend)
46
+ dynamodb_table_name: Optional DynamoDB table name
47
+ dynamodb_region: Optional AWS region for DynamoDB
48
+ dynamodb_endpoint_url: Optional local DynamoDB endpoint URL
49
+
50
+ Returns:
51
+ YAML configuration as string
52
+
53
+ Example:
54
+ >>> yaml_content = generate_yaml_config(
55
+ ... module="myapp.workflows",
56
+ ... runtime="celery",
57
+ ... storage_type="sqlite",
58
+ ... storage_path="./pyworkflow_data/pyworkflow.db",
59
+ ... broker_url="redis://localhost:6379/0",
60
+ ... result_backend="redis://localhost:6379/1"
61
+ ... )
62
+ """
63
+ config: dict[str, Any] = {}
64
+
65
+ # Add module if provided
66
+ if module:
67
+ config["module"] = module
68
+
69
+ # Runtime configuration
70
+ config["runtime"] = runtime
71
+
72
+ # Storage configuration
73
+ storage_config: dict[str, Any] = {"type": storage_type}
74
+ if storage_path and storage_type in ["file", "sqlite"]:
75
+ storage_config["base_path"] = storage_path
76
+ if storage_type == "postgres":
77
+ if postgres_host:
78
+ storage_config["host"] = postgres_host
79
+ if postgres_port:
80
+ storage_config["port"] = int(postgres_port)
81
+ if postgres_user:
82
+ storage_config["user"] = postgres_user
83
+ if postgres_password:
84
+ storage_config["password"] = postgres_password
85
+ if postgres_database:
86
+ storage_config["database"] = postgres_database
87
+ elif storage_type == "dynamodb":
88
+ if dynamodb_table_name:
89
+ storage_config["table_name"] = dynamodb_table_name
90
+ if dynamodb_region:
91
+ storage_config["region"] = dynamodb_region
92
+ if dynamodb_endpoint_url:
93
+ storage_config["endpoint_url"] = dynamodb_endpoint_url
94
+ config["storage"] = storage_config
95
+
96
+ # Celery configuration (only for celery runtime)
97
+ if runtime == "celery":
98
+ config["celery"] = {
99
+ "broker": broker_url,
100
+ "result_backend": result_backend,
101
+ }
102
+
103
+ # Generate YAML with header comment
104
+ header = f"""# PyWorkflow Configuration
105
+ # Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
106
+ # Documentation: https://docs.pyworkflow.dev
107
+
108
+ """
109
+
110
+ # Convert to YAML with nice formatting
111
+ yaml_content = yaml.dump(
112
+ config,
113
+ default_flow_style=False,
114
+ sort_keys=False,
115
+ allow_unicode=True,
116
+ )
117
+
118
+ return header + yaml_content
119
+
120
+
121
+ def write_yaml_config(
122
+ config_content: str,
123
+ path: Path | None = None,
124
+ backup: bool = True,
125
+ ) -> Path:
126
+ """
127
+ Write YAML configuration to file.
128
+
129
+ Args:
130
+ config_content: YAML content string
131
+ path: Target file path (default: ./pyworkflow.config.yaml)
132
+ backup: If True, backup existing config before overwriting
133
+
134
+ Returns:
135
+ Path to written config file
136
+
137
+ Example:
138
+ >>> yaml_content = generate_yaml_config(...)
139
+ >>> config_path = write_yaml_config(yaml_content)
140
+ >>> print(f"Config written to {config_path}")
141
+ """
142
+ path = Path.cwd() / "pyworkflow.config.yaml" if path is None else Path(path)
143
+
144
+ # Backup existing config if requested
145
+ if backup and path.exists():
146
+ backup_path = path.with_suffix(".yaml.backup")
147
+ backup_path.write_text(path.read_text())
148
+
149
+ # Write new config
150
+ path.write_text(config_content)
151
+
152
+ return path
153
+
154
+
155
+ def load_yaml_config(path: Path | None = None) -> dict[str, Any]:
156
+ """
157
+ Load YAML configuration from file.
158
+
159
+ Args:
160
+ path: Config file path (default: ./pyworkflow.config.yaml)
161
+
162
+ Returns:
163
+ Configuration dictionary
164
+
165
+ Raises:
166
+ FileNotFoundError: If config file doesn't exist
167
+ ValueError: If YAML is invalid
168
+
169
+ Example:
170
+ >>> config = load_yaml_config()
171
+ >>> print(config.get("runtime"))
172
+ celery
173
+ """
174
+ path = Path.cwd() / "pyworkflow.config.yaml" if path is None else Path(path)
175
+
176
+ if not path.exists():
177
+ raise FileNotFoundError(f"Config file not found: {path}")
178
+
179
+ try:
180
+ with open(path) as f:
181
+ config = yaml.safe_load(f)
182
+
183
+ # Ensure it's a dict
184
+ if not isinstance(config, dict):
185
+ raise ValueError("Config file must contain a YAML dictionary")
186
+
187
+ return config
188
+ except yaml.YAMLError as e:
189
+ raise ValueError(f"Invalid YAML in config file: {e}")
190
+
191
+
192
+ def find_yaml_config(start_path: Path | None = None) -> Path | None:
193
+ """
194
+ Find pyworkflow.config.yaml in current directory or parents.
195
+
196
+ Args:
197
+ start_path: Starting directory (default: current working directory)
198
+
199
+ Returns:
200
+ Path to config file if found, None otherwise
201
+
202
+ Example:
203
+ >>> config_path = find_yaml_config()
204
+ >>> if config_path:
205
+ ... print(f"Found config at {config_path}")
206
+ """
207
+ start_path = Path.cwd() if start_path is None else Path(start_path)
208
+
209
+ # Search current directory and parents
210
+ for directory in [start_path] + list(start_path.parents):
211
+ config_path = directory / "pyworkflow.config.yaml"
212
+ if config_path.exists():
213
+ return config_path
214
+
215
+ return None
216
+
217
+
218
+ def display_config_summary(config: dict[str, Any]) -> list[str]:
219
+ """
220
+ Generate a human-readable summary of configuration.
221
+
222
+ Args:
223
+ config: Configuration dictionary
224
+
225
+ Returns:
226
+ List of summary lines
227
+
228
+ Example:
229
+ >>> config = {"runtime": "celery", "storage": {"type": "sqlite"}}
230
+ >>> for line in display_config_summary(config):
231
+ ... print(line)
232
+ """
233
+ lines = []
234
+ lines.append("Configuration Summary:")
235
+ lines.append("=" * 50)
236
+
237
+ # Module
238
+ if "module" in config:
239
+ lines.append(f" Workflow Module: {config['module']}")
240
+ else:
241
+ lines.append(" Workflow Module: (not configured)")
242
+
243
+ # Runtime
244
+ runtime = config.get("runtime", "local")
245
+ lines.append(f" Runtime: {runtime}")
246
+
247
+ # Storage
248
+ storage = config.get("storage", {})
249
+ storage_type = storage.get("type", "file")
250
+ lines.append(f" Storage Type: {storage_type}")
251
+
252
+ if "base_path" in storage:
253
+ lines.append(f" Storage Path: {storage['base_path']}")
254
+ if storage_type == "postgres":
255
+ host = storage.get("host", "localhost")
256
+ port = storage.get("port", 5432)
257
+ database = storage.get("database", "pyworkflow")
258
+ user = storage.get("user", "pyworkflow")
259
+ lines.append(f" PostgreSQL: {user}@{host}:{port}/{database}")
260
+
261
+ # DynamoDB-specific config
262
+ if storage_type == "dynamodb":
263
+ if "table_name" in storage:
264
+ lines.append(f" DynamoDB Table: {storage['table_name']}")
265
+ if "region" in storage:
266
+ lines.append(f" AWS Region: {storage['region']}")
267
+ if "endpoint_url" in storage:
268
+ lines.append(f" Endpoint URL: {storage['endpoint_url']}")
269
+
270
+ # Celery (if applicable)
271
+ if runtime == "celery" and "celery" in config:
272
+ celery = config["celery"]
273
+ lines.append(f" Broker: {celery.get('broker', 'N/A')}")
274
+ lines.append(f" Result Backend: {celery.get('result_backend', 'N/A')}")
275
+
276
+ lines.append("=" * 50)
277
+
278
+ return lines
279
+
280
+
281
+ def validate_config(config: dict[str, Any]) -> tuple[bool, list[str]]:
282
+ """
283
+ Validate configuration dictionary.
284
+
285
+ Args:
286
+ config: Configuration dictionary to validate
287
+
288
+ Returns:
289
+ Tuple of (is_valid, list of error messages)
290
+
291
+ Example:
292
+ >>> config = {"runtime": "celery"}
293
+ >>> valid, errors = validate_config(config)
294
+ >>> if not valid:
295
+ ... for error in errors:
296
+ ... print(f"Error: {error}")
297
+ """
298
+ errors = []
299
+
300
+ # Check runtime
301
+ runtime = config.get("runtime")
302
+ if not runtime:
303
+ errors.append("Missing 'runtime' configuration")
304
+ elif runtime not in ["local", "celery"]:
305
+ errors.append(f"Invalid runtime: {runtime}. Must be 'local' or 'celery'")
306
+
307
+ # Check storage
308
+ storage = config.get("storage")
309
+ if not storage:
310
+ errors.append("Missing 'storage' configuration")
311
+ elif not isinstance(storage, dict):
312
+ errors.append("'storage' must be a dictionary")
313
+ else:
314
+ storage_type = storage.get("type")
315
+ if not storage_type:
316
+ errors.append("Missing storage 'type'")
317
+ elif storage_type not in ["file", "memory", "sqlite", "redis", "postgres", "dynamodb"]:
318
+ errors.append(
319
+ f"Invalid storage type: {storage_type}. "
320
+ "Must be 'file', 'memory', 'sqlite', 'redis', 'postgres' or 'dynamodb'"
321
+ )
322
+
323
+ # Check Celery config if using celery runtime
324
+ if runtime == "celery":
325
+ celery = config.get("celery")
326
+ if not celery:
327
+ errors.append("Missing 'celery' configuration for celery runtime")
328
+ elif not isinstance(celery, dict):
329
+ errors.append("'celery' must be a dictionary")
330
+ else:
331
+ if not celery.get("broker"):
332
+ errors.append("Missing celery 'broker' URL")
333
+ if not celery.get("result_backend"):
334
+ errors.append("Missing celery 'result_backend' URL")
335
+
336
+ # Module validation (optional but if present should be valid)
337
+ if "module" in config:
338
+ module = config["module"]
339
+ if not isinstance(module, str):
340
+ errors.append("'module' must be a string")
341
+ elif " " in module:
342
+ errors.append("'module' path cannot contain spaces")
343
+
344
+ return len(errors) == 0, errors
@@ -0,0 +1,53 @@
1
+ """
2
+ CLI-specific workflow discovery utilities.
3
+
4
+ This module provides CLI-friendly wrappers around the core discovery functions,
5
+ converting DiscoveryError to click.ClickException for proper CLI error handling.
6
+ """
7
+
8
+ import click
9
+
10
+ # Re-export core discovery functions
11
+ from pyworkflow.discovery import (
12
+ DiscoveryError,
13
+ _ensure_project_in_path, # noqa: F401
14
+ _find_project_root, # noqa: F401
15
+ _import_module, # noqa: F401
16
+ _load_yaml_config, # noqa: F401
17
+ )
18
+ from pyworkflow.discovery import (
19
+ discover_workflows as _discover_workflows,
20
+ )
21
+
22
+ __all__ = [
23
+ "discover_workflows",
24
+ "DiscoveryError",
25
+ "_find_project_root",
26
+ "_ensure_project_in_path",
27
+ "_import_module",
28
+ "_load_yaml_config",
29
+ ]
30
+
31
+
32
+ def discover_workflows(
33
+ module_path: str | None = None,
34
+ config: dict | None = None,
35
+ config_path: str | None = None,
36
+ ) -> None:
37
+ """
38
+ CLI-friendly wrapper for discover_workflows.
39
+
40
+ Converts DiscoveryError to click.ClickException for proper CLI error handling.
41
+
42
+ Args:
43
+ module_path: Explicit module path to import
44
+ config: Configuration dict containing 'module' or 'modules' key
45
+ config_path: Path to the config file for sys.path resolution
46
+
47
+ Raises:
48
+ click.ClickException: If workflow discovery fails
49
+ """
50
+ try:
51
+ _discover_workflows(module_path=module_path, config=config, config_path=config_path)
52
+ except DiscoveryError as e:
53
+ raise click.ClickException(str(e))