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/schemas.py
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"""API Schema Definitions
|
|
2
|
+
|
|
3
|
+
This module contains Pydantic models that define the structure of API requests
|
|
4
|
+
and responses for the Loom web dashboard.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
# === Enums ===
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WorkflowStatus(str, Enum):
|
|
17
|
+
"""Workflow execution status"""
|
|
18
|
+
|
|
19
|
+
RUNNING = "RUNNING"
|
|
20
|
+
COMPLETED = "COMPLETED"
|
|
21
|
+
FAILED = "FAILED"
|
|
22
|
+
CANCELED = "CANCELED"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskStatus(str, Enum):
|
|
26
|
+
"""Task execution status"""
|
|
27
|
+
|
|
28
|
+
PENDING = "PENDING"
|
|
29
|
+
RUNNING = "RUNNING"
|
|
30
|
+
COMPLETED = "COMPLETED"
|
|
31
|
+
FAILED = "FAILED"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TaskKind(str, Enum):
|
|
35
|
+
"""Task type/kind"""
|
|
36
|
+
|
|
37
|
+
STEP = "STEP"
|
|
38
|
+
ACTIVITY = "ACTIVITY"
|
|
39
|
+
TIMER = "TIMER"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EventType(str, Enum):
|
|
43
|
+
"""Workflow event types"""
|
|
44
|
+
|
|
45
|
+
WORKFLOW_STARTED = "WORKFLOW_STARTED"
|
|
46
|
+
WORKFLOW_COMPLETED = "WORKFLOW_COMPLETED"
|
|
47
|
+
WORKFLOW_FAILED = "WORKFLOW_FAILED"
|
|
48
|
+
WORKFLOW_CANCELLED = "WORKFLOW_CANCELLED"
|
|
49
|
+
STEP_START = "STEP_START"
|
|
50
|
+
STEP_END = "STEP_END"
|
|
51
|
+
STATE_SET = "STATE_SET"
|
|
52
|
+
STATE_UPDATE = "STATE_UPDATE"
|
|
53
|
+
ACTIVITY_SCHEDULED = "ACTIVITY_SCHEDULED"
|
|
54
|
+
ACTIVITY_COMPLETED = "ACTIVITY_COMPLETED"
|
|
55
|
+
ACTIVITY_FAILED = "ACTIVITY_FAILED"
|
|
56
|
+
TIMER_SCHEDULED = "TIMER_SCHEDULED"
|
|
57
|
+
TIMER_FIRED = "TIMER_FIRED"
|
|
58
|
+
SIGNAL_RECEIVED = "SIGNAL_RECEIVED"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LogLevel(str, Enum):
|
|
62
|
+
"""Log severity levels"""
|
|
63
|
+
|
|
64
|
+
DEBUG = "DEBUG"
|
|
65
|
+
INFO = "INFO"
|
|
66
|
+
WARNING = "WARNING"
|
|
67
|
+
ERROR = "ERROR"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# === Base Response Models ===
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PaginationMeta(BaseModel):
|
|
74
|
+
"""Pagination metadata for list responses"""
|
|
75
|
+
|
|
76
|
+
page: int = Field(..., description="Current page number (1-based)")
|
|
77
|
+
per_page: int = Field(..., description="Items per page")
|
|
78
|
+
total: int = Field(..., description="Total number of items")
|
|
79
|
+
pages: int = Field(..., description="Total number of pages")
|
|
80
|
+
has_prev: bool = Field(..., description="True if there is a previous page")
|
|
81
|
+
has_next: bool = Field(..., description="True if there is a next page")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PaginatedResponse[T](BaseModel):
|
|
85
|
+
"""Generic paginated response wrapper"""
|
|
86
|
+
|
|
87
|
+
data: List[T] = Field(..., description="List of items for current page")
|
|
88
|
+
meta: PaginationMeta = Field(..., description="Pagination metadata")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# === Workflow Models ===
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WorkflowSummary(BaseModel):
|
|
95
|
+
"""Workflow summary for list views"""
|
|
96
|
+
|
|
97
|
+
id: str = Field(
|
|
98
|
+
...,
|
|
99
|
+
description="Unique workflow identifier",
|
|
100
|
+
examples=["wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0"],
|
|
101
|
+
)
|
|
102
|
+
name: str = Field(
|
|
103
|
+
...,
|
|
104
|
+
description="Workflow name/type",
|
|
105
|
+
examples=[
|
|
106
|
+
"UserRegistrationWorkflow",
|
|
107
|
+
"OrderProcessingWorkflow",
|
|
108
|
+
"DataPipelineWorkflow",
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
status: WorkflowStatus = Field(..., description="Current execution status")
|
|
112
|
+
created_at: datetime = Field(
|
|
113
|
+
..., description="When workflow was created (ISO 8601 format)"
|
|
114
|
+
)
|
|
115
|
+
updated_at: datetime = Field(
|
|
116
|
+
..., description="When workflow was last updated (ISO 8601 format)"
|
|
117
|
+
)
|
|
118
|
+
duration: Optional[int] = Field(
|
|
119
|
+
None,
|
|
120
|
+
description="Execution duration in seconds (if completed)",
|
|
121
|
+
examples=[120, 1800, 45],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
class Config:
|
|
125
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
126
|
+
json_schema_extra = {
|
|
127
|
+
"examples": [
|
|
128
|
+
{
|
|
129
|
+
"id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0",
|
|
130
|
+
"name": "UserRegistrationWorkflow",
|
|
131
|
+
"status": "COMPLETED",
|
|
132
|
+
"created_at": "2026-01-29T10:15:00Z",
|
|
133
|
+
"updated_at": "2026-01-29T10:17:30Z",
|
|
134
|
+
"duration": 150,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F1",
|
|
138
|
+
"name": "OrderProcessingWorkflow",
|
|
139
|
+
"status": "RUNNING",
|
|
140
|
+
"created_at": "2026-01-29T10:20:00Z",
|
|
141
|
+
"updated_at": "2026-01-29T10:25:15Z",
|
|
142
|
+
"duration": None,
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class WorkflowDetail(BaseModel):
|
|
149
|
+
"""Complete workflow information"""
|
|
150
|
+
|
|
151
|
+
id: str = Field(
|
|
152
|
+
...,
|
|
153
|
+
description="Unique workflow identifier",
|
|
154
|
+
examples=["wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0"],
|
|
155
|
+
)
|
|
156
|
+
name: str = Field(
|
|
157
|
+
..., description="Workflow name/type", examples=["UserRegistrationWorkflow"]
|
|
158
|
+
)
|
|
159
|
+
description: Optional[str] = Field(
|
|
160
|
+
None,
|
|
161
|
+
description="Workflow description",
|
|
162
|
+
examples=[
|
|
163
|
+
"Handles new user registration with email verification and profile setup"
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
version: str = Field(
|
|
167
|
+
..., description="Workflow version", examples=["1.0.0", "2.1.3", "1.0.0-beta.1"]
|
|
168
|
+
)
|
|
169
|
+
module: str = Field(
|
|
170
|
+
...,
|
|
171
|
+
description="Python module containing workflow",
|
|
172
|
+
examples=["workflows.user_registration", "workflows.order_processing"],
|
|
173
|
+
)
|
|
174
|
+
status: WorkflowStatus = Field(..., description="Current execution status")
|
|
175
|
+
input: Dict[str, Any] = Field(
|
|
176
|
+
...,
|
|
177
|
+
description="Workflow input data (JSON)",
|
|
178
|
+
examples=[
|
|
179
|
+
{"user_id": 12345, "email": "user@example.com", "plan": "premium"},
|
|
180
|
+
{"order_id": "ord_123", "items": [{"id": "item_1", "quantity": 2}]},
|
|
181
|
+
],
|
|
182
|
+
)
|
|
183
|
+
created_at: datetime = Field(
|
|
184
|
+
..., description="When workflow was created (ISO 8601 format)"
|
|
185
|
+
)
|
|
186
|
+
updated_at: datetime = Field(
|
|
187
|
+
..., description="When workflow was last updated (ISO 8601 format)"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Computed fields
|
|
191
|
+
duration: Optional[int] = Field(
|
|
192
|
+
None, description="Execution duration in seconds", examples=[150, 3600, None]
|
|
193
|
+
)
|
|
194
|
+
event_count: int = Field(
|
|
195
|
+
..., description="Total number of events", examples=[12, 45, 3]
|
|
196
|
+
)
|
|
197
|
+
current_state: Dict[str, Any] = Field(
|
|
198
|
+
default_factory=dict,
|
|
199
|
+
description="Current workflow state reconstructed from events",
|
|
200
|
+
examples=[
|
|
201
|
+
{"step": "email_verification", "verified": True, "profile_created": False},
|
|
202
|
+
{"order_status": "payment_pending", "inventory_reserved": True},
|
|
203
|
+
],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
class Config:
|
|
207
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
208
|
+
json_schema_extra = {
|
|
209
|
+
"examples": [
|
|
210
|
+
{
|
|
211
|
+
"id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0",
|
|
212
|
+
"name": "UserRegistrationWorkflow",
|
|
213
|
+
"description": "Handles new user registration with email verification",
|
|
214
|
+
"version": "1.0.0",
|
|
215
|
+
"module": "workflows.user_registration",
|
|
216
|
+
"status": "COMPLETED",
|
|
217
|
+
"input": {
|
|
218
|
+
"user_id": 12345,
|
|
219
|
+
"email": "user@example.com",
|
|
220
|
+
"plan": "premium",
|
|
221
|
+
},
|
|
222
|
+
"created_at": "2026-01-29T10:15:00Z",
|
|
223
|
+
"updated_at": "2026-01-29T10:17:30Z",
|
|
224
|
+
"duration": 150,
|
|
225
|
+
"event_count": 12,
|
|
226
|
+
"current_state": {
|
|
227
|
+
"email_verified": True,
|
|
228
|
+
"profile_created": True,
|
|
229
|
+
"welcome_email_sent": True,
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# === Task Models ===
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class TaskSummary(BaseModel):
|
|
240
|
+
"""Task summary for list views"""
|
|
241
|
+
|
|
242
|
+
id: str = Field(
|
|
243
|
+
...,
|
|
244
|
+
description="Unique task identifier",
|
|
245
|
+
examples=["task_01HQK8XA1B2C3D4E5F6G7H8I9J"],
|
|
246
|
+
)
|
|
247
|
+
workflow_id: str = Field(
|
|
248
|
+
...,
|
|
249
|
+
description="Parent workflow ID",
|
|
250
|
+
examples=["wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0"],
|
|
251
|
+
)
|
|
252
|
+
workflow_name: str = Field(
|
|
253
|
+
..., description="Parent workflow name", examples=["UserRegistrationWorkflow"]
|
|
254
|
+
)
|
|
255
|
+
kind: TaskKind = Field(..., description="Task type/kind")
|
|
256
|
+
target: str = Field(
|
|
257
|
+
...,
|
|
258
|
+
description="Task target (step name, activity name, etc.)",
|
|
259
|
+
examples=[
|
|
260
|
+
"send_welcome_email",
|
|
261
|
+
"UserRegistration.verify_email",
|
|
262
|
+
"payment_timeout",
|
|
263
|
+
],
|
|
264
|
+
)
|
|
265
|
+
status: TaskStatus = Field(..., description="Current task status")
|
|
266
|
+
attempts: int = Field(
|
|
267
|
+
..., description="Number of execution attempts", examples=[1, 2, 3]
|
|
268
|
+
)
|
|
269
|
+
max_attempts: int = Field(
|
|
270
|
+
..., description="Maximum allowed attempts", examples=[3, 5, 10]
|
|
271
|
+
)
|
|
272
|
+
run_at: datetime = Field(
|
|
273
|
+
..., description="When task should/did run (ISO 8601 format)"
|
|
274
|
+
)
|
|
275
|
+
created_at: datetime = Field(
|
|
276
|
+
..., description="When task was created (ISO 8601 format)"
|
|
277
|
+
)
|
|
278
|
+
updated_at: Optional[datetime] = Field(
|
|
279
|
+
None, description="When task was last updated (ISO 8601 format)"
|
|
280
|
+
)
|
|
281
|
+
last_error: Optional[str] = Field(
|
|
282
|
+
None,
|
|
283
|
+
description="Last error message (if failed)",
|
|
284
|
+
examples=[
|
|
285
|
+
"SMTP connection failed: Unable to connect to mail server",
|
|
286
|
+
"HTTP 429: Rate limit exceeded",
|
|
287
|
+
"Database connection timeout after 30s",
|
|
288
|
+
],
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
class Config:
|
|
292
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
293
|
+
json_schema_extra = {
|
|
294
|
+
"examples": [
|
|
295
|
+
{
|
|
296
|
+
"id": "task_01HQK8XA1B2C3D4E5F6G7H8I9J",
|
|
297
|
+
"workflow_id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0",
|
|
298
|
+
"workflow_name": "UserRegistrationWorkflow",
|
|
299
|
+
"kind": "ACTIVITY",
|
|
300
|
+
"target": "send_welcome_email",
|
|
301
|
+
"status": "COMPLETED",
|
|
302
|
+
"attempts": 1,
|
|
303
|
+
"max_attempts": 3,
|
|
304
|
+
"run_at": "2026-01-29T10:16:00Z",
|
|
305
|
+
"created_at": "2026-01-29T10:15:30Z",
|
|
306
|
+
"updated_at": "2026-01-29T10:16:15Z",
|
|
307
|
+
"last_error": None,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"id": "task_01HQK8XA1B2C3D4E5F6G7H8I9K",
|
|
311
|
+
"workflow_id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F1",
|
|
312
|
+
"workflow_name": "OrderProcessingWorkflow",
|
|
313
|
+
"kind": "ACTIVITY",
|
|
314
|
+
"target": "charge_payment",
|
|
315
|
+
"status": "FAILED",
|
|
316
|
+
"attempts": 3,
|
|
317
|
+
"max_attempts": 3,
|
|
318
|
+
"run_at": "2026-01-29T10:21:30Z",
|
|
319
|
+
"created_at": "2026-01-29T10:21:00Z",
|
|
320
|
+
"updated_at": "2026-01-29T10:23:45Z",
|
|
321
|
+
"last_error": "Payment failed: Insufficient funds",
|
|
322
|
+
},
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class TaskDetail(TaskSummary):
|
|
328
|
+
"""Complete task information (extends TaskSummary)"""
|
|
329
|
+
|
|
330
|
+
...
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# === Event Models ===
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class EventSummary(BaseModel):
|
|
337
|
+
"""Event summary for list views"""
|
|
338
|
+
|
|
339
|
+
id: int = Field(..., description="Unique event identifier (sequence number)")
|
|
340
|
+
workflow_id: str = Field(..., description="Parent workflow ID")
|
|
341
|
+
type: EventType = Field(..., description="Event type")
|
|
342
|
+
payload: Dict[str, Any] = Field(..., description="Event data (JSON)")
|
|
343
|
+
created_at: datetime = Field(..., description="When event was created")
|
|
344
|
+
|
|
345
|
+
class Config:
|
|
346
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class EventDetail(EventSummary):
|
|
350
|
+
"""Complete event information (extends EventSummary)"""
|
|
351
|
+
|
|
352
|
+
workflow_name: str = Field(..., description="Parent workflow name")
|
|
353
|
+
workflow_status: WorkflowStatus = Field(
|
|
354
|
+
..., description="Workflow status when event occurred"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# === Log Models ===
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class LogEntry(BaseModel):
|
|
362
|
+
"""Workflow log entry"""
|
|
363
|
+
|
|
364
|
+
id: int = Field(..., description="Unique log entry identifier")
|
|
365
|
+
workflow_id: str = Field(..., description="Parent workflow ID")
|
|
366
|
+
workflow_name: str = Field(..., description="Parent workflow name")
|
|
367
|
+
level: LogLevel = Field(..., description="Log severity level")
|
|
368
|
+
message: str = Field(..., description="Log message content")
|
|
369
|
+
created_at: datetime = Field(..., description="When log entry was created")
|
|
370
|
+
|
|
371
|
+
class Config:
|
|
372
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# === Statistics Models ===
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class WorkflowStats(BaseModel):
|
|
379
|
+
"""Workflow execution statistics"""
|
|
380
|
+
|
|
381
|
+
total: int = Field(..., description="Total number of workflows")
|
|
382
|
+
running: int = Field(..., description="Number of running workflows")
|
|
383
|
+
completed: int = Field(..., description="Number of completed workflows")
|
|
384
|
+
failed: int = Field(..., description="Number of failed workflows")
|
|
385
|
+
canceled: int = Field(..., description="Number of canceled workflows")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class TaskStats(BaseModel):
|
|
389
|
+
"""Task execution statistics"""
|
|
390
|
+
|
|
391
|
+
total: int = Field(..., description="Total number of tasks")
|
|
392
|
+
pending: int = Field(..., description="Number of pending tasks")
|
|
393
|
+
running: int = Field(..., description="Number of running tasks")
|
|
394
|
+
completed: int = Field(..., description="Number of completed tasks")
|
|
395
|
+
failed: int = Field(..., description="Number of failed tasks")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class SystemStats(BaseModel):
|
|
399
|
+
"""Overall system statistics"""
|
|
400
|
+
|
|
401
|
+
workflows: WorkflowStats = Field(..., description="Workflow statistics")
|
|
402
|
+
tasks: TaskStats = Field(..., description="Task statistics")
|
|
403
|
+
events: int = Field(..., description="Total number of events")
|
|
404
|
+
logs: int = Field(..., description="Total number of log entries")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# === Request Models ===
|
|
408
|
+
|
|
409
|
+
# === Request Parameter Models ===
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class WorkflowListParams(BaseModel):
|
|
413
|
+
"""Parameters for workflow list endpoint"""
|
|
414
|
+
|
|
415
|
+
page: int = Field(1, ge=1, description="Page number (1-based)", examples=[1, 2, 10])
|
|
416
|
+
per_page: int = Field(
|
|
417
|
+
50,
|
|
418
|
+
ge=1,
|
|
419
|
+
le=1000,
|
|
420
|
+
description="Items per page (max 1000)",
|
|
421
|
+
examples=[10, 50, 100],
|
|
422
|
+
)
|
|
423
|
+
status: Optional[WorkflowStatus] = Field(
|
|
424
|
+
None, description="Filter by workflow execution status"
|
|
425
|
+
)
|
|
426
|
+
name: Optional[str] = Field(
|
|
427
|
+
None,
|
|
428
|
+
description="Filter by workflow name (partial match, case-insensitive)",
|
|
429
|
+
examples=["User", "Registration", "Order"],
|
|
430
|
+
)
|
|
431
|
+
sort_by: str = Field(
|
|
432
|
+
"created_at",
|
|
433
|
+
description="Field to sort by",
|
|
434
|
+
examples=["created_at", "updated_at", "name", "status"],
|
|
435
|
+
)
|
|
436
|
+
sort_order: str = Field(
|
|
437
|
+
"desc",
|
|
438
|
+
pattern="^(asc|desc)$",
|
|
439
|
+
description="Sort order (asc=ascending, desc=descending)",
|
|
440
|
+
examples=["asc", "desc"],
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
class Config:
|
|
444
|
+
json_schema_extra = {
|
|
445
|
+
"examples": [
|
|
446
|
+
{
|
|
447
|
+
"page": 1,
|
|
448
|
+
"per_page": 50,
|
|
449
|
+
"status": "RUNNING",
|
|
450
|
+
"name": "User",
|
|
451
|
+
"sort_by": "created_at",
|
|
452
|
+
"sort_order": "desc",
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class TaskListParams(BaseModel):
|
|
459
|
+
"""Parameters for task list endpoint"""
|
|
460
|
+
|
|
461
|
+
page: int = Field(1, ge=1, description="Page number (1-based)", examples=[1, 2, 5])
|
|
462
|
+
per_page: int = Field(
|
|
463
|
+
50,
|
|
464
|
+
ge=1,
|
|
465
|
+
le=1000,
|
|
466
|
+
description="Items per page (max 1000)",
|
|
467
|
+
examples=[25, 50, 100],
|
|
468
|
+
)
|
|
469
|
+
workflow_id: Optional[str] = Field(
|
|
470
|
+
None,
|
|
471
|
+
description="Filter by specific workflow ID",
|
|
472
|
+
examples=["wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0"],
|
|
473
|
+
)
|
|
474
|
+
status: Optional[TaskStatus] = Field(
|
|
475
|
+
None, description="Filter by task execution status"
|
|
476
|
+
)
|
|
477
|
+
kind: Optional[TaskKind] = Field(None, description="Filter by task type/kind")
|
|
478
|
+
sort_by: str = Field(
|
|
479
|
+
"created_at",
|
|
480
|
+
description="Field to sort by",
|
|
481
|
+
examples=["created_at", "run_at", "status", "attempts"],
|
|
482
|
+
)
|
|
483
|
+
sort_order: str = Field(
|
|
484
|
+
"desc",
|
|
485
|
+
pattern="^(asc|desc)$",
|
|
486
|
+
description="Sort order (asc=ascending, desc=descending)",
|
|
487
|
+
examples=["asc", "desc"],
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
class Config:
|
|
491
|
+
json_schema_extra = {
|
|
492
|
+
"examples": [
|
|
493
|
+
{
|
|
494
|
+
"page": 1,
|
|
495
|
+
"per_page": 50,
|
|
496
|
+
"workflow_id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0",
|
|
497
|
+
"status": "PENDING",
|
|
498
|
+
"kind": "ACTIVITY",
|
|
499
|
+
"sort_by": "run_at",
|
|
500
|
+
"sort_order": "asc",
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class EventListParams(BaseModel):
|
|
507
|
+
"""Parameters for event list endpoint"""
|
|
508
|
+
|
|
509
|
+
page: int = Field(1, ge=1, description="Page number (1-based)", examples=[1, 3, 15])
|
|
510
|
+
per_page: int = Field(
|
|
511
|
+
100,
|
|
512
|
+
ge=1,
|
|
513
|
+
le=1000,
|
|
514
|
+
description="Items per page (max 1000)",
|
|
515
|
+
examples=[50, 100, 200],
|
|
516
|
+
)
|
|
517
|
+
workflow_id: Optional[str] = Field(
|
|
518
|
+
None,
|
|
519
|
+
description="Filter by specific workflow ID",
|
|
520
|
+
examples=["wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0"],
|
|
521
|
+
)
|
|
522
|
+
type: Optional[EventType] = Field(None, description="Filter by event type")
|
|
523
|
+
since: Optional[datetime] = Field(
|
|
524
|
+
None, description="Filter events created after this timestamp (ISO 8601 format)"
|
|
525
|
+
)
|
|
526
|
+
sort_by: str = Field(
|
|
527
|
+
"id", description="Field to sort by", examples=["id", "created_at", "type"]
|
|
528
|
+
)
|
|
529
|
+
sort_order: str = Field(
|
|
530
|
+
"desc",
|
|
531
|
+
pattern="^(asc|desc)$",
|
|
532
|
+
description="Sort order (asc=ascending, desc=descending)",
|
|
533
|
+
examples=["asc", "desc"],
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
class Config:
|
|
537
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
538
|
+
json_schema_extra = {
|
|
539
|
+
"examples": [
|
|
540
|
+
{
|
|
541
|
+
"page": 1,
|
|
542
|
+
"per_page": 100,
|
|
543
|
+
"workflow_id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0",
|
|
544
|
+
"type": "ACTIVITY_COMPLETED",
|
|
545
|
+
"since": "2026-01-29T10:00:00Z",
|
|
546
|
+
"sort_by": "created_at",
|
|
547
|
+
"sort_order": "desc",
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class LogListParams(BaseModel):
|
|
554
|
+
"""Parameters for log list endpoint"""
|
|
555
|
+
|
|
556
|
+
page: int = Field(1, ge=1, description="Page number (1-based)", examples=[1, 2, 8])
|
|
557
|
+
per_page: int = Field(
|
|
558
|
+
100,
|
|
559
|
+
ge=1,
|
|
560
|
+
le=1000,
|
|
561
|
+
description="Items per page (max 1000)",
|
|
562
|
+
examples=[50, 100, 250],
|
|
563
|
+
)
|
|
564
|
+
workflow_id: Optional[str] = Field(
|
|
565
|
+
None,
|
|
566
|
+
description="Filter by specific workflow ID",
|
|
567
|
+
examples=["wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0"],
|
|
568
|
+
)
|
|
569
|
+
level: Optional[LogLevel] = Field(None, description="Filter by minimum log level")
|
|
570
|
+
since: Optional[datetime] = Field(
|
|
571
|
+
None, description="Filter logs created after this timestamp (ISO 8601 format)"
|
|
572
|
+
)
|
|
573
|
+
sort_by: str = Field(
|
|
574
|
+
"created_at",
|
|
575
|
+
description="Field to sort by",
|
|
576
|
+
examples=["created_at", "level", "workflow_id"],
|
|
577
|
+
)
|
|
578
|
+
sort_order: str = Field(
|
|
579
|
+
"desc",
|
|
580
|
+
pattern="^(asc|desc)$",
|
|
581
|
+
description="Sort order (asc=ascending, desc=descending)",
|
|
582
|
+
examples=["asc", "desc"],
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
class Config:
|
|
586
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
587
|
+
json_schema_extra = {
|
|
588
|
+
"examples": [
|
|
589
|
+
{
|
|
590
|
+
"page": 1,
|
|
591
|
+
"per_page": 100,
|
|
592
|
+
"workflow_id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0",
|
|
593
|
+
"level": "ERROR",
|
|
594
|
+
"since": "2026-01-29T09:00:00Z",
|
|
595
|
+
"sort_by": "created_at",
|
|
596
|
+
"sort_order": "desc",
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# === Error Models ===
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class ErrorResponse(BaseModel):
|
|
606
|
+
"""Standard error response"""
|
|
607
|
+
|
|
608
|
+
error: str = Field(
|
|
609
|
+
...,
|
|
610
|
+
description="Error type/code",
|
|
611
|
+
examples=["VALIDATION_ERROR", "NOT_FOUND", "INTERNAL_ERROR", "BAD_REQUEST"],
|
|
612
|
+
)
|
|
613
|
+
message: str = Field(
|
|
614
|
+
...,
|
|
615
|
+
description="Human-readable error message",
|
|
616
|
+
examples=[
|
|
617
|
+
"Workflow not found",
|
|
618
|
+
"Invalid page parameter: must be >= 1",
|
|
619
|
+
"Database connection failed",
|
|
620
|
+
"Invalid sort field: 'invalid_field'",
|
|
621
|
+
],
|
|
622
|
+
)
|
|
623
|
+
details: Optional[Dict[str, Any]] = Field(
|
|
624
|
+
None,
|
|
625
|
+
description="Additional error details and context",
|
|
626
|
+
examples=[
|
|
627
|
+
{
|
|
628
|
+
"workflow_id": "wf_123",
|
|
629
|
+
"available_fields": ["created_at", "updated_at", "name"],
|
|
630
|
+
},
|
|
631
|
+
{"field": "per_page", "max_value": 1000, "provided": 1500},
|
|
632
|
+
],
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
class Config:
|
|
636
|
+
json_schema_extra = {
|
|
637
|
+
"examples": [
|
|
638
|
+
{
|
|
639
|
+
"error": "NOT_FOUND",
|
|
640
|
+
"message": "Workflow 'wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0' not found",
|
|
641
|
+
"details": {
|
|
642
|
+
"workflow_id": "wf_01HQK8X9M2Y3Z4A5B6C7D8E9F0",
|
|
643
|
+
"resource": "workflow",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
"error": "VALIDATION_ERROR",
|
|
648
|
+
"message": "Invalid pagination parameters",
|
|
649
|
+
"details": {
|
|
650
|
+
"field": "per_page",
|
|
651
|
+
"max_value": 1000,
|
|
652
|
+
"provided": 1500,
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
]
|
|
656
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
loom/__init__.py,sha256=
|
|
1
|
+
loom/__init__.py,sha256=N-Txt2Gs1avUyEKPfk5Iwz2T123ja0UOnIGDbjW3NDY,1587
|
|
2
2
|
loom/__main__.py,sha256=7GEA5PPS_5CBdaXC5sZ64SlwScccpJNhF9KLiAwve_s,161
|
|
3
3
|
loom/cli/__init__.py,sha256=BI8VfSIB5MF4_D9GxHFJwynNEvosDHReVGvsliS99ho,73
|
|
4
4
|
loom/cli/cli.py,sha256=pk1qvciybKWPiLKBx0VRJIdmdeIUISVohO77xwMeil8,9695
|
|
@@ -45,9 +45,19 @@ loom/schemas/events.py,sha256=Gz-R836nVXNSsp4Y54idhKs_WTvzFPmx5KlA8PunP28,2216
|
|
|
45
45
|
loom/schemas/graph.py,sha256=DQ_XfZzSPWjzhmyuqdO2zpJCuSMblu2qZW1qQCn_Ebg,5684
|
|
46
46
|
loom/schemas/tasks.py,sha256=DJbLInggIGxI-CfP1kSyK79Bz83e3sZyEKh6C2HE8q4,1341
|
|
47
47
|
loom/schemas/workflow.py,sha256=F1cNEJRuEWLjPhGinEZE2T6vwchDC8RuTHmxEIaH6Ls,671
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
loom/web/__init__.py,sha256=PZC66J5iBFo4PPOkj7QR9RNab8BbQcOHyy_YbXLQr1Y,200
|
|
49
|
+
loom/web/main.py,sha256=IEhCNYSxVLmNHJ0-unCE6c2IYvTdDZFKvIItjhPCBjk,10366
|
|
50
|
+
loom/web/schemas.py,sha256=yHVFA9xIH-jrVcai8EWSpl8_ZeiESj9vTxOZshzBjCI,21561
|
|
51
|
+
loom/web/api/__init__.py,sha256=QIKxk00wHRkU9lgPr-FNSOnoUStGpf08rZleXXM51GE,97
|
|
52
|
+
loom/web/api/events.py,sha256=mSP09HaI2lbgZ6rlXUCEYcqMUywGSQ7Id-fryLYXNUo,10292
|
|
53
|
+
loom/web/api/graphs.py,sha256=9zkrhWuCzRva1pDZ-qSCMnnrWVqo234iFh8j4bRh1Sg,8478
|
|
54
|
+
loom/web/api/logs.py,sha256=8xRparl14uiqliWrynlOBhH_KZZkxULWamqxd4tcoYs,10522
|
|
55
|
+
loom/web/api/stats.py,sha256=sZkxHzrTLuYJXRFXjSWyb5pxT4IKQCaCM_5fyr58meQ,10451
|
|
56
|
+
loom/web/api/tasks.py,sha256=U8Ap995pbjDW8jQAYNpzGb-9-Nd1Xr8PPHdY7L_pEAA,10383
|
|
57
|
+
loom/web/api/workflows.py,sha256=o8ExSo_Gy6azu3hHecu_IqgaMWpm5SY8kQ6VAJkqfxk,17927
|
|
58
|
+
loom_core-1.0.2.dist-info/licenses/LICENSE,sha256=8EpC-clAYRUfJQ92T3iQEIIWYjx2A3Kfk28zOd8lh7I,1095
|
|
59
|
+
loom_core-1.0.2.dist-info/METADATA,sha256=Q89x0_ObzD-N_u4a3Us_pruejShW-XCaf3TVPWnCXRE,9013
|
|
60
|
+
loom_core-1.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
61
|
+
loom_core-1.0.2.dist-info/entry_points.txt,sha256=Jx5HXHL2y7jvSjkwkH3QqF954cbSxiE6OGwL2coldyE,42
|
|
62
|
+
loom_core-1.0.2.dist-info/top_level.txt,sha256=cAfRgAuCuit-cU9iBrf0bS4ovvmq-URykNd9fmYMojg,5
|
|
63
|
+
loom_core-1.0.2.dist-info/RECORD,,
|
|
File without changes
|