ArchEnhancer / backend /services /progress_tracker.py
Aguilar Elizondo
Initial commit: Architecture AI Enhancer v1.0.0
6bfa765
Raw
History Blame Contribute Delete
4.82 kB
"""
Progress Tracking System
Manages progress updates for long-running enhancement tasks
Supports Server-Sent Events (SSE) for real-time frontend updates
"""
import asyncio
import logging
from typing import Dict, Optional
from datetime import datetime
logger = logging.getLogger(__name__)
class ProgressTracker:
"""
Tracks progress for enhancement operations
Allows multiple clients to subscribe to progress updates
"""
def __init__(self):
self.tasks: Dict[str, Dict] = {}
self.subscribers: Dict[str, list] = {}
def create_task(self, task_id: str, total_steps: int = 100):
"""Initialize a new task for progress tracking"""
self.tasks[task_id] = {
"id": task_id,
"status": "initializing",
"progress": 0,
"total_steps": total_steps,
"current_step": 0,
"message": "Starting enhancement...",
"started_at": datetime.now().isoformat(),
"error": None
}
self.subscribers[task_id] = []
logger.info(f"Created progress tracker for task {task_id}")
def update_progress(
self,
task_id: str,
current_step: int,
message: str,
status: str = "processing"
):
"""Update progress for a task (synchronous)"""
if task_id not in self.tasks:
logger.warning(f"Task {task_id} not found for progress update")
return
task = self.tasks[task_id]
task["current_step"] = current_step
task["progress"] = int((current_step / task["total_steps"]) * 100)
task["message"] = message
task["status"] = status
logger.info(f"Task {task_id}: {task['progress']}% - {message}")
def complete_task(self, task_id: str, result_url: Optional[str] = None):
"""Mark a task as completed"""
if task_id not in self.tasks:
return
self.tasks[task_id].update({
"status": "completed",
"progress": 100,
"message": "Enhancement completed!",
"result_url": result_url,
"completed_at": datetime.now().isoformat()
})
# Notify subscribers if event loop is running
try:
asyncio.create_task(self._notify_subscribers(task_id))
except RuntimeError:
# No event loop running (called from thread)
pass
logger.info(f"Task {task_id} completed")
def fail_task(self, task_id: str, error: str):
"""Mark a task as failed"""
if task_id not in self.tasks:
return
self.tasks[task_id].update({
"status": "failed",
"message": f"Error: {error}",
"error": error,
"failed_at": datetime.now().isoformat()
})
# Notify subscribers if event loop is running
try:
asyncio.create_task(self._notify_subscribers(task_id))
except RuntimeError:
# No event loop running (called from thread)
pass
logger.error(f"Task {task_id} failed: {error}")
def get_task_status(self, task_id: str) -> Optional[Dict]:
"""Get current status of a task"""
return self.tasks.get(task_id)
def subscribe(self, task_id: str, queue: asyncio.Queue):
"""Subscribe to progress updates for a task"""
if task_id not in self.subscribers:
self.subscribers[task_id] = []
self.subscribers[task_id].append(queue)
logger.debug(f"New subscriber for task {task_id}")
def unsubscribe(self, task_id: str, queue: asyncio.Queue):
"""Unsubscribe from progress updates"""
if task_id in self.subscribers and queue in self.subscribers[task_id]:
self.subscribers[task_id].remove(queue)
async def _notify_subscribers(self, task_id: str):
"""Send current task status to all subscribers"""
if task_id not in self.subscribers:
return
task_data = self.tasks.get(task_id)
if not task_data:
return
# Send to all subscribers
for queue in self.subscribers[task_id]:
try:
await queue.put(task_data.copy())
except Exception as e:
logger.error(f"Error notifying subscriber: {e}")
def cleanup_task(self, task_id: str):
"""Remove task data after completion"""
if task_id in self.tasks:
del self.tasks[task_id]
if task_id in self.subscribers:
del self.subscribers[task_id]
logger.debug(f"Cleaned up task {task_id}")
# Global progress tracker instance
progress_tracker = ProgressTracker()