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 @@
|
|
|
1
|
+
"""PyWorkflow Dashboard Backend."""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Dashboard configuration using pydantic-settings."""
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Settings(BaseSettings):
|
|
7
|
+
"""Dashboard settings loaded from environment variables."""
|
|
8
|
+
|
|
9
|
+
# PyWorkflow configuration
|
|
10
|
+
pyworkflow_config_path: str | None = None # Path to pyworkflow.config.yaml
|
|
11
|
+
|
|
12
|
+
# Storage configuration (fallback if pyworkflow config not set)
|
|
13
|
+
storage_type: str = "file"
|
|
14
|
+
storage_path: str = "./pyworkflow_data"
|
|
15
|
+
|
|
16
|
+
# Server configuration
|
|
17
|
+
host: str = "0.0.0.0"
|
|
18
|
+
port: int = 8585
|
|
19
|
+
|
|
20
|
+
# CORS configuration
|
|
21
|
+
cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"]
|
|
22
|
+
|
|
23
|
+
# Debug mode
|
|
24
|
+
debug: bool = False
|
|
25
|
+
|
|
26
|
+
class Config:
|
|
27
|
+
env_prefix = "DASHBOARD_"
|
|
28
|
+
env_file = ".env"
|
|
29
|
+
env_file_encoding = "utf-8"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
settings = Settings()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Controller for workflow run endpoints."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from app.repositories.run_repository import RunRepository
|
|
6
|
+
from app.schemas.event import EventListResponse
|
|
7
|
+
from app.schemas.run import RunDetailResponse, RunListResponse, StartRunRequest, StartRunResponse
|
|
8
|
+
from app.services.run_service import RunService
|
|
9
|
+
from pyworkflow.storage.base import StorageBackend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RunController:
|
|
13
|
+
"""Controller handling workflow run-related requests."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, storage: StorageBackend):
|
|
16
|
+
"""Initialize controller with storage backend.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
storage: PyWorkflow storage backend.
|
|
20
|
+
"""
|
|
21
|
+
self.repository = RunRepository(storage)
|
|
22
|
+
self.service = RunService(self.repository)
|
|
23
|
+
|
|
24
|
+
async def list_runs(
|
|
25
|
+
self,
|
|
26
|
+
query: str | None = None,
|
|
27
|
+
status: str | None = None,
|
|
28
|
+
start_time: datetime | None = None,
|
|
29
|
+
end_time: datetime | None = None,
|
|
30
|
+
limit: int = 100,
|
|
31
|
+
cursor: str | None = None,
|
|
32
|
+
) -> RunListResponse:
|
|
33
|
+
"""List workflow runs with optional filtering and cursor-based pagination.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
query: Case-insensitive search in workflow name and input kwargs.
|
|
37
|
+
status: Filter by status.
|
|
38
|
+
start_time: Filter runs started at or after this time.
|
|
39
|
+
end_time: Filter runs started before this time.
|
|
40
|
+
limit: Maximum results.
|
|
41
|
+
cursor: Run ID to start after (for pagination).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
RunListResponse with matching runs and next_cursor.
|
|
45
|
+
"""
|
|
46
|
+
return await self.service.list_runs(
|
|
47
|
+
query=query,
|
|
48
|
+
status=status,
|
|
49
|
+
start_time=start_time,
|
|
50
|
+
end_time=end_time,
|
|
51
|
+
limit=limit,
|
|
52
|
+
cursor=cursor,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def get_run(self, run_id: str) -> RunDetailResponse | None:
|
|
56
|
+
"""Get detailed information about a run.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
run_id: The run ID.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
RunDetailResponse if found, None otherwise.
|
|
63
|
+
"""
|
|
64
|
+
return await self.service.get_run(run_id)
|
|
65
|
+
|
|
66
|
+
async def get_events(self, run_id: str) -> EventListResponse:
|
|
67
|
+
"""Get events for a run.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
run_id: The run ID.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
EventListResponse with run events.
|
|
74
|
+
"""
|
|
75
|
+
return await self.service.get_events(run_id)
|
|
76
|
+
|
|
77
|
+
async def start_run(self, request: StartRunRequest) -> StartRunResponse:
|
|
78
|
+
"""Start a new workflow run.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
request: The start run request containing workflow name and kwargs.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
StartRunResponse with run_id and workflow_name.
|
|
85
|
+
"""
|
|
86
|
+
return await self.service.start_run(request)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Controller for workflow endpoints."""
|
|
2
|
+
|
|
3
|
+
from app.repositories.workflow_repository import WorkflowRepository
|
|
4
|
+
from app.schemas.workflow import WorkflowListResponse, WorkflowResponse
|
|
5
|
+
from app.services.workflow_service import WorkflowService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkflowController:
|
|
9
|
+
"""Controller handling workflow-related requests."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""Initialize controller with service layer."""
|
|
13
|
+
self.repository = WorkflowRepository()
|
|
14
|
+
self.service = WorkflowService(self.repository)
|
|
15
|
+
|
|
16
|
+
def list_workflows(self) -> WorkflowListResponse:
|
|
17
|
+
"""Get all registered workflows.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
WorkflowListResponse with all workflows.
|
|
21
|
+
"""
|
|
22
|
+
return self.service.list_workflows()
|
|
23
|
+
|
|
24
|
+
def get_workflow(self, name: str) -> WorkflowResponse | None:
|
|
25
|
+
"""Get a specific workflow by name.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name: Workflow name.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
WorkflowResponse if found, None otherwise.
|
|
32
|
+
"""
|
|
33
|
+
return self.service.get_workflow(name)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Storage dependency for FastAPI."""
|
|
2
|
+
|
|
3
|
+
from app.config import settings
|
|
4
|
+
from pyworkflow import get_storage as pyworkflow_get_storage
|
|
5
|
+
from pyworkflow.storage.base import StorageBackend
|
|
6
|
+
from pyworkflow.storage.file import FileStorageBackend
|
|
7
|
+
from pyworkflow.storage.memory import InMemoryStorageBackend
|
|
8
|
+
|
|
9
|
+
_storage_instance: StorageBackend | None = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def reset_storage_cache() -> None:
|
|
13
|
+
"""Reset the cached storage instance.
|
|
14
|
+
|
|
15
|
+
Called during application startup to ensure fresh initialization
|
|
16
|
+
after pyworkflow configuration is loaded.
|
|
17
|
+
"""
|
|
18
|
+
global _storage_instance
|
|
19
|
+
_storage_instance = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def get_storage() -> StorageBackend:
|
|
23
|
+
"""Get or create the storage backend instance.
|
|
24
|
+
|
|
25
|
+
First tries to get storage from pyworkflow configuration.
|
|
26
|
+
Falls back to creating based on dashboard settings.
|
|
27
|
+
"""
|
|
28
|
+
global _storage_instance
|
|
29
|
+
|
|
30
|
+
if _storage_instance is None:
|
|
31
|
+
# Try to get from pyworkflow config first
|
|
32
|
+
storage = pyworkflow_get_storage()
|
|
33
|
+
|
|
34
|
+
if storage is None:
|
|
35
|
+
# Create based on dashboard config
|
|
36
|
+
if settings.storage_type == "file":
|
|
37
|
+
storage = FileStorageBackend(settings.storage_path)
|
|
38
|
+
elif settings.storage_type == "sqlite":
|
|
39
|
+
from pyworkflow.storage.sqlite import SQLiteStorageBackend
|
|
40
|
+
|
|
41
|
+
db_path = f"{settings.storage_path}/pyworkflow.db"
|
|
42
|
+
storage = SQLiteStorageBackend(db_path)
|
|
43
|
+
elif settings.storage_type == "memory":
|
|
44
|
+
storage = InMemoryStorageBackend()
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(f"Unknown storage type: {settings.storage_type}")
|
|
47
|
+
|
|
48
|
+
_storage_instance = storage
|
|
49
|
+
|
|
50
|
+
return _storage_instance
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Repository for workflow run data access."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pyworkflow.engine.events import Event
|
|
6
|
+
from pyworkflow.storage.base import StorageBackend
|
|
7
|
+
from pyworkflow.storage.schemas import (
|
|
8
|
+
RunStatus,
|
|
9
|
+
WorkflowRun,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RunRepository:
|
|
14
|
+
"""Repository for accessing workflow run data via pyworkflow storage."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, storage: StorageBackend):
|
|
17
|
+
"""Initialize with a storage backend.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
storage: PyWorkflow storage backend instance.
|
|
21
|
+
"""
|
|
22
|
+
self.storage = storage
|
|
23
|
+
|
|
24
|
+
async def list_runs(
|
|
25
|
+
self,
|
|
26
|
+
query: str | None = None,
|
|
27
|
+
status: RunStatus | None = None,
|
|
28
|
+
start_time: datetime | None = None,
|
|
29
|
+
end_time: datetime | None = None,
|
|
30
|
+
limit: int = 100,
|
|
31
|
+
cursor: str | None = None,
|
|
32
|
+
) -> tuple[list[WorkflowRun], str | None]:
|
|
33
|
+
"""List workflow runs with optional filtering and cursor-based pagination.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
query: Case-insensitive search in workflow name and input kwargs.
|
|
37
|
+
status: Filter by run status.
|
|
38
|
+
start_time: Filter runs started at or after this time.
|
|
39
|
+
end_time: Filter runs started before this time.
|
|
40
|
+
limit: Maximum number of results.
|
|
41
|
+
cursor: Run ID to start after (for pagination).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple of (list of workflow runs, next_cursor or None).
|
|
45
|
+
"""
|
|
46
|
+
return await self.storage.list_runs(
|
|
47
|
+
query=query,
|
|
48
|
+
status=status,
|
|
49
|
+
start_time=start_time,
|
|
50
|
+
end_time=end_time,
|
|
51
|
+
limit=limit,
|
|
52
|
+
cursor=cursor,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def get_run(self, run_id: str) -> WorkflowRun | None:
|
|
56
|
+
"""Get a workflow run by ID.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
run_id: The run ID.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
WorkflowRun if found, None otherwise.
|
|
63
|
+
"""
|
|
64
|
+
return await self.storage.get_run(run_id)
|
|
65
|
+
|
|
66
|
+
async def get_events(
|
|
67
|
+
self,
|
|
68
|
+
run_id: str,
|
|
69
|
+
event_types: list[str] | None = None,
|
|
70
|
+
) -> list[Event]:
|
|
71
|
+
"""Get all events for a workflow run.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
run_id: The run ID.
|
|
75
|
+
event_types: Optional filter by event types.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of events ordered by sequence.
|
|
79
|
+
"""
|
|
80
|
+
return await self.storage.get_events(run_id, event_types=event_types)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Repository for workflow metadata access."""
|
|
2
|
+
|
|
3
|
+
from pyworkflow import get_workflow, list_workflows
|
|
4
|
+
from pyworkflow.core.registry import WorkflowMetadata
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkflowRepository:
|
|
8
|
+
"""Repository for accessing registered workflow metadata."""
|
|
9
|
+
|
|
10
|
+
def list_all(self) -> dict[str, WorkflowMetadata]:
|
|
11
|
+
"""Get all registered workflows.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Dictionary mapping workflow names to their metadata.
|
|
15
|
+
"""
|
|
16
|
+
return list_workflows()
|
|
17
|
+
|
|
18
|
+
def get_by_name(self, name: str) -> WorkflowMetadata | None:
|
|
19
|
+
"""Get a specific workflow by name.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
name: The workflow name.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
WorkflowMetadata if found, None otherwise.
|
|
26
|
+
"""
|
|
27
|
+
return get_workflow(name)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""V1 API routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from app.rest.v1.health import router as health_router
|
|
6
|
+
from app.rest.v1.runs import router as runs_router
|
|
7
|
+
from app.rest.v1.workflows import router as workflows_router
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
router.include_router(health_router, tags=["health"])
|
|
11
|
+
router.include_router(workflows_router, prefix="/workflows", tags=["workflows"])
|
|
12
|
+
router.include_router(runs_router, prefix="/runs", tags=["runs"])
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Health check endpoint."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from app.dependencies import get_storage
|
|
7
|
+
from pyworkflow.storage.base import StorageBackend
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HealthResponse(BaseModel):
|
|
13
|
+
"""Health check response."""
|
|
14
|
+
|
|
15
|
+
status: str
|
|
16
|
+
storage_healthy: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/health", response_model=HealthResponse)
|
|
20
|
+
async def health_check(
|
|
21
|
+
storage: StorageBackend = Depends(get_storage),
|
|
22
|
+
) -> HealthResponse:
|
|
23
|
+
"""Check API and storage health.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
HealthResponse with status information.
|
|
27
|
+
"""
|
|
28
|
+
storage_healthy = await storage.health_check()
|
|
29
|
+
|
|
30
|
+
return HealthResponse(
|
|
31
|
+
status="healthy" if storage_healthy else "degraded",
|
|
32
|
+
storage_healthy=storage_healthy,
|
|
33
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Workflow run endpoints."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
6
|
+
|
|
7
|
+
from app.controllers.run_controller import RunController
|
|
8
|
+
from app.dependencies import get_storage
|
|
9
|
+
from app.schemas.event import EventListResponse
|
|
10
|
+
from app.schemas.run import RunDetailResponse, RunListResponse, StartRunRequest, StartRunResponse
|
|
11
|
+
from pyworkflow.storage.base import StorageBackend
|
|
12
|
+
|
|
13
|
+
router = APIRouter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("", response_model=RunListResponse)
|
|
17
|
+
async def list_runs(
|
|
18
|
+
query: str | None = Query(
|
|
19
|
+
None, description="Search in workflow name and input kwargs (case-insensitive)"
|
|
20
|
+
),
|
|
21
|
+
status: str | None = Query(
|
|
22
|
+
None,
|
|
23
|
+
description="Filter by status (pending, running, suspended, completed, failed, interrupted, cancelled)",
|
|
24
|
+
),
|
|
25
|
+
start_time: datetime | None = Query(
|
|
26
|
+
None, description="Filter runs started at or after this time (ISO 8601)"
|
|
27
|
+
),
|
|
28
|
+
end_time: datetime | None = Query(
|
|
29
|
+
None, description="Filter runs started before this time (ISO 8601)"
|
|
30
|
+
),
|
|
31
|
+
limit: int = Query(100, ge=1, le=1000, description="Maximum results"),
|
|
32
|
+
cursor: str | None = Query(None, description="Run ID to start after (for pagination)"),
|
|
33
|
+
storage: StorageBackend = Depends(get_storage),
|
|
34
|
+
) -> RunListResponse:
|
|
35
|
+
"""List workflow runs with optional filtering and cursor-based pagination.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
query: Case-insensitive search in workflow name and input kwargs.
|
|
39
|
+
status: Filter by run status.
|
|
40
|
+
start_time: Filter runs started at or after this time.
|
|
41
|
+
end_time: Filter runs started before this time.
|
|
42
|
+
limit: Maximum number of results (1-1000).
|
|
43
|
+
cursor: Run ID to start after (for pagination).
|
|
44
|
+
storage: Storage backend (injected).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
RunListResponse with matching runs and next_cursor.
|
|
48
|
+
"""
|
|
49
|
+
controller = RunController(storage)
|
|
50
|
+
return await controller.list_runs(
|
|
51
|
+
query=query,
|
|
52
|
+
status=status,
|
|
53
|
+
start_time=start_time,
|
|
54
|
+
end_time=end_time,
|
|
55
|
+
limit=limit,
|
|
56
|
+
cursor=cursor,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.post("", response_model=StartRunResponse, status_code=201)
|
|
61
|
+
async def start_run(
|
|
62
|
+
request: StartRunRequest,
|
|
63
|
+
storage: StorageBackend = Depends(get_storage),
|
|
64
|
+
) -> StartRunResponse:
|
|
65
|
+
"""Start a new workflow run.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
request: Start run request with workflow name and kwargs.
|
|
69
|
+
storage: Storage backend (injected).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
StartRunResponse with run_id and workflow_name.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
HTTPException: 404 if workflow not found, 400 for validation errors.
|
|
76
|
+
"""
|
|
77
|
+
controller = RunController(storage)
|
|
78
|
+
try:
|
|
79
|
+
return await controller.start_run(request)
|
|
80
|
+
except ValueError as e:
|
|
81
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.get("/{run_id}", response_model=RunDetailResponse)
|
|
87
|
+
async def get_run(
|
|
88
|
+
run_id: str,
|
|
89
|
+
storage: StorageBackend = Depends(get_storage),
|
|
90
|
+
) -> RunDetailResponse:
|
|
91
|
+
"""Get detailed information about a workflow run.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
run_id: The run ID.
|
|
95
|
+
storage: Storage backend (injected).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
RunDetailResponse with run details.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
HTTPException: 404 if run not found.
|
|
102
|
+
"""
|
|
103
|
+
controller = RunController(storage)
|
|
104
|
+
run = await controller.get_run(run_id)
|
|
105
|
+
|
|
106
|
+
if run is None:
|
|
107
|
+
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
108
|
+
|
|
109
|
+
return run
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.get("/{run_id}/events", response_model=EventListResponse)
|
|
113
|
+
async def get_run_events(
|
|
114
|
+
run_id: str,
|
|
115
|
+
storage: StorageBackend = Depends(get_storage),
|
|
116
|
+
) -> EventListResponse:
|
|
117
|
+
"""Get all events for a workflow run.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
run_id: The run ID.
|
|
121
|
+
storage: Storage backend (injected).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
EventListResponse with run events.
|
|
125
|
+
"""
|
|
126
|
+
controller = RunController(storage)
|
|
127
|
+
|
|
128
|
+
# Verify run exists
|
|
129
|
+
run = await controller.get_run(run_id)
|
|
130
|
+
if run is None:
|
|
131
|
+
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
132
|
+
|
|
133
|
+
return await controller.get_events(run_id)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Workflow endpoints."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
|
|
5
|
+
from app.controllers.workflow_controller import WorkflowController
|
|
6
|
+
from app.schemas.workflow import WorkflowListResponse, WorkflowResponse
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.get("", response_model=WorkflowListResponse)
|
|
12
|
+
async def list_workflows() -> WorkflowListResponse:
|
|
13
|
+
"""List all registered workflows.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
WorkflowListResponse with all registered workflows.
|
|
17
|
+
"""
|
|
18
|
+
controller = WorkflowController()
|
|
19
|
+
return controller.list_workflows()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/{name}", response_model=WorkflowResponse)
|
|
23
|
+
async def get_workflow(name: str) -> WorkflowResponse:
|
|
24
|
+
"""Get a specific workflow by name.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Workflow name.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
WorkflowResponse with workflow details.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
HTTPException: 404 if workflow not found.
|
|
34
|
+
"""
|
|
35
|
+
controller = WorkflowController()
|
|
36
|
+
workflow = controller.get_workflow(name)
|
|
37
|
+
|
|
38
|
+
if workflow is None:
|
|
39
|
+
raise HTTPException(status_code=404, detail=f"Workflow '{name}' not found")
|
|
40
|
+
|
|
41
|
+
return workflow
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Pydantic schemas for API request/response models."""
|
|
2
|
+
|
|
3
|
+
from app.schemas.common import PaginatedResponse
|
|
4
|
+
from app.schemas.event import EventListResponse, EventResponse
|
|
5
|
+
from app.schemas.hook import HookListResponse, HookResponse
|
|
6
|
+
from app.schemas.run import RunDetailResponse, RunListResponse, RunResponse
|
|
7
|
+
from app.schemas.step import StepListResponse, StepResponse
|
|
8
|
+
from app.schemas.workflow import WorkflowListResponse, WorkflowResponse
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"PaginatedResponse",
|
|
12
|
+
"WorkflowResponse",
|
|
13
|
+
"WorkflowListResponse",
|
|
14
|
+
"RunResponse",
|
|
15
|
+
"RunDetailResponse",
|
|
16
|
+
"RunListResponse",
|
|
17
|
+
"EventResponse",
|
|
18
|
+
"EventListResponse",
|
|
19
|
+
"StepResponse",
|
|
20
|
+
"StepListResponse",
|
|
21
|
+
"HookResponse",
|
|
22
|
+
"HookListResponse",
|
|
23
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Common schema types."""
|
|
2
|
+
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
11
|
+
"""Base paginated response model."""
|
|
12
|
+
|
|
13
|
+
items: list[T]
|
|
14
|
+
count: int
|
|
15
|
+
limit: int = 100
|
|
16
|
+
offset: int = 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Event response schemas."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventResponse(BaseModel):
|
|
10
|
+
"""Response model for a workflow event."""
|
|
11
|
+
|
|
12
|
+
event_id: str
|
|
13
|
+
run_id: str
|
|
14
|
+
type: str
|
|
15
|
+
timestamp: datetime
|
|
16
|
+
sequence: int | None = None
|
|
17
|
+
data: dict[str, Any] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventListResponse(BaseModel):
|
|
21
|
+
"""Response model for listing events."""
|
|
22
|
+
|
|
23
|
+
items: list[EventResponse]
|
|
24
|
+
count: int
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Hook response schemas."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HookResponse(BaseModel):
|
|
9
|
+
"""Response model for a hook."""
|
|
10
|
+
|
|
11
|
+
hook_id: str
|
|
12
|
+
run_id: str
|
|
13
|
+
name: str | None = None
|
|
14
|
+
status: str
|
|
15
|
+
created_at: datetime
|
|
16
|
+
received_at: datetime | None = None
|
|
17
|
+
expires_at: datetime | None = None
|
|
18
|
+
has_payload: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HookListResponse(BaseModel):
|
|
22
|
+
"""Response model for listing hooks."""
|
|
23
|
+
|
|
24
|
+
items: list[HookResponse]
|
|
25
|
+
count: int
|