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/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
+ )