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/main.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Loom Web Dashboard FastAPI Application
|
|
2
|
+
|
|
3
|
+
Main entry point for the web dashboard, providing REST APIs and server-sent events
|
|
4
|
+
for monitoring and managing Loom workflows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI, Request
|
|
14
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
15
|
+
from fastapi.responses import HTMLResponse
|
|
16
|
+
from fastapi.staticfiles import StaticFiles
|
|
17
|
+
|
|
18
|
+
from ..database.db import Database
|
|
19
|
+
from .api import events, graphs, logs, stats, tasks, workflows
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@asynccontextmanager
|
|
23
|
+
async def lifespan(app: FastAPI):
|
|
24
|
+
"""Application lifespan manager"""
|
|
25
|
+
# Startup: Initialize database
|
|
26
|
+
async with Database[Any, Any]() as db:
|
|
27
|
+
await db._init_db()
|
|
28
|
+
yield
|
|
29
|
+
# Shutdown: cleanup if needed
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
app = FastAPI(
|
|
33
|
+
title="Loom Workflow Dashboard",
|
|
34
|
+
description="""
|
|
35
|
+
## Loom Workflow Orchestration API
|
|
36
|
+
|
|
37
|
+
**Loom** is a Python-based durable workflow orchestration engine inspired by Temporal and Durable Task Framework.
|
|
38
|
+
This API provides comprehensive monitoring and management capabilities for workflows, tasks, events, and logs.
|
|
39
|
+
|
|
40
|
+
### Key Features
|
|
41
|
+
- **Event-sourced workflows** with automatic recovery and replay
|
|
42
|
+
- **Deterministic execution** with state reconstruction from events
|
|
43
|
+
- **Real-time monitoring** via Server-Sent Events (SSE)
|
|
44
|
+
- **Comprehensive pagination** and filtering across all endpoints
|
|
45
|
+
- **Task queue management** with retry policies and timeouts
|
|
46
|
+
|
|
47
|
+
### API Structure
|
|
48
|
+
- **Workflows**: Manage workflow lifecycle and state
|
|
49
|
+
- **Tasks**: Monitor task execution and queue status
|
|
50
|
+
- **Events**: Audit trail with real-time streaming
|
|
51
|
+
- **Logs**: Application logging with structured output
|
|
52
|
+
- **Statistics**: System metrics and performance data
|
|
53
|
+
|
|
54
|
+
### Authentication
|
|
55
|
+
Currently no authentication required (development mode).
|
|
56
|
+
Production deployments should implement appropriate security measures.
|
|
57
|
+
|
|
58
|
+
### Real-time Updates
|
|
59
|
+
Use Server-Sent Events endpoints (`/stream/*`) for real-time monitoring.
|
|
60
|
+
These endpoints return `text/event-stream` with JSON payloads.
|
|
61
|
+
|
|
62
|
+
### Rate Limiting
|
|
63
|
+
No rate limiting currently implemented.
|
|
64
|
+
Consider implementing in production environments.
|
|
65
|
+
""",
|
|
66
|
+
version="0.2.0",
|
|
67
|
+
docs_url="/docs",
|
|
68
|
+
redoc_url="/redoc",
|
|
69
|
+
lifespan=lifespan,
|
|
70
|
+
contact={
|
|
71
|
+
"name": "Loom Development Team",
|
|
72
|
+
"url": "https://github.com/yourusername/loom",
|
|
73
|
+
},
|
|
74
|
+
license_info={
|
|
75
|
+
"name": "MIT",
|
|
76
|
+
"url": "https://opensource.org/licenses/MIT",
|
|
77
|
+
},
|
|
78
|
+
servers=[
|
|
79
|
+
{"url": "http://localhost:8000", "description": "Development server"},
|
|
80
|
+
{
|
|
81
|
+
"url": "https://your-production-domain.com",
|
|
82
|
+
"description": "Production server",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
openapi_tags=[
|
|
86
|
+
{
|
|
87
|
+
"name": "Workflows",
|
|
88
|
+
"description": "Manage workflow definitions, execution state, and lifecycle operations.",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "Tasks",
|
|
92
|
+
"description": "Monitor task execution, queue status, and retry policies.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "Events",
|
|
96
|
+
"description": "Event sourcing audit trail with real-time streaming capabilities.",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "Logs",
|
|
100
|
+
"description": "Application logging with structured output and filtering.",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "Statistics",
|
|
104
|
+
"description": "System metrics, performance data, and analytics.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "Health",
|
|
108
|
+
"description": "System health checks and status monitoring.",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# CORS middleware for development
|
|
114
|
+
app.add_middleware(
|
|
115
|
+
CORSMiddleware,
|
|
116
|
+
allow_origins=[
|
|
117
|
+
"*",
|
|
118
|
+
], # React dev server
|
|
119
|
+
allow_credentials=True,
|
|
120
|
+
allow_methods=["*"],
|
|
121
|
+
allow_headers=["*"],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Import and register API routers
|
|
125
|
+
|
|
126
|
+
app.include_router(workflows.router, prefix="/api/workflows", tags=["Workflows"])
|
|
127
|
+
app.include_router(tasks.router, prefix="/api/tasks", tags=["Tasks"])
|
|
128
|
+
app.include_router(events.router, prefix="/api/events", tags=["Events"])
|
|
129
|
+
app.include_router(logs.router, prefix="/api/logs", tags=["Logs"])
|
|
130
|
+
app.include_router(stats.router, prefix="/api/stats", tags=["Statistics"])
|
|
131
|
+
app.include_router(graphs.router, prefix="/api/graphs", tags=["Graphs"])
|
|
132
|
+
|
|
133
|
+
# Mount static files for React UI (must be after API routes)
|
|
134
|
+
dist_dir = Path(__file__).parent / "dist"
|
|
135
|
+
if dist_dir.exists():
|
|
136
|
+
app.mount("/assets", StaticFiles(directory=str(dist_dir / "assets")), name="assets")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Health check endpoints
|
|
140
|
+
@app.get(
|
|
141
|
+
"/health",
|
|
142
|
+
tags=["Health"],
|
|
143
|
+
summary="Health Check",
|
|
144
|
+
description="Simple health check endpoint to verify API availability.",
|
|
145
|
+
responses={
|
|
146
|
+
200: {
|
|
147
|
+
"description": "API is healthy",
|
|
148
|
+
"content": {
|
|
149
|
+
"application/json": {
|
|
150
|
+
"example": {
|
|
151
|
+
"status": "healthy",
|
|
152
|
+
"timestamp": "2026-01-29T10:30:00Z",
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
async def health_check():
|
|
160
|
+
"""Basic health check endpoint"""
|
|
161
|
+
from datetime import datetime, timezone
|
|
162
|
+
|
|
163
|
+
return {"status": "healthy", "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.get(
|
|
167
|
+
"/health/detailed",
|
|
168
|
+
tags=["Health"],
|
|
169
|
+
summary="Detailed Health Check",
|
|
170
|
+
description="""
|
|
171
|
+
Comprehensive health check including database connectivity and system status.
|
|
172
|
+
|
|
173
|
+
**Checks performed:**
|
|
174
|
+
- Database connection and schema validation
|
|
175
|
+
- Memory usage and performance metrics
|
|
176
|
+
- Active workflow and task counts
|
|
177
|
+
""",
|
|
178
|
+
responses={
|
|
179
|
+
200: {
|
|
180
|
+
"description": "Detailed system health information",
|
|
181
|
+
"content": {
|
|
182
|
+
"application/json": {
|
|
183
|
+
"example": {
|
|
184
|
+
"status": "healthy",
|
|
185
|
+
"timestamp": "2026-01-29T10:30:00Z",
|
|
186
|
+
"database": {"status": "connected", "response_time_ms": 12},
|
|
187
|
+
"workflows": {"total": 150, "active": 23},
|
|
188
|
+
"tasks": {"pending": 5, "running": 2},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
503: {
|
|
194
|
+
"description": "System unhealthy",
|
|
195
|
+
"content": {
|
|
196
|
+
"application/json": {
|
|
197
|
+
"example": {
|
|
198
|
+
"status": "unhealthy",
|
|
199
|
+
"timestamp": "2026-01-29T10:30:00Z",
|
|
200
|
+
"errors": ["Database connection failed"],
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
async def detailed_health_check():
|
|
208
|
+
"""Detailed health check with database connectivity"""
|
|
209
|
+
import time
|
|
210
|
+
from datetime import datetime, timezone
|
|
211
|
+
|
|
212
|
+
health_status = {
|
|
213
|
+
"status": "healthy",
|
|
214
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
215
|
+
"errors": [],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
# Check database connectivity
|
|
220
|
+
start_time = time.time()
|
|
221
|
+
async with Database[Any, Any]() as db:
|
|
222
|
+
# Simple query to test database
|
|
223
|
+
result = await db.fetchone("SELECT COUNT(*) as count FROM workflows")
|
|
224
|
+
workflow_count = result["count"] if result else 0
|
|
225
|
+
|
|
226
|
+
# Get task counts
|
|
227
|
+
pending_tasks = await db.fetchone(
|
|
228
|
+
"SELECT COUNT(*) as count FROM tasks WHERE status = 'PENDING'"
|
|
229
|
+
)
|
|
230
|
+
running_tasks = await db.fetchone(
|
|
231
|
+
"SELECT COUNT(*) as count FROM tasks WHERE status = 'RUNNING'"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
response_time = (time.time() - start_time) * 1000
|
|
235
|
+
|
|
236
|
+
health_status.update(
|
|
237
|
+
{
|
|
238
|
+
"database": { # type: ignore
|
|
239
|
+
"status": "connected",
|
|
240
|
+
"response_time_ms": round(response_time, 2),
|
|
241
|
+
},
|
|
242
|
+
"workflows": { # type: ignore
|
|
243
|
+
"total": workflow_count,
|
|
244
|
+
"active": await _get_active_workflow_count(db),
|
|
245
|
+
},
|
|
246
|
+
"tasks": { # type: ignore
|
|
247
|
+
"pending": pending_tasks["count"] if pending_tasks else 0,
|
|
248
|
+
"running": running_tasks["count"] if running_tasks else 0,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
health_status["status"] = "unhealthy"
|
|
255
|
+
health_status["errors"].append(f"Database connection failed: {str(e)}") # type: ignore
|
|
256
|
+
|
|
257
|
+
status_code = 200 if health_status["status"] == "healthy" else 503
|
|
258
|
+
from fastapi import Response
|
|
259
|
+
|
|
260
|
+
return Response(
|
|
261
|
+
content=json.dumps(health_status),
|
|
262
|
+
status_code=status_code,
|
|
263
|
+
media_type="application/json",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def _get_active_workflow_count(db: Database[Any, Any]) -> int:
|
|
268
|
+
"""Get count of active workflows (RUNNING or PENDING)"""
|
|
269
|
+
result = await db.fetchone(
|
|
270
|
+
"SELECT COUNT(*) as count FROM workflows WHERE status IN ('RUNNING', 'PENDING')"
|
|
271
|
+
)
|
|
272
|
+
return result["count"] if result else 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.get("/", include_in_schema=False)
|
|
276
|
+
async def root(request: Request):
|
|
277
|
+
"""Serve React UI with API URL configuration"""
|
|
278
|
+
dist_dir = Path(__file__).parent / "dist"
|
|
279
|
+
index_file = dist_dir / "index.html"
|
|
280
|
+
|
|
281
|
+
# Check if React build exists
|
|
282
|
+
if not index_file.exists():
|
|
283
|
+
return {"message": "Loom Dashboard API", "docs": "/docs", "note": "React UI not built"}
|
|
284
|
+
|
|
285
|
+
# Read index.html
|
|
286
|
+
with open(index_file, "r", encoding="utf-8") as f:
|
|
287
|
+
html_content = f.read()
|
|
288
|
+
|
|
289
|
+
# Inject API URL configuration
|
|
290
|
+
api_url = str(request.base_url).rstrip('/')
|
|
291
|
+
config_script = f"""
|
|
292
|
+
<script>
|
|
293
|
+
window.__API_URL__ = "{api_url}";
|
|
294
|
+
</script>
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
# Insert before </head> tag
|
|
298
|
+
html_content = html_content.replace("</head>", f"{config_script}</head>")
|
|
299
|
+
|
|
300
|
+
return HTMLResponse(content=html_content)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@app.get("/health")
|
|
304
|
+
async def health():
|
|
305
|
+
"""Health check endpoint"""
|
|
306
|
+
return {"status": "healthy", "service": "loom-dashboard"}
|