198 lines
5.6 KiB
Python
198 lines
5.6 KiB
Python
#!/opt/homebrew/bin/python3.11
|
|
"""
|
|
Redis Progress Publisher
|
|
Common module for publishing progress to Redis
|
|
|
|
Usage:
|
|
from redis_publisher import RedisPublisher
|
|
|
|
pub = RedisPublisher("video-uuid-123")
|
|
pub.info("asr", "Starting ASR processing")
|
|
pub.progress("asr", current=50, total=100, message="Processing segment")
|
|
pub.complete("asr", "Transcription complete")
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import time
|
|
import redis
|
|
from typing import Optional, Any, Dict
|
|
from dataclasses import dataclass, asdict
|
|
from enum import Enum
|
|
|
|
|
|
class MessageType(Enum):
|
|
INFO = "info"
|
|
PROGRESS = "progress"
|
|
COMPLETE = "complete"
|
|
ERROR = "error"
|
|
WARNING = "warning"
|
|
|
|
|
|
@dataclass
|
|
class ProgressData:
|
|
message: Optional[str] = None
|
|
current: Optional[int] = None
|
|
total: Optional[int] = None
|
|
output_count: Optional[int] = None
|
|
output_type: Optional[str] = None
|
|
extra: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
@dataclass
|
|
class StructuredMessage:
|
|
type: str
|
|
processor: str
|
|
uuid: str
|
|
timestamp: int
|
|
data: ProgressData
|
|
|
|
|
|
class RedisPublisher:
|
|
def __init__(self, uuid: str):
|
|
self.uuid = uuid
|
|
prefix = os.environ.get("MOMENTRY_REDIS_PREFIX", "momentry:")
|
|
self.channel = f"{prefix}progress:{uuid}"
|
|
self._enabled = False
|
|
self._client = None
|
|
self._connect()
|
|
|
|
def _connect(self) -> None:
|
|
redis_url = os.environ.get("REDIS_URL")
|
|
if not redis_url:
|
|
password = os.environ.get("REDIS_PASSWORD", "accusys")
|
|
redis_url = f"redis://:{password}@localhost:6379"
|
|
|
|
try:
|
|
self._client = redis.from_url(redis_url, decode_responses=True)
|
|
self._client.ping()
|
|
self._enabled = True
|
|
except redis.ConnectionError as e:
|
|
import sys
|
|
|
|
print(f"[RedisPublisher] Connection failed: {e}", file=sys.stderr)
|
|
except Exception as e:
|
|
import sys
|
|
|
|
print(f"[RedisPublisher] Redis not available: {e}", file=sys.stderr)
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return self._enabled
|
|
|
|
def _publish_json(self, msg: StructuredMessage) -> bool:
|
|
if not self._enabled or self._client is None:
|
|
return False
|
|
|
|
try:
|
|
client: redis.Redis = self._client
|
|
json_data = json.dumps(asdict(msg))
|
|
client.publish(self.channel, json_data)
|
|
client.hset(self.channel, msg.processor, json_data)
|
|
return True
|
|
except Exception as e:
|
|
import sys
|
|
|
|
print(f"[RedisPublisher] Publish error: {e}", file=sys.stderr)
|
|
return False
|
|
|
|
try:
|
|
self._client.publish(self.channel, json.dumps(asdict(msg)))
|
|
return True
|
|
except Exception as e:
|
|
import sys
|
|
|
|
print(f"[RedisPublisher] Publish error: {e}", file=sys.stderr)
|
|
return False
|
|
|
|
def publish(
|
|
self,
|
|
msg_type: MessageType,
|
|
processor: str,
|
|
message: Optional[str] = None,
|
|
current: Optional[int] = None,
|
|
total: Optional[int] = None,
|
|
output_count: Optional[int] = None,
|
|
output_type: Optional[str] = None,
|
|
extra: Optional[Dict[str, Any]] = None,
|
|
) -> bool:
|
|
if not self._enabled:
|
|
return False
|
|
|
|
msg = StructuredMessage(
|
|
type=msg_type.value,
|
|
processor=processor,
|
|
uuid=self.uuid,
|
|
timestamp=int(time.time()),
|
|
data=ProgressData(
|
|
message=message,
|
|
current=current,
|
|
total=total,
|
|
output_count=output_count,
|
|
output_type=output_type,
|
|
extra=extra,
|
|
),
|
|
)
|
|
|
|
return self._publish_json(msg)
|
|
|
|
def info(self, processor: str, message: str) -> bool:
|
|
return self.publish(MessageType.INFO, processor, message=message)
|
|
|
|
def progress(
|
|
self,
|
|
processor: str,
|
|
current: int,
|
|
total: int,
|
|
message: str = "",
|
|
output_count: Optional[int] = None,
|
|
output_type: Optional[str] = None,
|
|
) -> bool:
|
|
return self.publish(
|
|
MessageType.PROGRESS,
|
|
processor,
|
|
message=message,
|
|
current=current,
|
|
total=total,
|
|
output_count=output_count,
|
|
output_type=output_type,
|
|
)
|
|
|
|
def complete(self, processor: str, message: str = "") -> bool:
|
|
return self.publish(MessageType.COMPLETE, processor, message=message)
|
|
|
|
def error(self, processor: str, message: str) -> bool:
|
|
return self.publish(MessageType.ERROR, processor, message=message)
|
|
|
|
def warning(self, processor: str, message: str) -> bool:
|
|
return self.publish(MessageType.WARNING, processor, message=message)
|
|
|
|
def percentage(self, processor: str, percent: float, message: str = "") -> bool:
|
|
return self.publish(
|
|
MessageType.PROGRESS,
|
|
processor,
|
|
message=message,
|
|
current=int(percent),
|
|
total=100,
|
|
extra={"percentage": percent},
|
|
)
|
|
|
|
|
|
class ProgressContext:
|
|
"""Context manager for tracking processor progress"""
|
|
|
|
def __init__(self, publisher: RedisPublisher, processor: str):
|
|
self.publisher = publisher
|
|
self.processor = processor
|
|
|
|
def __enter__(self):
|
|
self.publisher.info(self.processor, f"{self.processor.upper()} started")
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if exc_type is not None:
|
|
self.publisher.error(self.processor, str(exc_val))
|
|
else:
|
|
self.publisher.complete(self.processor)
|
|
return False
|