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,495 @@
|
|
|
1
|
+
"""Quickstart command to scaffold a new PyWorkflow project."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from pyworkflow.cli.output.formatters import (
|
|
9
|
+
print_error,
|
|
10
|
+
print_info,
|
|
11
|
+
print_success,
|
|
12
|
+
print_warning,
|
|
13
|
+
)
|
|
14
|
+
from pyworkflow.cli.utils.config_generator import (
|
|
15
|
+
generate_yaml_config,
|
|
16
|
+
write_yaml_config,
|
|
17
|
+
)
|
|
18
|
+
from pyworkflow.cli.utils.docker_manager import (
|
|
19
|
+
check_docker_available,
|
|
20
|
+
check_service_health,
|
|
21
|
+
generate_docker_compose_content,
|
|
22
|
+
run_docker_command,
|
|
23
|
+
write_docker_compose,
|
|
24
|
+
)
|
|
25
|
+
from pyworkflow.cli.utils.interactive import (
|
|
26
|
+
confirm,
|
|
27
|
+
select,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Template content for sample workflows
|
|
31
|
+
WORKFLOWS_INIT_TEMPLATE = '''"""Project workflows."""
|
|
32
|
+
from .orders import process_order
|
|
33
|
+
from .notifications import send_notification
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"process_order",
|
|
37
|
+
"send_notification",
|
|
38
|
+
]
|
|
39
|
+
'''
|
|
40
|
+
|
|
41
|
+
ORDERS_WORKFLOW_TEMPLATE = '''"""Order processing workflow example."""
|
|
42
|
+
from pyworkflow import workflow, step
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@step()
|
|
46
|
+
async def validate_order(order_id: str) -> dict:
|
|
47
|
+
"""Validate the order exists and is valid."""
|
|
48
|
+
# In a real app, check database or external service
|
|
49
|
+
return {"order_id": order_id, "valid": True}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@step()
|
|
53
|
+
async def process_payment(order_id: str, amount: float) -> dict:
|
|
54
|
+
"""Process payment for the order."""
|
|
55
|
+
# In a real app, integrate with payment provider
|
|
56
|
+
return {"order_id": order_id, "payment_status": "completed", "amount": amount}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@step()
|
|
60
|
+
async def update_inventory(order_id: str) -> dict:
|
|
61
|
+
"""Update inventory after order."""
|
|
62
|
+
# In a real app, update inventory database
|
|
63
|
+
return {"order_id": order_id, "inventory_updated": True}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@workflow()
|
|
67
|
+
async def process_order(order_id: str, amount: float = 99.99) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Process an order through validation, payment, and inventory update.
|
|
70
|
+
|
|
71
|
+
This is a sample workflow demonstrating:
|
|
72
|
+
- Multiple steps executed in sequence
|
|
73
|
+
- Data passing between steps
|
|
74
|
+
- Automatic retry on failures
|
|
75
|
+
|
|
76
|
+
Run with:
|
|
77
|
+
pyworkflow workflows run process_order --input '{"order_id": "123", "amount": 49.99}'
|
|
78
|
+
"""
|
|
79
|
+
validation = await validate_order(order_id)
|
|
80
|
+
if not validation["valid"]:
|
|
81
|
+
raise ValueError(f"Order {order_id} is not valid")
|
|
82
|
+
|
|
83
|
+
payment = await process_payment(order_id, amount)
|
|
84
|
+
inventory = await update_inventory(order_id)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"order_id": order_id,
|
|
88
|
+
"status": "completed",
|
|
89
|
+
"payment": payment,
|
|
90
|
+
"inventory": inventory,
|
|
91
|
+
}
|
|
92
|
+
'''
|
|
93
|
+
|
|
94
|
+
NOTIFICATIONS_WORKFLOW_TEMPLATE = '''"""Notification workflow example."""
|
|
95
|
+
from pyworkflow import workflow, step, sleep
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@step()
|
|
99
|
+
async def send_email(to: str, subject: str, body: str) -> dict:
|
|
100
|
+
"""Send an email notification."""
|
|
101
|
+
# In a real app, integrate with email service (SendGrid, SES, etc.)
|
|
102
|
+
return {"sent": True, "to": to, "subject": subject}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@step()
|
|
106
|
+
async def send_sms(phone: str, message: str) -> dict:
|
|
107
|
+
"""Send an SMS notification."""
|
|
108
|
+
# In a real app, integrate with SMS service (Twilio, etc.)
|
|
109
|
+
return {"sent": True, "phone": phone}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@workflow()
|
|
113
|
+
async def send_notification(
|
|
114
|
+
user_id: str,
|
|
115
|
+
message: str,
|
|
116
|
+
channels: list[str] | None = None,
|
|
117
|
+
) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Send notifications through multiple channels.
|
|
120
|
+
|
|
121
|
+
This workflow demonstrates:
|
|
122
|
+
- Conditional step execution
|
|
123
|
+
- Sleep/delay functionality
|
|
124
|
+
- Multiple notification channels
|
|
125
|
+
|
|
126
|
+
Run with:
|
|
127
|
+
pyworkflow workflows run send_notification --input '{"user_id": "user-123", "message": "Hello!"}'
|
|
128
|
+
"""
|
|
129
|
+
if channels is None:
|
|
130
|
+
channels = ["email"]
|
|
131
|
+
|
|
132
|
+
results = {"user_id": user_id, "notifications": []}
|
|
133
|
+
|
|
134
|
+
if "email" in channels:
|
|
135
|
+
email_result = await send_email(
|
|
136
|
+
to=f"{user_id}@example.com",
|
|
137
|
+
subject="Notification",
|
|
138
|
+
body=message,
|
|
139
|
+
)
|
|
140
|
+
results["notifications"].append({"channel": "email", **email_result})
|
|
141
|
+
|
|
142
|
+
if "sms" in channels:
|
|
143
|
+
# Add small delay between channels
|
|
144
|
+
await sleep("1s")
|
|
145
|
+
sms_result = await send_sms(
|
|
146
|
+
phone="+1234567890",
|
|
147
|
+
message=message,
|
|
148
|
+
)
|
|
149
|
+
results["notifications"].append({"channel": "sms", **sms_result})
|
|
150
|
+
|
|
151
|
+
return results
|
|
152
|
+
'''
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _check_sqlite_available() -> bool:
|
|
156
|
+
"""Check if SQLite is available in the Python build."""
|
|
157
|
+
try:
|
|
158
|
+
import sqlite3 # noqa: F401
|
|
159
|
+
|
|
160
|
+
return True
|
|
161
|
+
except ImportError:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@click.command(name="quickstart")
|
|
166
|
+
@click.option(
|
|
167
|
+
"--non-interactive",
|
|
168
|
+
is_flag=True,
|
|
169
|
+
help="Run without prompts (use defaults)",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--skip-docker",
|
|
173
|
+
is_flag=True,
|
|
174
|
+
help="Skip Docker services setup",
|
|
175
|
+
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--template",
|
|
178
|
+
type=click.Choice(["basic"]),
|
|
179
|
+
default="basic",
|
|
180
|
+
help="Project template to use",
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
"--storage",
|
|
184
|
+
type=click.Choice(["sqlite", "file"], case_sensitive=False),
|
|
185
|
+
help="Storage backend type",
|
|
186
|
+
)
|
|
187
|
+
@click.pass_context
|
|
188
|
+
def quickstart(
|
|
189
|
+
ctx: click.Context,
|
|
190
|
+
non_interactive: bool,
|
|
191
|
+
skip_docker: bool,
|
|
192
|
+
template: str,
|
|
193
|
+
storage: str | None,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Create a new PyWorkflow project with sample workflows.
|
|
197
|
+
|
|
198
|
+
This command will:
|
|
199
|
+
1. Create a workflows/ package with sample workflows
|
|
200
|
+
2. Generate pyworkflow.config.yaml
|
|
201
|
+
3. Optionally start Docker services (Redis + Dashboard)
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
204
|
+
|
|
205
|
+
# Interactive quickstart
|
|
206
|
+
$ pyworkflow quickstart
|
|
207
|
+
|
|
208
|
+
# Non-interactive with defaults
|
|
209
|
+
$ pyworkflow quickstart --non-interactive
|
|
210
|
+
|
|
211
|
+
# Without Docker
|
|
212
|
+
$ pyworkflow quickstart --skip-docker
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
_run_quickstart(
|
|
216
|
+
ctx=ctx,
|
|
217
|
+
non_interactive=non_interactive,
|
|
218
|
+
skip_docker=skip_docker,
|
|
219
|
+
template=template,
|
|
220
|
+
storage_override=storage,
|
|
221
|
+
)
|
|
222
|
+
except click.Abort:
|
|
223
|
+
print_warning("\nQuickstart cancelled by user")
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
print_error(f"\nQuickstart failed: {str(e)}")
|
|
227
|
+
if ctx.obj and ctx.obj.get("verbose"):
|
|
228
|
+
raise
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _run_quickstart(
|
|
233
|
+
ctx: click.Context,
|
|
234
|
+
non_interactive: bool,
|
|
235
|
+
skip_docker: bool,
|
|
236
|
+
template: str,
|
|
237
|
+
storage_override: str | None,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Main quickstart workflow."""
|
|
240
|
+
# 1. Welcome banner
|
|
241
|
+
_print_welcome()
|
|
242
|
+
|
|
243
|
+
# 2. Check for existing files
|
|
244
|
+
cwd = Path.cwd()
|
|
245
|
+
workflows_dir = cwd / "workflows"
|
|
246
|
+
config_path = cwd / "pyworkflow.config.yaml"
|
|
247
|
+
|
|
248
|
+
if workflows_dir.exists():
|
|
249
|
+
print_warning(f"Directory already exists: {workflows_dir}")
|
|
250
|
+
if not non_interactive:
|
|
251
|
+
if not confirm("Overwrite existing workflows directory?", default=False):
|
|
252
|
+
raise click.Abort()
|
|
253
|
+
print_info("")
|
|
254
|
+
|
|
255
|
+
if config_path.exists():
|
|
256
|
+
print_warning(f"Config already exists: {config_path}")
|
|
257
|
+
if not non_interactive:
|
|
258
|
+
if not confirm("Overwrite existing config?", default=False):
|
|
259
|
+
raise click.Abort()
|
|
260
|
+
print_info("")
|
|
261
|
+
|
|
262
|
+
# 3. Template selection (currently only basic)
|
|
263
|
+
if not non_interactive:
|
|
264
|
+
print_info("Select a project template:\n")
|
|
265
|
+
template = select(
|
|
266
|
+
"Template:",
|
|
267
|
+
choices=[
|
|
268
|
+
{
|
|
269
|
+
"name": "Basic - Order processing and notifications (2 workflows)",
|
|
270
|
+
"value": "basic",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# 4. Storage backend selection
|
|
276
|
+
sqlite_available = _check_sqlite_available()
|
|
277
|
+
if storage_override:
|
|
278
|
+
storage_type = storage_override.lower()
|
|
279
|
+
if storage_type == "sqlite" and not sqlite_available:
|
|
280
|
+
print_error("SQLite is not available in your Python build")
|
|
281
|
+
print_info("Use --storage file or install libsqlite3-dev and rebuild Python")
|
|
282
|
+
raise click.Abort()
|
|
283
|
+
elif non_interactive:
|
|
284
|
+
storage_type = "sqlite" if sqlite_available else "file"
|
|
285
|
+
else:
|
|
286
|
+
print_info("")
|
|
287
|
+
choices = []
|
|
288
|
+
if sqlite_available:
|
|
289
|
+
choices.append(
|
|
290
|
+
{"name": "SQLite - Single file database (recommended)", "value": "sqlite"}
|
|
291
|
+
)
|
|
292
|
+
choices.append(
|
|
293
|
+
{
|
|
294
|
+
"name": "File - JSON files on disk"
|
|
295
|
+
+ (" (recommended)" if not sqlite_available else ""),
|
|
296
|
+
"value": "file",
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
storage_type = select("Storage backend:", choices=choices)
|
|
301
|
+
|
|
302
|
+
# 5. Docker prompt
|
|
303
|
+
docker_available, docker_error = check_docker_available()
|
|
304
|
+
start_docker = False
|
|
305
|
+
|
|
306
|
+
if not skip_docker:
|
|
307
|
+
if not docker_available:
|
|
308
|
+
print_warning(f"\nDocker: {docker_error}")
|
|
309
|
+
print_info("Skipping Docker setup. You can run 'pyworkflow setup' later.\n")
|
|
310
|
+
elif non_interactive:
|
|
311
|
+
start_docker = True
|
|
312
|
+
else:
|
|
313
|
+
print_info("")
|
|
314
|
+
start_docker = confirm("Start Docker services (Redis + Dashboard)?", default=True)
|
|
315
|
+
|
|
316
|
+
# 6. Create project structure
|
|
317
|
+
print_info("\nCreating project structure...")
|
|
318
|
+
_create_project_files(cwd, template)
|
|
319
|
+
|
|
320
|
+
# 7. Generate config
|
|
321
|
+
storage_path = (
|
|
322
|
+
"pyworkflow_data/pyworkflow.db" if storage_type == "sqlite" else "pyworkflow_data"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
yaml_content = generate_yaml_config(
|
|
326
|
+
module="workflows",
|
|
327
|
+
runtime="celery",
|
|
328
|
+
storage_type=storage_type,
|
|
329
|
+
storage_path=storage_path,
|
|
330
|
+
broker_url="redis://localhost:6379/0",
|
|
331
|
+
result_backend="redis://localhost:6379/1",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
write_yaml_config(yaml_content, config_path, backup=True)
|
|
335
|
+
print_success(f" Created: {config_path.name}")
|
|
336
|
+
|
|
337
|
+
# 8. Docker setup
|
|
338
|
+
dashboard_available = False
|
|
339
|
+
if start_docker:
|
|
340
|
+
dashboard_available = _setup_docker(cwd, storage_type, storage_path)
|
|
341
|
+
|
|
342
|
+
# 9. Show next steps
|
|
343
|
+
_show_next_steps(start_docker, dashboard_available)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _print_welcome() -> None:
|
|
347
|
+
"""Print welcome banner."""
|
|
348
|
+
print_info("")
|
|
349
|
+
print_info("=" * 60)
|
|
350
|
+
print_info(" PyWorkflow Quickstart")
|
|
351
|
+
print_info("=" * 60)
|
|
352
|
+
print_info("")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _create_project_files(cwd: Path, template: str) -> None:
|
|
356
|
+
"""Create project structure based on template."""
|
|
357
|
+
workflows_dir = cwd / "workflows"
|
|
358
|
+
workflows_dir.mkdir(exist_ok=True)
|
|
359
|
+
|
|
360
|
+
# Write template files
|
|
361
|
+
files = {
|
|
362
|
+
workflows_dir / "__init__.py": WORKFLOWS_INIT_TEMPLATE,
|
|
363
|
+
workflows_dir / "orders.py": ORDERS_WORKFLOW_TEMPLATE,
|
|
364
|
+
workflows_dir / "notifications.py": NOTIFICATIONS_WORKFLOW_TEMPLATE,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for file_path, content in files.items():
|
|
368
|
+
file_path.write_text(content)
|
|
369
|
+
print_success(f" Created: {file_path.relative_to(cwd)}")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _setup_docker(
|
|
373
|
+
cwd: Path,
|
|
374
|
+
storage_type: str,
|
|
375
|
+
storage_path: str,
|
|
376
|
+
) -> bool:
|
|
377
|
+
"""Set up Docker infrastructure.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
True if dashboard is available, False otherwise
|
|
381
|
+
"""
|
|
382
|
+
print_info("\nSetting up Docker services...")
|
|
383
|
+
|
|
384
|
+
# Generate docker-compose.yml
|
|
385
|
+
compose_content = generate_docker_compose_content(
|
|
386
|
+
storage_type=storage_type,
|
|
387
|
+
storage_path=storage_path,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
compose_path = cwd / "docker-compose.yml"
|
|
391
|
+
write_docker_compose(compose_content, compose_path)
|
|
392
|
+
print_success(f" Created: {compose_path.name}")
|
|
393
|
+
|
|
394
|
+
# Pull images
|
|
395
|
+
print_info("\n Pulling Docker images...")
|
|
396
|
+
print_info("")
|
|
397
|
+
pull_success, _ = run_docker_command(
|
|
398
|
+
["pull"],
|
|
399
|
+
compose_file=compose_path,
|
|
400
|
+
stream_output=True,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
dashboard_available = pull_success
|
|
404
|
+
if not pull_success:
|
|
405
|
+
print_warning("\n Failed to pull dashboard images")
|
|
406
|
+
print_info(" Continuing with Redis setup only...")
|
|
407
|
+
|
|
408
|
+
# Start services
|
|
409
|
+
print_info("\n Starting services...")
|
|
410
|
+
print_info("")
|
|
411
|
+
|
|
412
|
+
services_to_start = ["redis"]
|
|
413
|
+
if dashboard_available:
|
|
414
|
+
services_to_start.extend(["dashboard-backend", "dashboard-frontend"])
|
|
415
|
+
|
|
416
|
+
success, _ = run_docker_command(
|
|
417
|
+
["up", "-d"] + services_to_start,
|
|
418
|
+
compose_file=compose_path,
|
|
419
|
+
stream_output=True,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if not success:
|
|
423
|
+
print_error("\n Failed to start services")
|
|
424
|
+
print_info(" Try: docker compose down && docker compose up -d")
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
print_success("\n Services started")
|
|
428
|
+
|
|
429
|
+
# Health checks
|
|
430
|
+
print_info("\n Checking service health...")
|
|
431
|
+
health_checks = {
|
|
432
|
+
"Redis": {"type": "tcp", "host": "localhost", "port": 6379},
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if dashboard_available:
|
|
436
|
+
health_checks["Dashboard Backend"] = {
|
|
437
|
+
"type": "http",
|
|
438
|
+
"url": "http://localhost:8585/api/v1/health",
|
|
439
|
+
}
|
|
440
|
+
health_checks["Dashboard Frontend"] = {
|
|
441
|
+
"type": "http",
|
|
442
|
+
"url": "http://localhost:5173",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
health_results = check_service_health(health_checks)
|
|
446
|
+
|
|
447
|
+
for service_name, healthy in health_results.items():
|
|
448
|
+
if healthy:
|
|
449
|
+
print_success(f" {service_name}: Ready")
|
|
450
|
+
else:
|
|
451
|
+
print_warning(f" {service_name}: Not responding (may still be starting)")
|
|
452
|
+
|
|
453
|
+
return dashboard_available
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _show_next_steps(docker_started: bool, dashboard_available: bool) -> None:
|
|
457
|
+
"""Display next steps."""
|
|
458
|
+
print_info("\n" + "=" * 60)
|
|
459
|
+
print_success("Project Created!")
|
|
460
|
+
print_info("=" * 60)
|
|
461
|
+
|
|
462
|
+
print_info("\nProject structure:")
|
|
463
|
+
print_info(" workflows/")
|
|
464
|
+
print_info(" __init__.py")
|
|
465
|
+
print_info(" orders.py (process_order workflow)")
|
|
466
|
+
print_info(" notifications.py (send_notification workflow)")
|
|
467
|
+
print_info(" pyworkflow.config.yaml")
|
|
468
|
+
|
|
469
|
+
if docker_started:
|
|
470
|
+
print_info("\nServices running:")
|
|
471
|
+
print_info(" Redis: redis://localhost:6379")
|
|
472
|
+
if dashboard_available:
|
|
473
|
+
print_info(" Dashboard: http://localhost:5173")
|
|
474
|
+
print_info(" Dashboard API: http://localhost:8585/docs")
|
|
475
|
+
|
|
476
|
+
print_info("\nNext steps:")
|
|
477
|
+
print_info("")
|
|
478
|
+
print_info(" 1. Start a worker:")
|
|
479
|
+
print_info(" $ pyworkflow worker start")
|
|
480
|
+
print_info("")
|
|
481
|
+
print_info(" 2. Run a workflow:")
|
|
482
|
+
print_info(" $ pyworkflow workflows run process_order \\")
|
|
483
|
+
print_info(' --input \'{"order_id": "123", "amount": 49.99}\'')
|
|
484
|
+
|
|
485
|
+
if docker_started and dashboard_available:
|
|
486
|
+
print_info("")
|
|
487
|
+
print_info(" 3. View the dashboard:")
|
|
488
|
+
print_info(" Open http://localhost:5173 in your browser")
|
|
489
|
+
|
|
490
|
+
if docker_started:
|
|
491
|
+
print_info("")
|
|
492
|
+
print_info("To stop services:")
|
|
493
|
+
print_info(" $ docker compose down")
|
|
494
|
+
|
|
495
|
+
print_info("")
|