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.
- dashboard/backend/app/__init__.py +1 -0
- dashboard/backend/app/config.py +32 -0
- dashboard/backend/app/controllers/__init__.py +6 -0
- dashboard/backend/app/controllers/run_controller.py +86 -0
- dashboard/backend/app/controllers/workflow_controller.py +33 -0
- dashboard/backend/app/dependencies/__init__.py +5 -0
- dashboard/backend/app/dependencies/storage.py +50 -0
- dashboard/backend/app/repositories/__init__.py +6 -0
- dashboard/backend/app/repositories/run_repository.py +80 -0
- dashboard/backend/app/repositories/workflow_repository.py +27 -0
- dashboard/backend/app/rest/__init__.py +8 -0
- dashboard/backend/app/rest/v1/__init__.py +12 -0
- dashboard/backend/app/rest/v1/health.py +33 -0
- dashboard/backend/app/rest/v1/runs.py +133 -0
- dashboard/backend/app/rest/v1/workflows.py +41 -0
- dashboard/backend/app/schemas/__init__.py +23 -0
- dashboard/backend/app/schemas/common.py +16 -0
- dashboard/backend/app/schemas/event.py +24 -0
- dashboard/backend/app/schemas/hook.py +25 -0
- dashboard/backend/app/schemas/run.py +54 -0
- dashboard/backend/app/schemas/step.py +28 -0
- dashboard/backend/app/schemas/workflow.py +31 -0
- dashboard/backend/app/server.py +87 -0
- dashboard/backend/app/services/__init__.py +6 -0
- dashboard/backend/app/services/run_service.py +240 -0
- dashboard/backend/app/services/workflow_service.py +155 -0
- dashboard/backend/main.py +18 -0
- docs/concepts/cancellation.mdx +362 -0
- docs/concepts/continue-as-new.mdx +434 -0
- docs/concepts/events.mdx +266 -0
- docs/concepts/fault-tolerance.mdx +370 -0
- docs/concepts/hooks.mdx +552 -0
- docs/concepts/limitations.mdx +167 -0
- docs/concepts/schedules.mdx +775 -0
- docs/concepts/sleep.mdx +312 -0
- docs/concepts/steps.mdx +301 -0
- docs/concepts/workflows.mdx +255 -0
- docs/guides/cli.mdx +942 -0
- docs/guides/configuration.mdx +560 -0
- docs/introduction.mdx +155 -0
- docs/quickstart.mdx +279 -0
- examples/__init__.py +1 -0
- examples/celery/__init__.py +1 -0
- examples/celery/durable/docker-compose.yml +55 -0
- examples/celery/durable/pyworkflow.config.yaml +12 -0
- examples/celery/durable/workflows/__init__.py +122 -0
- examples/celery/durable/workflows/basic.py +87 -0
- examples/celery/durable/workflows/batch_processing.py +102 -0
- examples/celery/durable/workflows/cancellation.py +273 -0
- examples/celery/durable/workflows/child_workflow_patterns.py +240 -0
- examples/celery/durable/workflows/child_workflows.py +202 -0
- examples/celery/durable/workflows/continue_as_new.py +260 -0
- examples/celery/durable/workflows/fault_tolerance.py +210 -0
- examples/celery/durable/workflows/hooks.py +211 -0
- examples/celery/durable/workflows/idempotency.py +112 -0
- examples/celery/durable/workflows/long_running.py +99 -0
- examples/celery/durable/workflows/retries.py +101 -0
- examples/celery/durable/workflows/schedules.py +209 -0
- examples/celery/transient/01_basic_workflow.py +91 -0
- examples/celery/transient/02_fault_tolerance.py +257 -0
- examples/celery/transient/__init__.py +20 -0
- examples/celery/transient/pyworkflow.config.yaml +25 -0
- examples/local/__init__.py +1 -0
- examples/local/durable/01_basic_workflow.py +94 -0
- examples/local/durable/02_file_storage.py +132 -0
- examples/local/durable/03_retries.py +169 -0
- examples/local/durable/04_long_running.py +119 -0
- examples/local/durable/05_event_log.py +145 -0
- examples/local/durable/06_idempotency.py +148 -0
- examples/local/durable/07_hooks.py +334 -0
- examples/local/durable/08_cancellation.py +233 -0
- examples/local/durable/09_child_workflows.py +198 -0
- examples/local/durable/10_child_workflow_patterns.py +265 -0
- examples/local/durable/11_continue_as_new.py +249 -0
- examples/local/durable/12_schedules.py +198 -0
- examples/local/durable/__init__.py +1 -0
- examples/local/transient/01_quick_tasks.py +87 -0
- examples/local/transient/02_retries.py +130 -0
- examples/local/transient/03_sleep.py +141 -0
- examples/local/transient/__init__.py +1 -0
- pyworkflow/__init__.py +256 -0
- pyworkflow/aws/__init__.py +68 -0
- pyworkflow/aws/context.py +234 -0
- pyworkflow/aws/handler.py +184 -0
- pyworkflow/aws/testing.py +310 -0
- pyworkflow/celery/__init__.py +41 -0
- pyworkflow/celery/app.py +198 -0
- pyworkflow/celery/scheduler.py +315 -0
- pyworkflow/celery/tasks.py +1746 -0
- pyworkflow/cli/__init__.py +132 -0
- pyworkflow/cli/__main__.py +6 -0
- pyworkflow/cli/commands/__init__.py +1 -0
- pyworkflow/cli/commands/hooks.py +640 -0
- pyworkflow/cli/commands/quickstart.py +495 -0
- pyworkflow/cli/commands/runs.py +773 -0
- pyworkflow/cli/commands/scheduler.py +130 -0
- pyworkflow/cli/commands/schedules.py +794 -0
- pyworkflow/cli/commands/setup.py +703 -0
- pyworkflow/cli/commands/worker.py +413 -0
- pyworkflow/cli/commands/workflows.py +1257 -0
- pyworkflow/cli/output/__init__.py +1 -0
- pyworkflow/cli/output/formatters.py +321 -0
- pyworkflow/cli/output/styles.py +121 -0
- pyworkflow/cli/utils/__init__.py +1 -0
- pyworkflow/cli/utils/async_helpers.py +30 -0
- pyworkflow/cli/utils/config.py +130 -0
- pyworkflow/cli/utils/config_generator.py +344 -0
- pyworkflow/cli/utils/discovery.py +53 -0
- pyworkflow/cli/utils/docker_manager.py +651 -0
- pyworkflow/cli/utils/interactive.py +364 -0
- pyworkflow/cli/utils/storage.py +115 -0
- pyworkflow/config.py +329 -0
- pyworkflow/context/__init__.py +63 -0
- pyworkflow/context/aws.py +230 -0
- pyworkflow/context/base.py +416 -0
- pyworkflow/context/local.py +930 -0
- pyworkflow/context/mock.py +381 -0
- pyworkflow/core/__init__.py +0 -0
- pyworkflow/core/exceptions.py +353 -0
- pyworkflow/core/registry.py +313 -0
- pyworkflow/core/scheduled.py +328 -0
- pyworkflow/core/step.py +494 -0
- pyworkflow/core/workflow.py +294 -0
- pyworkflow/discovery.py +248 -0
- pyworkflow/engine/__init__.py +0 -0
- pyworkflow/engine/events.py +879 -0
- pyworkflow/engine/executor.py +682 -0
- pyworkflow/engine/replay.py +273 -0
- pyworkflow/observability/__init__.py +19 -0
- pyworkflow/observability/logging.py +234 -0
- pyworkflow/primitives/__init__.py +33 -0
- pyworkflow/primitives/child_handle.py +174 -0
- pyworkflow/primitives/child_workflow.py +372 -0
- pyworkflow/primitives/continue_as_new.py +101 -0
- pyworkflow/primitives/define_hook.py +150 -0
- pyworkflow/primitives/hooks.py +97 -0
- pyworkflow/primitives/resume_hook.py +210 -0
- pyworkflow/primitives/schedule.py +545 -0
- pyworkflow/primitives/shield.py +96 -0
- pyworkflow/primitives/sleep.py +100 -0
- pyworkflow/runtime/__init__.py +21 -0
- pyworkflow/runtime/base.py +179 -0
- pyworkflow/runtime/celery.py +310 -0
- pyworkflow/runtime/factory.py +101 -0
- pyworkflow/runtime/local.py +706 -0
- pyworkflow/scheduler/__init__.py +9 -0
- pyworkflow/scheduler/local.py +248 -0
- pyworkflow/serialization/__init__.py +0 -0
- pyworkflow/serialization/decoder.py +146 -0
- pyworkflow/serialization/encoder.py +162 -0
- pyworkflow/storage/__init__.py +54 -0
- pyworkflow/storage/base.py +612 -0
- pyworkflow/storage/config.py +185 -0
- pyworkflow/storage/dynamodb.py +1315 -0
- pyworkflow/storage/file.py +827 -0
- pyworkflow/storage/memory.py +549 -0
- pyworkflow/storage/postgres.py +1161 -0
- pyworkflow/storage/schemas.py +486 -0
- pyworkflow/storage/sqlite.py +1136 -0
- pyworkflow/utils/__init__.py +0 -0
- pyworkflow/utils/duration.py +177 -0
- pyworkflow/utils/schedule.py +391 -0
- pyworkflow_engine-0.1.7.dist-info/METADATA +687 -0
- pyworkflow_engine-0.1.7.dist-info/RECORD +196 -0
- pyworkflow_engine-0.1.7.dist-info/WHEEL +5 -0
- pyworkflow_engine-0.1.7.dist-info/entry_points.txt +2 -0
- pyworkflow_engine-0.1.7.dist-info/licenses/LICENSE +21 -0
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +5 -0
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +330 -0
- tests/integration/test_child_workflows.py +439 -0
- tests/integration/test_continue_as_new.py +428 -0
- tests/integration/test_dynamodb_storage.py +1146 -0
- tests/integration/test_fault_tolerance.py +369 -0
- tests/integration/test_schedule_storage.py +484 -0
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +1 -0
- tests/unit/backends/test_dynamodb_storage.py +1554 -0
- tests/unit/backends/test_postgres_storage.py +1281 -0
- tests/unit/backends/test_sqlite_storage.py +1460 -0
- tests/unit/conftest.py +41 -0
- tests/unit/test_cancellation.py +364 -0
- tests/unit/test_child_workflows.py +680 -0
- tests/unit/test_continue_as_new.py +441 -0
- tests/unit/test_event_limits.py +316 -0
- tests/unit/test_executor.py +320 -0
- tests/unit/test_fault_tolerance.py +334 -0
- tests/unit/test_hooks.py +495 -0
- tests/unit/test_registry.py +261 -0
- tests/unit/test_replay.py +420 -0
- tests/unit/test_schedule_schemas.py +285 -0
- tests/unit/test_schedule_utils.py +286 -0
- tests/unit/test_scheduled_workflow.py +274 -0
- tests/unit/test_step.py +353 -0
- 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))
|