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 @@
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,6 @@
1
+ """Controllers layer."""
2
+
3
+ from app.controllers.run_controller import RunController
4
+ from app.controllers.workflow_controller import WorkflowController
5
+
6
+ __all__ = ["WorkflowController", "RunController"]
@@ -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,5 @@
1
+ """FastAPI dependencies."""
2
+
3
+ from app.dependencies.storage import get_storage
4
+
5
+ __all__ = ["get_storage"]
@@ -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,6 @@
1
+ """Repository layer."""
2
+
3
+ from app.repositories.run_repository import RunRepository
4
+ from app.repositories.workflow_repository import WorkflowRepository
5
+
6
+ __all__ = ["WorkflowRepository", "RunRepository"]
@@ -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,8 @@
1
+ """REST API routes."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from app.rest.v1 import router as v1_router
6
+
7
+ router = APIRouter()
8
+ router.include_router(v1_router, prefix="/api/v1")
@@ -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