loom-core 1.0.0__py3-none-any.whl → 1.0.2__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.
- loom/__init__.py +8 -7
- loom/web/__init__.py +5 -0
- loom/web/api/__init__.py +4 -0
- loom/web/api/events.py +315 -0
- loom/web/api/graphs.py +236 -0
- loom/web/api/logs.py +342 -0
- loom/web/api/stats.py +283 -0
- loom/web/api/tasks.py +333 -0
- loom/web/api/workflows.py +524 -0
- loom/web/main.py +306 -0
- loom/web/schemas.py +656 -0
- {loom_core-1.0.0.dist-info → loom_core-1.0.2.dist-info}/METADATA +1 -1
- {loom_core-1.0.0.dist-info → loom_core-1.0.2.dist-info}/RECORD +17 -7
- {loom_core-1.0.0.dist-info → loom_core-1.0.2.dist-info}/WHEEL +0 -0
- {loom_core-1.0.0.dist-info → loom_core-1.0.2.dist-info}/entry_points.txt +0 -0
- {loom_core-1.0.0.dist-info → loom_core-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {loom_core-1.0.0.dist-info → loom_core-1.0.2.dist-info}/top_level.txt +0 -0
loom/web/api/tasks.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Task API Endpoints
|
|
2
|
+
|
|
3
|
+
Provides REST endpoints for managing and querying workflow tasks.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
10
|
+
|
|
11
|
+
from ...database.db import Database
|
|
12
|
+
from ..schemas import (
|
|
13
|
+
ErrorResponse,
|
|
14
|
+
PaginatedResponse,
|
|
15
|
+
PaginationMeta,
|
|
16
|
+
TaskDetail,
|
|
17
|
+
TaskKind,
|
|
18
|
+
TaskListParams,
|
|
19
|
+
TaskStatus,
|
|
20
|
+
TaskSummary,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
router = APIRouter()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_db():
|
|
27
|
+
"""Database dependency"""
|
|
28
|
+
async with Database[Any, Any]() as db:
|
|
29
|
+
yield db
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get(
|
|
33
|
+
"/",
|
|
34
|
+
response_model=PaginatedResponse[TaskSummary],
|
|
35
|
+
summary="List tasks",
|
|
36
|
+
description="""
|
|
37
|
+
Retrieve a paginated list of tasks with optional filtering and sorting.
|
|
38
|
+
|
|
39
|
+
**Filtering Options:**
|
|
40
|
+
- `workflow_id`: Filter by parent workflow ID
|
|
41
|
+
- `status`: Filter by task execution status (PENDING, RUNNING, COMPLETED, FAILED)
|
|
42
|
+
- `kind`: Filter by task type (STEP, ACTIVITY, TIMER)
|
|
43
|
+
|
|
44
|
+
**Sorting Options:**
|
|
45
|
+
- `sort_by`: Field to sort by (created_at, updated_at, run_at, attempts)
|
|
46
|
+
- `sort_order`: Sort direction (asc/desc)
|
|
47
|
+
|
|
48
|
+
**Pagination:**
|
|
49
|
+
- `page`: Page number (1-based)
|
|
50
|
+
- `per_page`: Items per page (1-1000, default 50)
|
|
51
|
+
|
|
52
|
+
**Response includes:**
|
|
53
|
+
- List of task summaries with execution metadata
|
|
54
|
+
- Retry attempt counts and error information
|
|
55
|
+
- Pagination metadata
|
|
56
|
+
""",
|
|
57
|
+
responses={
|
|
58
|
+
400: {"model": ErrorResponse, "description": "Invalid request parameters"},
|
|
59
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
async def list_tasks(
|
|
63
|
+
params: TaskListParams = Depends(), db: Database = Depends(get_db)
|
|
64
|
+
):
|
|
65
|
+
"""List tasks with pagination and filtering"""
|
|
66
|
+
try:
|
|
67
|
+
# Build WHERE clause
|
|
68
|
+
where_conditions = []
|
|
69
|
+
query_params = []
|
|
70
|
+
|
|
71
|
+
if params.workflow_id:
|
|
72
|
+
where_conditions.append("t.workflow_id = ?")
|
|
73
|
+
query_params.append(params.workflow_id)
|
|
74
|
+
|
|
75
|
+
if params.status:
|
|
76
|
+
where_conditions.append("t.status = ?")
|
|
77
|
+
query_params.append(params.status.value)
|
|
78
|
+
|
|
79
|
+
if params.kind:
|
|
80
|
+
where_conditions.append("t.kind = ?")
|
|
81
|
+
query_params.append(params.kind.value)
|
|
82
|
+
|
|
83
|
+
where_clause = (
|
|
84
|
+
f"WHERE {' AND '.join(where_conditions)}" if where_conditions else ""
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Get total count
|
|
88
|
+
count_sql = f"""
|
|
89
|
+
SELECT COUNT(*) as total
|
|
90
|
+
FROM tasks t
|
|
91
|
+
JOIN workflows w ON t.workflow_id = w.id
|
|
92
|
+
{where_clause}
|
|
93
|
+
"""
|
|
94
|
+
count_result = await db.fetchone(count_sql, tuple(query_params))
|
|
95
|
+
total = count_result["total"] if count_result else 0
|
|
96
|
+
|
|
97
|
+
# Calculate pagination
|
|
98
|
+
pages = math.ceil(total / params.per_page) if total > 0 else 1
|
|
99
|
+
offset = (params.page - 1) * params.per_page
|
|
100
|
+
|
|
101
|
+
# Build ORDER BY clause
|
|
102
|
+
order_clause = f"ORDER BY t.{params.sort_by} {params.sort_order.upper()}"
|
|
103
|
+
|
|
104
|
+
# Get tasks for current page
|
|
105
|
+
tasks_sql = f"""
|
|
106
|
+
SELECT
|
|
107
|
+
t.id,
|
|
108
|
+
t.workflow_id,
|
|
109
|
+
w.name as workflow_name,
|
|
110
|
+
t.kind,
|
|
111
|
+
t.target,
|
|
112
|
+
t.status,
|
|
113
|
+
t.attempts,
|
|
114
|
+
t.max_attempts,
|
|
115
|
+
t.run_at,
|
|
116
|
+
t.created_at,
|
|
117
|
+
t.updated_at,
|
|
118
|
+
t.last_error
|
|
119
|
+
FROM tasks t
|
|
120
|
+
JOIN workflows w ON t.workflow_id = w.id
|
|
121
|
+
{where_clause}
|
|
122
|
+
{order_clause}
|
|
123
|
+
LIMIT {params.per_page} OFFSET {offset}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
tasks = await db.query(tasks_sql, tuple(query_params))
|
|
127
|
+
|
|
128
|
+
# Convert to response models
|
|
129
|
+
task_summaries = [
|
|
130
|
+
TaskSummary(
|
|
131
|
+
id=t["id"],
|
|
132
|
+
workflow_id=t["workflow_id"],
|
|
133
|
+
workflow_name=t["workflow_name"],
|
|
134
|
+
kind=TaskKind(t["kind"]),
|
|
135
|
+
target=t["target"],
|
|
136
|
+
status=TaskStatus(t["status"]),
|
|
137
|
+
attempts=t["attempts"],
|
|
138
|
+
max_attempts=t["max_attempts"],
|
|
139
|
+
run_at=t["run_at"],
|
|
140
|
+
created_at=t["created_at"],
|
|
141
|
+
updated_at=t["updated_at"],
|
|
142
|
+
last_error=t["last_error"],
|
|
143
|
+
)
|
|
144
|
+
for t in tasks
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Build pagination metadata
|
|
148
|
+
meta = PaginationMeta(
|
|
149
|
+
page=params.page,
|
|
150
|
+
per_page=params.per_page,
|
|
151
|
+
total=total,
|
|
152
|
+
pages=pages,
|
|
153
|
+
has_prev=params.page > 1,
|
|
154
|
+
has_next=params.page < pages,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return PaginatedResponse(data=task_summaries, meta=meta)
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise HTTPException(status_code=500, detail=f"Failed to list tasks: {str(e)}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@router.get(
|
|
164
|
+
"/{task_id}",
|
|
165
|
+
response_model=TaskDetail,
|
|
166
|
+
summary="Get task details",
|
|
167
|
+
description="""
|
|
168
|
+
Retrieve complete information for a specific task.
|
|
169
|
+
|
|
170
|
+
**Returns:**
|
|
171
|
+
- Task execution metadata and current status
|
|
172
|
+
- Retry attempt information and error details
|
|
173
|
+
- Parent workflow information
|
|
174
|
+
- Execution timing and scheduling data
|
|
175
|
+
|
|
176
|
+
**Use Cases:**
|
|
177
|
+
- Debug failed or stuck tasks
|
|
178
|
+
- Monitor task retry behavior
|
|
179
|
+
- Understand task execution patterns
|
|
180
|
+
""",
|
|
181
|
+
responses={
|
|
182
|
+
404: {"model": ErrorResponse, "description": "Task not found"},
|
|
183
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
async def get_task(task_id: str, db: Database = Depends(get_db)):
|
|
187
|
+
"""Get detailed task information"""
|
|
188
|
+
try:
|
|
189
|
+
# Get task info with workflow name
|
|
190
|
+
task_sql = """
|
|
191
|
+
SELECT
|
|
192
|
+
t.id,
|
|
193
|
+
t.workflow_id,
|
|
194
|
+
w.name as workflow_name,
|
|
195
|
+
t.kind,
|
|
196
|
+
t.target,
|
|
197
|
+
t.status,
|
|
198
|
+
t.attempts,
|
|
199
|
+
t.max_attempts,
|
|
200
|
+
t.run_at,
|
|
201
|
+
t.created_at,
|
|
202
|
+
t.updated_at,
|
|
203
|
+
t.last_error
|
|
204
|
+
FROM tasks t
|
|
205
|
+
JOIN workflows w ON t.workflow_id = w.id
|
|
206
|
+
WHERE t.id = ?
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
task = await db.fetchone(task_sql, (task_id,))
|
|
210
|
+
if not task:
|
|
211
|
+
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
|
|
212
|
+
|
|
213
|
+
return TaskDetail(
|
|
214
|
+
id=task["id"],
|
|
215
|
+
workflow_id=task["workflow_id"],
|
|
216
|
+
workflow_name=task["workflow_name"],
|
|
217
|
+
kind=TaskKind(task["kind"]),
|
|
218
|
+
target=task["target"],
|
|
219
|
+
status=TaskStatus(task["status"]),
|
|
220
|
+
attempts=task["attempts"],
|
|
221
|
+
max_attempts=task["max_attempts"],
|
|
222
|
+
run_at=task["run_at"],
|
|
223
|
+
created_at=task["created_at"],
|
|
224
|
+
updated_at=task["updated_at"],
|
|
225
|
+
last_error=task["last_error"],
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
except HTTPException:
|
|
229
|
+
raise
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise HTTPException(status_code=500, detail=f"Failed to get task: {str(e)}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.get(
|
|
235
|
+
"/pending",
|
|
236
|
+
response_model=PaginatedResponse[TaskSummary],
|
|
237
|
+
summary="List pending tasks",
|
|
238
|
+
description="""
|
|
239
|
+
Retrieve tasks that are ready to be executed (status=PENDING, run_at <= now).
|
|
240
|
+
|
|
241
|
+
**Optimized endpoint for:**
|
|
242
|
+
- Task queue monitoring
|
|
243
|
+
- Worker load balancing
|
|
244
|
+
- Execution scheduling insights
|
|
245
|
+
|
|
246
|
+
**Sorting:**
|
|
247
|
+
- Default sort by `run_at` ascending (oldest first)
|
|
248
|
+
- Shows tasks in execution priority order
|
|
249
|
+
""",
|
|
250
|
+
responses={500: {"model": ErrorResponse, "description": "Internal server error"}},
|
|
251
|
+
)
|
|
252
|
+
async def list_pending_tasks(
|
|
253
|
+
page: int = 1, per_page: int = 50, db: Database = Depends(get_db)
|
|
254
|
+
):
|
|
255
|
+
"""List pending tasks ready for execution"""
|
|
256
|
+
try:
|
|
257
|
+
from datetime import datetime
|
|
258
|
+
|
|
259
|
+
now = datetime.now().isoformat()
|
|
260
|
+
|
|
261
|
+
# Get total count of pending tasks
|
|
262
|
+
count_sql = """
|
|
263
|
+
SELECT COUNT(*) as total
|
|
264
|
+
FROM tasks t
|
|
265
|
+
JOIN workflows w ON t.workflow_id = w.id
|
|
266
|
+
WHERE t.status = 'PENDING' AND t.run_at <= ?
|
|
267
|
+
"""
|
|
268
|
+
count_result = await db.fetchone(count_sql, (now,))
|
|
269
|
+
total = count_result["total"] if count_result else 0
|
|
270
|
+
|
|
271
|
+
# Calculate pagination
|
|
272
|
+
pages = math.ceil(total / per_page) if total > 0 else 1
|
|
273
|
+
offset = (page - 1) * per_page
|
|
274
|
+
|
|
275
|
+
# Get pending tasks ordered by run_at (oldest first)
|
|
276
|
+
tasks_sql = """
|
|
277
|
+
SELECT
|
|
278
|
+
t.id,
|
|
279
|
+
t.workflow_id,
|
|
280
|
+
w.name as workflow_name,
|
|
281
|
+
t.kind,
|
|
282
|
+
t.target,
|
|
283
|
+
t.status,
|
|
284
|
+
t.attempts,
|
|
285
|
+
t.max_attempts,
|
|
286
|
+
t.run_at,
|
|
287
|
+
t.created_at,
|
|
288
|
+
t.updated_at,
|
|
289
|
+
t.last_error
|
|
290
|
+
FROM tasks t
|
|
291
|
+
JOIN workflows w ON t.workflow_id = w.id
|
|
292
|
+
WHERE t.status = 'PENDING' AND t.run_at <= ?
|
|
293
|
+
ORDER BY t.run_at ASC
|
|
294
|
+
LIMIT ? OFFSET ?
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
tasks = await db.query(tasks_sql, (now, per_page, offset))
|
|
298
|
+
|
|
299
|
+
# Convert to response models
|
|
300
|
+
task_summaries = [
|
|
301
|
+
TaskSummary(
|
|
302
|
+
id=t["id"],
|
|
303
|
+
workflow_id=t["workflow_id"],
|
|
304
|
+
workflow_name=t["workflow_name"],
|
|
305
|
+
kind=TaskKind(t["kind"]),
|
|
306
|
+
target=t["target"],
|
|
307
|
+
status=TaskStatus(t["status"]),
|
|
308
|
+
attempts=t["attempts"],
|
|
309
|
+
max_attempts=t["max_attempts"],
|
|
310
|
+
run_at=t["run_at"],
|
|
311
|
+
created_at=t["created_at"],
|
|
312
|
+
updated_at=t["updated_at"],
|
|
313
|
+
last_error=t["last_error"],
|
|
314
|
+
)
|
|
315
|
+
for t in tasks
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
# Build pagination metadata
|
|
319
|
+
meta = PaginationMeta(
|
|
320
|
+
page=page,
|
|
321
|
+
per_page=per_page,
|
|
322
|
+
total=total,
|
|
323
|
+
pages=pages,
|
|
324
|
+
has_prev=page > 1,
|
|
325
|
+
has_next=page < pages,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return PaginatedResponse(data=task_summaries, meta=meta)
|
|
329
|
+
|
|
330
|
+
except Exception as e:
|
|
331
|
+
raise HTTPException(
|
|
332
|
+
status_code=500, detail=f"Failed to list pending tasks: {str(e)}"
|
|
333
|
+
)
|