mirror of
https://github.com/pacnpal/Pac-cogs.git
synced 2025-12-20 10:51:05 -05:00
fixed
This commit is contained in:
@@ -4,31 +4,68 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, TypedDict, ClassVar, List, Set, Union
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import discord
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.commands import GroupCog
|
||||
from redbot.core.commands import GroupCog, Context
|
||||
|
||||
from .settings import Settings
|
||||
from .lifecycle import LifecycleManager
|
||||
from .lifecycle import LifecycleManager, LifecycleState
|
||||
from .component_manager import ComponentManager, ComponentState
|
||||
from .error_handler import error_manager, handle_command_error
|
||||
from .response_handler import response_manager
|
||||
from .commands import setup_archiver_commands, setup_database_commands, setup_settings_commands
|
||||
from .events import setup_events
|
||||
from .commands.archiver_commands import setup_archiver_commands
|
||||
from .commands.database_commands import setup_database_commands
|
||||
from .commands.settings_commands import setup_settings_commands
|
||||
from .events import setup_events, EventManager
|
||||
|
||||
from ..processor.core import Processor
|
||||
from ..queue.manager import QueueManager
|
||||
from ..ffmpeg.ffmpeg_manager import FFmpegManager
|
||||
from ..database.video_archive_db import VideoArchiveDB
|
||||
from ..config_manager import ConfigManager
|
||||
from ..utils.exceptions import (
|
||||
CogError,
|
||||
ErrorContext,
|
||||
ErrorSeverity
|
||||
)
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
class CogStatus:
|
||||
class CogHealthCheck(TypedDict):
|
||||
"""Type definition for health check status"""
|
||||
name: str
|
||||
status: bool
|
||||
last_check: str
|
||||
details: Optional[Dict[str, Any]]
|
||||
|
||||
class CogStatus(TypedDict):
|
||||
"""Type definition for cog status"""
|
||||
uptime: float
|
||||
last_error: Optional[str]
|
||||
error_count: int
|
||||
command_count: int
|
||||
last_command: Optional[str]
|
||||
health_checks: Dict[str, CogHealthCheck]
|
||||
state: str
|
||||
ready: bool
|
||||
|
||||
class StatusTracker:
|
||||
"""Tracks cog status and health"""
|
||||
|
||||
def __init__(self):
|
||||
HEALTH_CHECK_INTERVAL: ClassVar[int] = 30 # Seconds between health checks
|
||||
ERROR_THRESHOLD: ClassVar[int] = 100 # Maximum errors before health warning
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.start_time = datetime.utcnow()
|
||||
self.last_error: Optional[str] = None
|
||||
self.error_count = 0
|
||||
self.command_count = 0
|
||||
self.last_command_time: Optional[datetime] = None
|
||||
self.health_checks: Dict[str, bool] = {}
|
||||
self.health_checks: Dict[str, CogHealthCheck] = {}
|
||||
|
||||
def record_error(self, error: str) -> None:
|
||||
"""Record an error occurrence"""
|
||||
@@ -40,36 +77,70 @@ class CogStatus:
|
||||
self.command_count += 1
|
||||
self.last_command_time = datetime.utcnow()
|
||||
|
||||
def update_health_check(self, check: str, status: bool) -> None:
|
||||
def update_health_check(
|
||||
self,
|
||||
name: str,
|
||||
status: bool,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""Update health check status"""
|
||||
self.health_checks[check] = status
|
||||
self.health_checks[name] = CogHealthCheck(
|
||||
name=name,
|
||||
status=status,
|
||||
last_check=datetime.utcnow().isoformat(),
|
||||
details=details
|
||||
)
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
def get_status(self) -> CogStatus:
|
||||
"""Get current status"""
|
||||
return {
|
||||
"uptime": (datetime.utcnow() - self.start_time).total_seconds(),
|
||||
"last_error": self.last_error,
|
||||
"error_count": self.error_count,
|
||||
"command_count": self.command_count,
|
||||
"last_command": self.last_command_time.isoformat() if self.last_command_time else None,
|
||||
"health_checks": self.health_checks.copy()
|
||||
}
|
||||
return CogStatus(
|
||||
uptime=(datetime.utcnow() - self.start_time).total_seconds(),
|
||||
last_error=self.last_error,
|
||||
error_count=self.error_count,
|
||||
command_count=self.command_count,
|
||||
last_command=self.last_command_time.isoformat() if self.last_command_time else None,
|
||||
health_checks=self.health_checks.copy(),
|
||||
state="healthy" if self.is_healthy() else "unhealthy",
|
||||
ready=True
|
||||
)
|
||||
|
||||
def is_healthy(self) -> bool:
|
||||
"""Check if cog is healthy"""
|
||||
if self.error_count > self.ERROR_THRESHOLD:
|
||||
return False
|
||||
return all(check["status"] for check in self.health_checks.values())
|
||||
|
||||
class ComponentAccessor:
|
||||
"""Provides safe access to components"""
|
||||
|
||||
def __init__(self, component_manager: ComponentManager):
|
||||
def __init__(self, component_manager: ComponentManager) -> None:
|
||||
self._component_manager = component_manager
|
||||
|
||||
def get_component(self, name: str) -> Optional[Any]:
|
||||
"""Get a component with state validation"""
|
||||
"""
|
||||
Get a component with state validation.
|
||||
|
||||
Args:
|
||||
name: Component name
|
||||
|
||||
Returns:
|
||||
Component instance if ready, None otherwise
|
||||
"""
|
||||
component = self._component_manager.get(name)
|
||||
if component and component.state == ComponentState.READY:
|
||||
return component
|
||||
return None
|
||||
|
||||
def get_component_status(self, name: str) -> Dict[str, Any]:
|
||||
"""Get component status"""
|
||||
"""
|
||||
Get component status.
|
||||
|
||||
Args:
|
||||
name: Component name
|
||||
|
||||
Returns:
|
||||
Component status dictionary
|
||||
"""
|
||||
return self._component_manager.get_component_status().get(name, {})
|
||||
|
||||
class VideoArchiver(GroupCog, Settings):
|
||||
@@ -85,7 +156,19 @@ class VideoArchiver(GroupCog, Settings):
|
||||
self.lifecycle_manager = LifecycleManager(self)
|
||||
self.component_manager = ComponentManager(self)
|
||||
self.component_accessor = ComponentAccessor(self.component_manager)
|
||||
self.status = CogStatus()
|
||||
self.status_tracker = StatusTracker()
|
||||
self.event_manager: Optional[EventManager] = None
|
||||
|
||||
# Initialize task trackers
|
||||
self._init_task: Optional[asyncio.Task] = None
|
||||
self._cleanup_task: Optional[asyncio.Task] = None
|
||||
self._queue_task: Optional[asyncio.Task] = None
|
||||
self._health_tasks: Set[asyncio.Task] = set()
|
||||
|
||||
# Initialize component storage
|
||||
self.components: Dict[int, Dict[str, Any]] = {}
|
||||
self.update_checker = None
|
||||
self._db = None
|
||||
|
||||
# Set up commands
|
||||
setup_archiver_commands(self)
|
||||
@@ -93,42 +176,85 @@ class VideoArchiver(GroupCog, Settings):
|
||||
setup_settings_commands(self)
|
||||
|
||||
# Set up events
|
||||
setup_events(self)
|
||||
self.event_manager = setup_events(self)
|
||||
|
||||
# Register cleanup handlers
|
||||
self.lifecycle_manager.register_cleanup_handler(self._cleanup_handler)
|
||||
|
||||
async def cog_load(self) -> None:
|
||||
"""Handle cog loading"""
|
||||
"""
|
||||
Handle cog loading.
|
||||
|
||||
Raises:
|
||||
CogError: If loading fails
|
||||
"""
|
||||
try:
|
||||
await self.lifecycle_manager.handle_load()
|
||||
await self._start_health_monitoring()
|
||||
except Exception as e:
|
||||
self.status.record_error(str(e))
|
||||
raise
|
||||
error = f"Failed to load cog: {str(e)}"
|
||||
self.status_tracker.record_error(error)
|
||||
logger.error(error, exc_info=True)
|
||||
raise CogError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"VideoArchiver",
|
||||
"cog_load",
|
||||
None,
|
||||
ErrorSeverity.CRITICAL
|
||||
)
|
||||
)
|
||||
|
||||
async def cog_unload(self) -> None:
|
||||
"""Handle cog unloading"""
|
||||
"""
|
||||
Handle cog unloading.
|
||||
|
||||
Raises:
|
||||
CogError: If unloading fails
|
||||
"""
|
||||
try:
|
||||
# Cancel health monitoring
|
||||
for task in self._health_tasks:
|
||||
task.cancel()
|
||||
self._health_tasks.clear()
|
||||
|
||||
await self.lifecycle_manager.handle_unload()
|
||||
except Exception as e:
|
||||
self.status.record_error(str(e))
|
||||
raise
|
||||
error = f"Failed to unload cog: {str(e)}"
|
||||
self.status_tracker.record_error(error)
|
||||
logger.error(error, exc_info=True)
|
||||
raise CogError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"VideoArchiver",
|
||||
"cog_unload",
|
||||
None,
|
||||
ErrorSeverity.CRITICAL
|
||||
)
|
||||
)
|
||||
|
||||
async def cog_command_error(self, ctx, error):
|
||||
async def cog_command_error(
|
||||
self,
|
||||
ctx: Context,
|
||||
error: Exception
|
||||
) -> None:
|
||||
"""Handle command errors"""
|
||||
self.status.record_error(str(error))
|
||||
self.status_tracker.record_error(str(error))
|
||||
await handle_command_error(ctx, error)
|
||||
|
||||
async def cog_before_invoke(self, ctx) -> bool:
|
||||
async def cog_before_invoke(self, ctx: Context) -> bool:
|
||||
"""Pre-command hook"""
|
||||
self.status.record_command()
|
||||
self.status_tracker.record_command()
|
||||
return True
|
||||
|
||||
async def _start_health_monitoring(self) -> None:
|
||||
"""Start health monitoring tasks"""
|
||||
asyncio.create_task(self._monitor_component_health())
|
||||
asyncio.create_task(self._monitor_system_health())
|
||||
self._health_tasks.add(
|
||||
asyncio.create_task(self._monitor_component_health())
|
||||
)
|
||||
self._health_tasks.add(
|
||||
asyncio.create_task(self._monitor_system_health())
|
||||
)
|
||||
|
||||
async def _monitor_component_health(self) -> None:
|
||||
"""Monitor component health"""
|
||||
@@ -136,98 +262,134 @@ class VideoArchiver(GroupCog, Settings):
|
||||
try:
|
||||
component_status = self.component_manager.get_component_status()
|
||||
for name, status in component_status.items():
|
||||
self.status.update_health_check(
|
||||
self.status_tracker.update_health_check(
|
||||
f"component_{name}",
|
||||
status["state"] == ComponentState.READY.value
|
||||
status["state"] == ComponentState.READY.name,
|
||||
status
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error monitoring component health: {e}")
|
||||
await asyncio.sleep(60) # Check every minute
|
||||
logger.error(f"Error monitoring component health: {e}", exc_info=True)
|
||||
await asyncio.sleep(self.status_tracker.HEALTH_CHECK_INTERVAL)
|
||||
|
||||
async def _monitor_system_health(self) -> None:
|
||||
"""Monitor system health metrics"""
|
||||
while True:
|
||||
try:
|
||||
# Check queue health
|
||||
queue_manager = self.queue_manager
|
||||
if queue_manager:
|
||||
if queue_manager := self.queue_manager:
|
||||
queue_status = await queue_manager.get_queue_status()
|
||||
self.status.update_health_check(
|
||||
self.status_tracker.update_health_check(
|
||||
"queue_health",
|
||||
queue_status["active"] and not queue_status["stalled"]
|
||||
queue_status["active"] and not queue_status["stalled"],
|
||||
queue_status
|
||||
)
|
||||
|
||||
# Check processor health
|
||||
processor = self.processor
|
||||
if processor:
|
||||
if processor := self.processor:
|
||||
processor_status = await processor.get_status()
|
||||
self.status.update_health_check(
|
||||
self.status_tracker.update_health_check(
|
||||
"processor_health",
|
||||
processor_status["active"]
|
||||
processor_status["active"],
|
||||
processor_status
|
||||
)
|
||||
|
||||
# Check database health
|
||||
db = self.db
|
||||
if db:
|
||||
if db := self.db:
|
||||
db_status = await db.get_status()
|
||||
self.status.update_health_check(
|
||||
self.status_tracker.update_health_check(
|
||||
"database_health",
|
||||
db_status["connected"]
|
||||
db_status["connected"],
|
||||
db_status
|
||||
)
|
||||
|
||||
# Check event system health
|
||||
if self.event_manager:
|
||||
event_stats = self.event_manager.get_stats()
|
||||
self.status_tracker.update_health_check(
|
||||
"event_health",
|
||||
event_stats["health"],
|
||||
event_stats
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error monitoring system health: {e}")
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
logger.error(f"Error monitoring system health: {e}", exc_info=True)
|
||||
await asyncio.sleep(self.status_tracker.HEALTH_CHECK_INTERVAL)
|
||||
|
||||
async def _cleanup_handler(self) -> None:
|
||||
"""Custom cleanup handler"""
|
||||
try:
|
||||
# Perform any custom cleanup
|
||||
pass
|
||||
# Cancel health monitoring tasks
|
||||
for task in self._health_tasks:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._health_tasks.clear()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in cleanup handler: {e}")
|
||||
logger.error(f"Error in cleanup handler: {e}", exc_info=True)
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive cog status"""
|
||||
"""
|
||||
Get comprehensive cog status.
|
||||
|
||||
Returns:
|
||||
Dictionary containing cog status information
|
||||
"""
|
||||
return {
|
||||
"cog": self.status.get_status(),
|
||||
"cog": self.status_tracker.get_status(),
|
||||
"lifecycle": self.lifecycle_manager.get_status(),
|
||||
"components": self.component_manager.get_component_status(),
|
||||
"errors": error_manager.tracker.get_error_stats()
|
||||
"errors": error_manager.tracker.get_error_stats(),
|
||||
"events": self.event_manager.get_stats() if self.event_manager else None
|
||||
}
|
||||
|
||||
# Component property accessors
|
||||
@property
|
||||
def processor(self):
|
||||
def processor(self) -> Optional[Processor]:
|
||||
"""Get the processor component"""
|
||||
return self.component_accessor.get_component("processor")
|
||||
|
||||
@property
|
||||
def queue_manager(self):
|
||||
def queue_manager(self) -> Optional[QueueManager]:
|
||||
"""Get the queue manager component"""
|
||||
return self.component_accessor.get_component("queue_manager")
|
||||
|
||||
@property
|
||||
def config_manager(self):
|
||||
def config_manager(self) -> Optional[ConfigManager]:
|
||||
"""Get the config manager component"""
|
||||
return self.component_accessor.get_component("config_manager")
|
||||
|
||||
@property
|
||||
def ffmpeg_mgr(self):
|
||||
def ffmpeg_mgr(self) -> Optional[FFmpegManager]:
|
||||
"""Get the FFmpeg manager component"""
|
||||
return self.component_accessor.get_component("ffmpeg_mgr")
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
def db(self) -> Optional[VideoArchiveDB]:
|
||||
"""Get the database component"""
|
||||
return self.component_accessor.get_component("db")
|
||||
return self._db
|
||||
|
||||
@db.setter
|
||||
def db(self, value: VideoArchiveDB) -> None:
|
||||
"""Set the database component"""
|
||||
self._db = value
|
||||
|
||||
@property
|
||||
def data_path(self):
|
||||
def data_path(self) -> Optional[Path]:
|
||||
"""Get the data path"""
|
||||
return self.component_accessor.get_component("data_path")
|
||||
|
||||
@property
|
||||
def download_path(self):
|
||||
def download_path(self) -> Optional[Path]:
|
||||
"""Get the download path"""
|
||||
return self.component_accessor.get_component("download_path")
|
||||
|
||||
@property
|
||||
def queue_handler(self):
|
||||
"""Get the queue handler from processor"""
|
||||
if processor := self.processor:
|
||||
return processor.queue_handler
|
||||
return None
|
||||
|
||||
@@ -2,33 +2,61 @@
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional, Set, List
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, Optional, Set, List, TypedDict, ClassVar, Type, Union, Protocol
|
||||
from enum import Enum, auto
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from ..utils.exceptions import (
|
||||
ComponentError,
|
||||
ErrorContext,
|
||||
ErrorSeverity
|
||||
)
|
||||
from ..utils.path_manager import ensure_directory
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
class ComponentState(Enum):
|
||||
"""Possible states of a component"""
|
||||
UNREGISTERED = "unregistered"
|
||||
REGISTERED = "registered"
|
||||
INITIALIZING = "initializing"
|
||||
READY = "ready"
|
||||
ERROR = "error"
|
||||
SHUTDOWN = "shutdown"
|
||||
UNREGISTERED = auto()
|
||||
REGISTERED = auto()
|
||||
INITIALIZING = auto()
|
||||
READY = auto()
|
||||
ERROR = auto()
|
||||
SHUTDOWN = auto()
|
||||
|
||||
class ComponentDependencyError(Exception):
|
||||
"""Raised when component dependencies cannot be satisfied"""
|
||||
pass
|
||||
class ComponentHistory(TypedDict):
|
||||
"""Type definition for component history entry"""
|
||||
component: str
|
||||
state: str
|
||||
timestamp: str
|
||||
error: Optional[str]
|
||||
duration: float
|
||||
|
||||
class ComponentLifecycleError(Exception):
|
||||
"""Raised when component lifecycle operations fail"""
|
||||
pass
|
||||
class ComponentStatus(TypedDict):
|
||||
"""Type definition for component status"""
|
||||
state: str
|
||||
registration_time: Optional[str]
|
||||
initialization_time: Optional[str]
|
||||
dependencies: Set[str]
|
||||
dependents: Set[str]
|
||||
error: Optional[str]
|
||||
health: bool
|
||||
|
||||
class Initializable(Protocol):
|
||||
"""Protocol for initializable components"""
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the component"""
|
||||
...
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the component"""
|
||||
...
|
||||
|
||||
class Component:
|
||||
"""Base class for managed components"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.state = ComponentState.UNREGISTERED
|
||||
self.dependencies: Set[str] = set()
|
||||
@@ -36,33 +64,74 @@ class Component:
|
||||
self.registration_time: Optional[datetime] = None
|
||||
self.initialization_time: Optional[datetime] = None
|
||||
self.error: Optional[str] = None
|
||||
self._health_check_task: Optional[asyncio.Task] = None
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the component"""
|
||||
"""
|
||||
Initialize the component.
|
||||
|
||||
Raises:
|
||||
ComponentError: If initialization fails
|
||||
"""
|
||||
pass
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the component"""
|
||||
pass
|
||||
"""
|
||||
Shutdown the component.
|
||||
|
||||
Raises:
|
||||
ComponentError: If shutdown fails
|
||||
"""
|
||||
if self._health_check_task:
|
||||
self._health_check_task.cancel()
|
||||
try:
|
||||
await self._health_check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
def is_healthy(self) -> bool:
|
||||
"""Check if component is healthy"""
|
||||
return self.state == ComponentState.READY and not self.error
|
||||
|
||||
class ComponentTracker:
|
||||
"""Tracks component states and relationships"""
|
||||
|
||||
def __init__(self):
|
||||
self.states: Dict[str, ComponentState] = {}
|
||||
self.history: List[Dict[str, Any]] = []
|
||||
MAX_HISTORY: ClassVar[int] = 1000 # Maximum history entries to keep
|
||||
|
||||
def update_state(self, name: str, state: ComponentState, error: Optional[str] = None) -> None:
|
||||
def __init__(self) -> None:
|
||||
self.states: Dict[str, ComponentState] = {}
|
||||
self.history: List[ComponentHistory] = []
|
||||
|
||||
def update_state(
|
||||
self,
|
||||
name: str,
|
||||
state: ComponentState,
|
||||
error: Optional[str] = None
|
||||
) -> None:
|
||||
"""Update component state"""
|
||||
self.states[name] = state
|
||||
self.history.append({
|
||||
"component": name,
|
||||
"state": state.value,
|
||||
"timestamp": datetime.utcnow(),
|
||||
"error": error
|
||||
})
|
||||
|
||||
# Add history entry
|
||||
now = datetime.utcnow()
|
||||
duration = 0.0
|
||||
if self.history:
|
||||
last_entry = self.history[-1]
|
||||
last_time = datetime.fromisoformat(last_entry["timestamp"])
|
||||
duration = (now - last_time).total_seconds()
|
||||
|
||||
def get_component_history(self, name: str) -> List[Dict[str, Any]]:
|
||||
self.history.append(ComponentHistory(
|
||||
component=name,
|
||||
state=state.name,
|
||||
timestamp=now.isoformat(),
|
||||
error=error,
|
||||
duration=duration
|
||||
))
|
||||
|
||||
# Cleanup old history
|
||||
if len(self.history) > self.MAX_HISTORY:
|
||||
self.history = self.history[-self.MAX_HISTORY:]
|
||||
|
||||
def get_component_history(self, name: str) -> List[ComponentHistory]:
|
||||
"""Get state history for a component"""
|
||||
return [
|
||||
entry for entry in self.history
|
||||
@@ -72,12 +141,33 @@ class ComponentTracker:
|
||||
class DependencyManager:
|
||||
"""Manages component dependencies"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.dependencies: Dict[str, Set[str]] = {}
|
||||
self.dependents: Dict[str, Set[str]] = {}
|
||||
|
||||
def add_dependency(self, component: str, dependency: str) -> None:
|
||||
"""Add a dependency relationship"""
|
||||
"""
|
||||
Add a dependency relationship.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
dependency: Dependency name
|
||||
|
||||
Raises:
|
||||
ComponentError: If dependency cycle is detected
|
||||
"""
|
||||
# Check for cycles
|
||||
if self._would_create_cycle(component, dependency):
|
||||
raise ComponentError(
|
||||
f"Dependency cycle detected: {component} -> {dependency}",
|
||||
context=ErrorContext(
|
||||
"DependencyManager",
|
||||
"add_dependency",
|
||||
{"component": component, "dependency": dependency},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
if component not in self.dependencies:
|
||||
self.dependencies[component] = set()
|
||||
self.dependencies[component].add(dependency)
|
||||
@@ -86,6 +176,23 @@ class DependencyManager:
|
||||
self.dependents[dependency] = set()
|
||||
self.dependents[dependency].add(component)
|
||||
|
||||
def _would_create_cycle(self, component: str, dependency: str) -> bool:
|
||||
"""Check if adding dependency would create a cycle"""
|
||||
visited = set()
|
||||
|
||||
def has_path(start: str, end: str) -> bool:
|
||||
if start == end:
|
||||
return True
|
||||
if start in visited:
|
||||
return False
|
||||
visited.add(start)
|
||||
return any(
|
||||
has_path(dep, end)
|
||||
for dep in self.dependencies.get(start, set())
|
||||
)
|
||||
|
||||
return has_path(dependency, component)
|
||||
|
||||
def get_dependencies(self, component: str) -> Set[str]:
|
||||
"""Get dependencies for a component"""
|
||||
return self.dependencies.get(component, set())
|
||||
@@ -95,27 +202,72 @@ class DependencyManager:
|
||||
return self.dependents.get(component, set())
|
||||
|
||||
def get_initialization_order(self) -> List[str]:
|
||||
"""Get components in dependency order"""
|
||||
visited = set()
|
||||
order = []
|
||||
"""
|
||||
Get components in dependency order.
|
||||
|
||||
Returns:
|
||||
List of component names in initialization order
|
||||
|
||||
Raises:
|
||||
ComponentError: If dependency cycle is detected
|
||||
"""
|
||||
visited: Set[str] = set()
|
||||
temp_visited: Set[str] = set()
|
||||
order: List[str] = []
|
||||
|
||||
def visit(component: str) -> None:
|
||||
if component in temp_visited:
|
||||
cycle = " -> ".join(
|
||||
name for name in self.dependencies
|
||||
if name in temp_visited
|
||||
)
|
||||
raise ComponentError(
|
||||
f"Dependency cycle detected: {cycle}",
|
||||
context=ErrorContext(
|
||||
"DependencyManager",
|
||||
"get_initialization_order",
|
||||
{"cycle": cycle},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
if component in visited:
|
||||
return
|
||||
visited.add(component)
|
||||
|
||||
temp_visited.add(component)
|
||||
for dep in self.dependencies.get(component, set()):
|
||||
visit(dep)
|
||||
temp_visited.remove(component)
|
||||
visited.add(component)
|
||||
order.append(component)
|
||||
|
||||
for component in self.dependencies:
|
||||
visit(component)
|
||||
try:
|
||||
for component in self.dependencies:
|
||||
if component not in visited:
|
||||
visit(component)
|
||||
except RecursionError:
|
||||
raise ComponentError(
|
||||
"Dependency resolution exceeded maximum recursion depth",
|
||||
context=ErrorContext(
|
||||
"DependencyManager",
|
||||
"get_initialization_order",
|
||||
None,
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
class ComponentManager:
|
||||
"""Manages VideoArchiver components"""
|
||||
|
||||
def __init__(self, cog):
|
||||
CORE_COMPONENTS: ClassVar[Dict[str, Tuple[Type[Any], Set[str]]]] = {
|
||||
"config_manager": ("..config_manager.ConfigManager", set()),
|
||||
"processor": ("..processor.core.Processor", {"config_manager"}),
|
||||
"queue_manager": ("..queue.manager.EnhancedVideoQueueManager", {"config_manager"}),
|
||||
"ffmpeg_mgr": ("..ffmpeg.ffmpeg_manager.FFmpegManager", set())
|
||||
}
|
||||
|
||||
def __init__(self, cog: Any) -> None:
|
||||
self.cog = cog
|
||||
self._components: Dict[str, Component] = {}
|
||||
self.tracker = ComponentTracker()
|
||||
@@ -124,21 +276,41 @@ class ComponentManager:
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
component: Any,
|
||||
component: Union[Component, Any],
|
||||
dependencies: Optional[Set[str]] = None
|
||||
) -> None:
|
||||
"""Register a component with dependencies"""
|
||||
"""
|
||||
Register a component with dependencies.
|
||||
|
||||
Args:
|
||||
name: Component name
|
||||
component: Component instance
|
||||
dependencies: Optional set of dependency names
|
||||
|
||||
Raises:
|
||||
ComponentError: If registration fails
|
||||
"""
|
||||
try:
|
||||
# Wrap non-Component objects
|
||||
if not isinstance(component, Component):
|
||||
component = Component(name)
|
||||
wrapped = Component(name)
|
||||
if isinstance(component, Initializable):
|
||||
wrapped.initialize = component.initialize
|
||||
wrapped.shutdown = component.shutdown
|
||||
component = wrapped
|
||||
|
||||
# Register dependencies
|
||||
if dependencies:
|
||||
for dep in dependencies:
|
||||
if dep not in self._components:
|
||||
raise ComponentDependencyError(
|
||||
f"Dependency {dep} not registered for {name}"
|
||||
raise ComponentError(
|
||||
f"Dependency {dep} not registered for {name}",
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"register",
|
||||
{"component": name, "dependency": dep},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
self.dependency_manager.add_dependency(name, dep)
|
||||
|
||||
@@ -149,19 +321,33 @@ class ComponentManager:
|
||||
logger.debug(f"Registered component: {name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error registering component {name}: {e}")
|
||||
error = f"Failed to register component {name}: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
self.tracker.update_state(name, ComponentState.ERROR, str(e))
|
||||
raise ComponentLifecycleError(f"Failed to register component: {str(e)}")
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"register",
|
||||
{"component": name},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
async def initialize_components(self) -> None:
|
||||
"""Initialize all components in dependency order"""
|
||||
"""
|
||||
Initialize all components in dependency order.
|
||||
|
||||
Raises:
|
||||
ComponentError: If initialization fails
|
||||
"""
|
||||
try:
|
||||
# Get initialization order
|
||||
init_order = self.dependency_manager.get_initialization_order()
|
||||
|
||||
# Initialize core components first
|
||||
await self._initialize_core_components()
|
||||
|
||||
# Get initialization order
|
||||
init_order = self.dependency_manager.get_initialization_order()
|
||||
|
||||
# Initialize remaining components
|
||||
for name in init_order:
|
||||
if name not in self._components:
|
||||
@@ -174,88 +360,172 @@ class ComponentManager:
|
||||
component.initialization_time = datetime.utcnow()
|
||||
self.tracker.update_state(name, ComponentState.READY)
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing component {name}: {e}")
|
||||
error = f"Failed to initialize component {name}: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
self.tracker.update_state(name, ComponentState.ERROR, str(e))
|
||||
raise ComponentLifecycleError(
|
||||
f"Failed to initialize component {name}: {str(e)}"
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"initialize_components",
|
||||
{"component": name},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during component initialization: {e}")
|
||||
raise ComponentLifecycleError(f"Component initialization failed: {str(e)}")
|
||||
error = f"Component initialization failed: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"initialize_components",
|
||||
None,
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
async def _initialize_core_components(self) -> None:
|
||||
"""Initialize core system components"""
|
||||
from ..config_manager import ConfigManager
|
||||
from ..processor.core import Processor
|
||||
from ..queue.manager import EnhancedVideoQueueManager
|
||||
from ..ffmpeg.ffmpeg_manager import FFmpegManager
|
||||
"""
|
||||
Initialize core system components.
|
||||
|
||||
Raises:
|
||||
ComponentError: If core component initialization fails
|
||||
"""
|
||||
try:
|
||||
for name, (component_path, deps) in self.CORE_COMPONENTS.items():
|
||||
module_path, class_name = component_path.rsplit(".", 1)
|
||||
module = __import__(module_path, fromlist=[class_name])
|
||||
component_class = getattr(module, class_name)
|
||||
|
||||
if name == "processor":
|
||||
component = component_class(self.cog)
|
||||
elif name == "ffmpeg_mgr":
|
||||
component = component_class(self.cog)
|
||||
else:
|
||||
component = component_class()
|
||||
|
||||
core_components = {
|
||||
"config_manager": (ConfigManager(self.cog), set()),
|
||||
"processor": (Processor(self.cog), {"config_manager"}),
|
||||
"queue_manager": (EnhancedVideoQueueManager(), {"config_manager"}),
|
||||
"ffmpeg_mgr": (FFmpegManager(self.cog), set())
|
||||
}
|
||||
self.register(name, component, deps)
|
||||
|
||||
for name, (component, deps) in core_components.items():
|
||||
self.register(name, component, deps)
|
||||
# Initialize paths
|
||||
await self._initialize_paths()
|
||||
|
||||
# Initialize paths
|
||||
await self._initialize_paths()
|
||||
except Exception as e:
|
||||
error = f"Failed to initialize core components: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"_initialize_core_components",
|
||||
None,
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
async def _initialize_paths(self) -> None:
|
||||
"""Initialize required paths"""
|
||||
from pathlib import Path
|
||||
from ..utils.path_manager import ensure_directory
|
||||
"""
|
||||
Initialize required paths.
|
||||
|
||||
Raises:
|
||||
ComponentError: If path initialization fails
|
||||
"""
|
||||
try:
|
||||
data_dir = Path(self.cog.bot.data_path) / "VideoArchiver"
|
||||
download_dir = data_dir / "downloads"
|
||||
|
||||
data_dir = Path(self.cog.bot.data_path) / "VideoArchiver"
|
||||
download_dir = data_dir / "downloads"
|
||||
# Ensure directories exist
|
||||
await ensure_directory(data_dir)
|
||||
await ensure_directory(download_dir)
|
||||
|
||||
# Ensure directories exist
|
||||
await ensure_directory(data_dir)
|
||||
await ensure_directory(download_dir)
|
||||
# Register paths
|
||||
self.register("data_path", data_dir)
|
||||
self.register("download_path", download_dir)
|
||||
|
||||
# Register paths
|
||||
self.register("data_path", data_dir)
|
||||
self.register("download_path", download_dir)
|
||||
except Exception as e:
|
||||
error = f"Failed to initialize paths: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"_initialize_paths",
|
||||
None,
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
def get(self, name: str) -> Optional[Any]:
|
||||
def get(self, name: str) -> Optional[Component]:
|
||||
"""Get a registered component"""
|
||||
component = self._components.get(name)
|
||||
return component if isinstance(component, Component) else None
|
||||
return self._components.get(name)
|
||||
|
||||
async def shutdown_components(self) -> None:
|
||||
"""Shutdown components in reverse dependency order"""
|
||||
shutdown_order = reversed(self.dependency_manager.get_initialization_order())
|
||||
"""
|
||||
Shutdown components in reverse dependency order.
|
||||
|
||||
for name in shutdown_order:
|
||||
if name not in self._components:
|
||||
continue
|
||||
|
||||
component = self._components[name]
|
||||
try:
|
||||
await component.shutdown()
|
||||
self.tracker.update_state(name, ComponentState.SHUTDOWN)
|
||||
except Exception as e:
|
||||
logger.error(f"Error shutting down component {name}: {e}")
|
||||
self.tracker.update_state(name, ComponentState.ERROR, str(e))
|
||||
Raises:
|
||||
ComponentError: If shutdown fails
|
||||
"""
|
||||
try:
|
||||
shutdown_order = reversed(self.dependency_manager.get_initialization_order())
|
||||
|
||||
for name in shutdown_order:
|
||||
if name not in self._components:
|
||||
continue
|
||||
|
||||
component = self._components[name]
|
||||
try:
|
||||
await component.shutdown()
|
||||
self.tracker.update_state(name, ComponentState.SHUTDOWN)
|
||||
except Exception as e:
|
||||
error = f"Error shutting down component {name}: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
self.tracker.update_state(name, ComponentState.ERROR, str(e))
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"shutdown_components",
|
||||
{"component": name},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error = f"Component shutdown failed: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ComponentManager",
|
||||
"shutdown_components",
|
||||
None,
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all registered components"""
|
||||
self._components.clear()
|
||||
logger.debug("Cleared all components")
|
||||
|
||||
def get_component_status(self) -> Dict[str, Any]:
|
||||
"""Get status of all components"""
|
||||
def get_component_status(self) -> Dict[str, ComponentStatus]:
|
||||
"""
|
||||
Get status of all components.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping component names to their status
|
||||
"""
|
||||
return {
|
||||
name: {
|
||||
"state": self.tracker.states.get(name, ComponentState.UNREGISTERED).value,
|
||||
"registration_time": component.registration_time,
|
||||
"initialization_time": component.initialization_time,
|
||||
"dependencies": self.dependency_manager.get_dependencies(name),
|
||||
"dependents": self.dependency_manager.get_dependents(name),
|
||||
"error": component.error
|
||||
}
|
||||
name: ComponentStatus(
|
||||
state=self.tracker.states.get(name, ComponentState.UNREGISTERED).name,
|
||||
registration_time=component.registration_time.isoformat() if component.registration_time else None,
|
||||
initialization_time=component.initialization_time.isoformat() if component.initialization_time else None,
|
||||
dependencies=self.dependency_manager.get_dependencies(name),
|
||||
dependents=self.dependency_manager.get_dependents(name),
|
||||
error=component.error,
|
||||
health=component.is_healthy()
|
||||
)
|
||||
for name, component in self._components.items()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Dict, Optional, Tuple, Type
|
||||
from typing import Dict, Optional, Tuple, Type, TypedDict, ClassVar
|
||||
from enum import Enum, auto
|
||||
import discord
|
||||
from redbot.core.commands import (
|
||||
Context,
|
||||
@@ -13,98 +14,179 @@ from redbot.core.commands import (
|
||||
CommandError
|
||||
)
|
||||
|
||||
from ..utils.exceptions import VideoArchiverError as ProcessingError, ConfigurationError as ConfigError
|
||||
from ..utils.exceptions import (
|
||||
VideoArchiverError,
|
||||
ErrorSeverity,
|
||||
ErrorContext,
|
||||
ProcessorError,
|
||||
ValidationError,
|
||||
DisplayError,
|
||||
URLExtractionError,
|
||||
MessageHandlerError,
|
||||
QueueHandlerError,
|
||||
QueueProcessorError,
|
||||
FFmpegError,
|
||||
DatabaseError,
|
||||
HealthCheckError,
|
||||
TrackingError,
|
||||
NetworkError,
|
||||
ResourceExhaustedError,
|
||||
ConfigurationError
|
||||
)
|
||||
from .response_handler import response_manager
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
class ErrorCategory(Enum):
|
||||
"""Categories of errors"""
|
||||
PERMISSION = auto()
|
||||
ARGUMENT = auto()
|
||||
CONFIGURATION = auto()
|
||||
PROCESSING = auto()
|
||||
NETWORK = auto()
|
||||
RESOURCE = auto()
|
||||
DATABASE = auto()
|
||||
VALIDATION = auto()
|
||||
QUEUE = auto()
|
||||
CLEANUP = auto()
|
||||
HEALTH = auto()
|
||||
UNEXPECTED = auto()
|
||||
|
||||
class ErrorStats(TypedDict):
|
||||
"""Type definition for error statistics"""
|
||||
counts: Dict[str, int]
|
||||
patterns: Dict[str, Dict[str, int]]
|
||||
severities: Dict[str, Dict[str, int]]
|
||||
|
||||
class ErrorFormatter:
|
||||
"""Formats error messages for display"""
|
||||
|
||||
@staticmethod
|
||||
def format_permission_error(error: Exception) -> str:
|
||||
"""Format permission error messages"""
|
||||
def format_error_message(error: Exception, context: Optional[ErrorContext] = None) -> str:
|
||||
"""Format error message with context"""
|
||||
base_message = str(error)
|
||||
if context:
|
||||
return f"{context}: {base_message}"
|
||||
return base_message
|
||||
|
||||
@staticmethod
|
||||
def format_user_message(error: Exception, category: ErrorCategory) -> str:
|
||||
"""Format user-friendly error message"""
|
||||
if isinstance(error, MissingPermissions):
|
||||
return "You don't have permission to use this command."
|
||||
elif isinstance(error, BotMissingPermissions):
|
||||
return "I don't have the required permissions to do that."
|
||||
return str(error)
|
||||
|
||||
@staticmethod
|
||||
def format_argument_error(error: Exception) -> str:
|
||||
"""Format argument error messages"""
|
||||
if isinstance(error, MissingRequiredArgument):
|
||||
elif isinstance(error, MissingRequiredArgument):
|
||||
return f"Missing required argument: {error.param.name}"
|
||||
elif isinstance(error, BadArgument):
|
||||
return f"Invalid argument: {str(error)}"
|
||||
elif isinstance(error, VideoArchiverError):
|
||||
return str(error)
|
||||
elif category == ErrorCategory.UNEXPECTED:
|
||||
return "An unexpected error occurred. Please check the logs for details."
|
||||
return str(error)
|
||||
|
||||
@staticmethod
|
||||
def format_processing_error(error: ProcessingError) -> str:
|
||||
"""Format processing error messages"""
|
||||
return f"Processing error: {str(error)}"
|
||||
|
||||
@staticmethod
|
||||
def format_config_error(error: ConfigError) -> str:
|
||||
"""Format configuration error messages"""
|
||||
return f"Configuration error: {str(error)}"
|
||||
|
||||
@staticmethod
|
||||
def format_unexpected_error(error: Exception) -> str:
|
||||
"""Format unexpected error messages"""
|
||||
return "An unexpected error occurred. Check the logs for details."
|
||||
|
||||
class ErrorCategorizer:
|
||||
"""Categorizes errors and determines handling strategy"""
|
||||
|
||||
ERROR_TYPES = {
|
||||
MissingPermissions: ("permission", "error"),
|
||||
BotMissingPermissions: ("permission", "error"),
|
||||
MissingRequiredArgument: ("argument", "warning"),
|
||||
BadArgument: ("argument", "warning"),
|
||||
ConfigError: ("configuration", "error"),
|
||||
ProcessingError: ("processing", "error"),
|
||||
ERROR_MAPPING: ClassVar[Dict[Type[Exception], Tuple[ErrorCategory, ErrorSeverity]]] = {
|
||||
# Discord command errors
|
||||
MissingPermissions: (ErrorCategory.PERMISSION, ErrorSeverity.MEDIUM),
|
||||
BotMissingPermissions: (ErrorCategory.PERMISSION, ErrorSeverity.HIGH),
|
||||
MissingRequiredArgument: (ErrorCategory.ARGUMENT, ErrorSeverity.LOW),
|
||||
BadArgument: (ErrorCategory.ARGUMENT, ErrorSeverity.LOW),
|
||||
|
||||
# VideoArchiver errors
|
||||
ProcessorError: (ErrorCategory.PROCESSING, ErrorSeverity.HIGH),
|
||||
ValidationError: (ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM),
|
||||
DisplayError: (ErrorCategory.PROCESSING, ErrorSeverity.LOW),
|
||||
URLExtractionError: (ErrorCategory.PROCESSING, ErrorSeverity.MEDIUM),
|
||||
MessageHandlerError: (ErrorCategory.PROCESSING, ErrorSeverity.MEDIUM),
|
||||
QueueHandlerError: (ErrorCategory.QUEUE, ErrorSeverity.HIGH),
|
||||
QueueProcessorError: (ErrorCategory.QUEUE, ErrorSeverity.HIGH),
|
||||
FFmpegError: (ErrorCategory.PROCESSING, ErrorSeverity.HIGH),
|
||||
DatabaseError: (ErrorCategory.DATABASE, ErrorSeverity.HIGH),
|
||||
HealthCheckError: (ErrorCategory.HEALTH, ErrorSeverity.HIGH),
|
||||
TrackingError: (ErrorCategory.PROCESSING, ErrorSeverity.MEDIUM),
|
||||
NetworkError: (ErrorCategory.NETWORK, ErrorSeverity.MEDIUM),
|
||||
ResourceExhaustedError: (ErrorCategory.RESOURCE, ErrorSeverity.HIGH),
|
||||
ConfigurationError: (ErrorCategory.CONFIGURATION, ErrorSeverity.HIGH)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def categorize_error(cls, error: Exception) -> Tuple[str, str]:
|
||||
"""Categorize an error and determine its severity
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: (Error category, Severity level)
|
||||
def categorize_error(cls, error: Exception) -> Tuple[ErrorCategory, ErrorSeverity]:
|
||||
"""
|
||||
for error_type, (category, severity) in cls.ERROR_TYPES.items():
|
||||
Categorize an error and determine its severity.
|
||||
|
||||
Args:
|
||||
error: Exception to categorize
|
||||
|
||||
Returns:
|
||||
Tuple of (Error category, Severity level)
|
||||
"""
|
||||
for error_type, (category, severity) in cls.ERROR_MAPPING.items():
|
||||
if isinstance(error, error_type):
|
||||
return category, severity
|
||||
return "unexpected", "error"
|
||||
return ErrorCategory.UNEXPECTED, ErrorSeverity.HIGH
|
||||
|
||||
class ErrorTracker:
|
||||
"""Tracks error occurrences and patterns"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.error_counts: Dict[str, int] = {}
|
||||
self.error_patterns: Dict[str, Dict[str, int]] = {}
|
||||
self.error_severities: Dict[str, Dict[str, int]] = {}
|
||||
|
||||
def track_error(self, error: Exception, category: str) -> None:
|
||||
"""Track an error occurrence"""
|
||||
def track_error(
|
||||
self,
|
||||
error: Exception,
|
||||
category: ErrorCategory,
|
||||
severity: ErrorSeverity
|
||||
) -> None:
|
||||
"""
|
||||
Track an error occurrence.
|
||||
|
||||
Args:
|
||||
error: Exception that occurred
|
||||
category: Error category
|
||||
severity: Error severity
|
||||
"""
|
||||
error_type = type(error).__name__
|
||||
|
||||
# Track error counts
|
||||
self.error_counts[error_type] = self.error_counts.get(error_type, 0) + 1
|
||||
|
||||
if category not in self.error_patterns:
|
||||
self.error_patterns[category] = {}
|
||||
self.error_patterns[category][error_type] = self.error_patterns[category].get(error_type, 0) + 1
|
||||
# Track error patterns by category
|
||||
if category.value not in self.error_patterns:
|
||||
self.error_patterns[category.value] = {}
|
||||
self.error_patterns[category.value][error_type] = (
|
||||
self.error_patterns[category.value].get(error_type, 0) + 1
|
||||
)
|
||||
|
||||
# Track error severities
|
||||
if severity.value not in self.error_severities:
|
||||
self.error_severities[severity.value] = {}
|
||||
self.error_severities[severity.value][error_type] = (
|
||||
self.error_severities[severity.value].get(error_type, 0) + 1
|
||||
)
|
||||
|
||||
def get_error_stats(self) -> Dict:
|
||||
"""Get error statistics"""
|
||||
return {
|
||||
"counts": self.error_counts.copy(),
|
||||
"patterns": self.error_patterns.copy()
|
||||
}
|
||||
def get_error_stats(self) -> ErrorStats:
|
||||
"""
|
||||
Get error statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary containing error statistics
|
||||
"""
|
||||
return ErrorStats(
|
||||
counts=self.error_counts.copy(),
|
||||
patterns=self.error_patterns.copy(),
|
||||
severities=self.error_severities.copy()
|
||||
)
|
||||
|
||||
class ErrorManager:
|
||||
"""Manages error handling and reporting"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.formatter = ErrorFormatter()
|
||||
self.categorizer = ErrorCategorizer()
|
||||
self.tracker = ErrorTracker()
|
||||
@@ -114,7 +196,8 @@ class ErrorManager:
|
||||
ctx: Context,
|
||||
error: Exception
|
||||
) -> None:
|
||||
"""Handle a command error
|
||||
"""
|
||||
Handle a command error.
|
||||
|
||||
Args:
|
||||
ctx: Command context
|
||||
@@ -124,24 +207,40 @@ class ErrorManager:
|
||||
# Categorize error
|
||||
category, severity = self.categorizer.categorize_error(error)
|
||||
|
||||
# Track error
|
||||
self.tracker.track_error(error, category)
|
||||
# Create error context
|
||||
context = ErrorContext(
|
||||
component=ctx.command.qualified_name if ctx.command else "unknown",
|
||||
operation="command_execution",
|
||||
details={
|
||||
"guild_id": str(ctx.guild.id) if ctx.guild else "DM",
|
||||
"channel_id": str(ctx.channel.id),
|
||||
"user_id": str(ctx.author.id)
|
||||
},
|
||||
severity=severity
|
||||
)
|
||||
|
||||
# Format error message
|
||||
error_msg = await self._format_error_message(error, category)
|
||||
# Track error
|
||||
self.tracker.track_error(error, category, severity)
|
||||
|
||||
# Format error messages
|
||||
log_message = self.formatter.format_error_message(error, context)
|
||||
user_message = self.formatter.format_user_message(error, category)
|
||||
|
||||
# Log error details
|
||||
self._log_error(ctx, error, category, severity)
|
||||
self._log_error(log_message, severity)
|
||||
|
||||
# Send response
|
||||
await response_manager.send_response(
|
||||
ctx,
|
||||
content=error_msg,
|
||||
response_type=severity
|
||||
content=user_message,
|
||||
response_type=severity.name.lower()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling command error: {str(e)}")
|
||||
logger.error(
|
||||
f"Error handling command error: {str(e)}\n"
|
||||
f"Original error: {traceback.format_exc()}"
|
||||
)
|
||||
try:
|
||||
await response_manager.send_response(
|
||||
ctx,
|
||||
@@ -151,46 +250,25 @@ class ErrorManager:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _format_error_message(
|
||||
self,
|
||||
error: Exception,
|
||||
category: str
|
||||
) -> str:
|
||||
"""Format error message based on category"""
|
||||
try:
|
||||
if category == "permission":
|
||||
return self.formatter.format_permission_error(error)
|
||||
elif category == "argument":
|
||||
return self.formatter.format_argument_error(error)
|
||||
elif category == "processing":
|
||||
return self.formatter.format_processing_error(error)
|
||||
elif category == "configuration":
|
||||
return self.formatter.format_config_error(error)
|
||||
else:
|
||||
return self.formatter.format_unexpected_error(error)
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting error message: {e}")
|
||||
return "An error occurred. Please check the logs."
|
||||
|
||||
def _log_error(
|
||||
self,
|
||||
ctx: Context,
|
||||
error: Exception,
|
||||
category: str,
|
||||
severity: str
|
||||
message: str,
|
||||
severity: ErrorSeverity
|
||||
) -> None:
|
||||
"""Log error details"""
|
||||
"""
|
||||
Log error details.
|
||||
|
||||
Args:
|
||||
message: Error message to log
|
||||
severity: Error severity
|
||||
"""
|
||||
try:
|
||||
if severity == "error":
|
||||
logger.error(
|
||||
f"Command error in {ctx.command} (Category: {category}):\n"
|
||||
f"{traceback.format_exc()}"
|
||||
)
|
||||
if severity in (ErrorSeverity.HIGH, ErrorSeverity.CRITICAL):
|
||||
logger.error(f"{message}\n{traceback.format_exc()}")
|
||||
elif severity == ErrorSeverity.MEDIUM:
|
||||
logger.warning(message)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Command warning in {ctx.command} (Category: {category}):\n"
|
||||
f"{str(error)}"
|
||||
)
|
||||
logger.info(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error logging error details: {e}")
|
||||
|
||||
@@ -198,5 +276,11 @@ class ErrorManager:
|
||||
error_manager = ErrorManager()
|
||||
|
||||
async def handle_command_error(ctx: Context, error: Exception) -> None:
|
||||
"""Helper function to handle command errors using the error manager"""
|
||||
"""
|
||||
Helper function to handle command errors using the error manager.
|
||||
|
||||
Args:
|
||||
ctx: Command context
|
||||
error: Exception to handle
|
||||
"""
|
||||
await error_manager.handle_error(ctx, error)
|
||||
|
||||
@@ -1,59 +1,147 @@
|
||||
"""Event handlers for VideoArchiver"""
|
||||
|
||||
import logging
|
||||
import discord
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from enum import Enum, auto
|
||||
from typing import TYPE_CHECKING, Dict, Any, Optional, TypedDict, ClassVar, List
|
||||
|
||||
import discord
|
||||
|
||||
from ..processor.constants import REACTIONS
|
||||
from ..processor.reactions import handle_archived_reaction
|
||||
from .guild import initialize_guild_components, cleanup_guild_components
|
||||
from .error_handler import error_manager
|
||||
from .response_handler import response_manager
|
||||
from ..utils.exceptions import EventError, ErrorContext, ErrorSeverity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import VideoArchiver
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
"""Types of Discord events"""
|
||||
GUILD_JOIN = auto()
|
||||
GUILD_REMOVE = auto()
|
||||
MESSAGE = auto()
|
||||
REACTION_ADD = auto()
|
||||
MESSAGE_PROCESSING = auto()
|
||||
REACTION_PROCESSING = auto()
|
||||
|
||||
|
||||
class EventStats(TypedDict):
|
||||
"""Type definition for event statistics"""
|
||||
counts: Dict[str, int]
|
||||
last_events: Dict[str, str]
|
||||
errors: Dict[str, int]
|
||||
error_rate: float
|
||||
health: bool
|
||||
|
||||
|
||||
class EventHistory(TypedDict):
|
||||
"""Type definition for event history entry"""
|
||||
event_type: str
|
||||
timestamp: str
|
||||
guild_id: Optional[int]
|
||||
channel_id: Optional[int]
|
||||
message_id: Optional[int]
|
||||
user_id: Optional[int]
|
||||
error: Optional[str]
|
||||
duration: float
|
||||
|
||||
|
||||
class EventTracker:
|
||||
"""Tracks event occurrences and patterns"""
|
||||
|
||||
def __init__(self):
|
||||
MAX_HISTORY: ClassVar[int] = 1000 # Maximum history entries to keep
|
||||
ERROR_THRESHOLD: ClassVar[float] = 0.1 # 10% error rate threshold
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.event_counts: Dict[str, int] = {}
|
||||
self.last_events: Dict[str, datetime] = {}
|
||||
self.error_counts: Dict[str, int] = {}
|
||||
self.history: List[EventHistory] = []
|
||||
|
||||
def record_event(self, event_type: str) -> None:
|
||||
def record_event(
|
||||
self,
|
||||
event_type: EventType,
|
||||
guild_id: Optional[int] = None,
|
||||
channel_id: Optional[int] = None,
|
||||
message_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Record an event occurrence"""
|
||||
self.event_counts[event_type] = self.event_counts.get(event_type, 0) + 1
|
||||
self.last_events[event_type] = datetime.utcnow()
|
||||
event_name = event_type.name
|
||||
self.event_counts[event_name] = self.event_counts.get(event_name, 0) + 1
|
||||
self.last_events[event_name] = datetime.utcnow()
|
||||
|
||||
def record_error(self, event_type: str) -> None:
|
||||
# Add to history
|
||||
self.history.append(
|
||||
EventHistory(
|
||||
event_type=event_name,
|
||||
timestamp=datetime.utcnow().isoformat(),
|
||||
guild_id=guild_id,
|
||||
channel_id=channel_id,
|
||||
message_id=message_id,
|
||||
user_id=user_id,
|
||||
error=None,
|
||||
duration=0.0,
|
||||
)
|
||||
)
|
||||
|
||||
# Cleanup old history
|
||||
if len(self.history) > self.MAX_HISTORY:
|
||||
self.history = self.history[-self.MAX_HISTORY:]
|
||||
|
||||
def record_error(
|
||||
self, event_type: EventType, error: str, duration: float = 0.0
|
||||
) -> None:
|
||||
"""Record an event error"""
|
||||
self.error_counts[event_type] = self.error_counts.get(event_type, 0) + 1
|
||||
event_name = event_type.name
|
||||
self.error_counts[event_name] = self.error_counts.get(event_name, 0) + 1
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
# Update last history entry with error
|
||||
if self.history:
|
||||
self.history[-1].update({"error": error, "duration": duration})
|
||||
|
||||
def get_stats(self) -> EventStats:
|
||||
"""Get event statistics"""
|
||||
return {
|
||||
"counts": self.event_counts.copy(),
|
||||
"last_events": {k: v.isoformat() for k, v in self.last_events.items()},
|
||||
"errors": self.error_counts.copy()
|
||||
}
|
||||
total_events = sum(self.event_counts.values())
|
||||
total_errors = sum(self.error_counts.values())
|
||||
error_rate = total_errors / total_events if total_events > 0 else 0.0
|
||||
|
||||
return EventStats(
|
||||
counts=self.event_counts.copy(),
|
||||
last_events={k: v.isoformat() for k, v in self.last_events.items()},
|
||||
errors=self.error_counts.copy(),
|
||||
error_rate=error_rate,
|
||||
health=error_rate < self.ERROR_THRESHOLD,
|
||||
)
|
||||
|
||||
|
||||
class GuildEventHandler:
|
||||
"""Handles guild-related events"""
|
||||
|
||||
def __init__(self, cog: "VideoArchiver", tracker: EventTracker):
|
||||
def __init__(self, cog: "VideoArchiver", tracker: EventTracker) -> None:
|
||||
self.cog = cog
|
||||
self.tracker = tracker
|
||||
|
||||
async def handle_guild_join(self, guild: discord.Guild) -> None:
|
||||
"""Handle bot joining a new guild"""
|
||||
self.tracker.record_event("guild_join")
|
||||
|
||||
"""
|
||||
Handle bot joining a new guild.
|
||||
|
||||
Args:
|
||||
guild: Discord guild that was joined
|
||||
|
||||
Raises:
|
||||
EventError: If guild initialization fails
|
||||
"""
|
||||
start_time = datetime.utcnow()
|
||||
self.tracker.record_event(EventType.GUILD_JOIN, guild_id=guild.id)
|
||||
|
||||
if not self.cog.ready.is_set():
|
||||
return
|
||||
|
||||
@@ -61,29 +149,72 @@ class GuildEventHandler:
|
||||
await initialize_guild_components(self.cog, guild.id)
|
||||
logger.info(f"Initialized components for new guild {guild.id}")
|
||||
except Exception as e:
|
||||
self.tracker.record_error("guild_join")
|
||||
logger.error(f"Failed to initialize new guild {guild.id}: {str(e)}")
|
||||
duration = (datetime.utcnow() - start_time).total_seconds()
|
||||
self.tracker.record_error(EventType.GUILD_JOIN, str(e), duration)
|
||||
error = f"Failed to initialize new guild {guild.id}: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise EventError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"GuildEventHandler",
|
||||
"handle_guild_join",
|
||||
{"guild_id": guild.id},
|
||||
ErrorSeverity.HIGH,
|
||||
),
|
||||
)
|
||||
|
||||
async def handle_guild_remove(self, guild: discord.Guild) -> None:
|
||||
"""Handle bot leaving a guild"""
|
||||
self.tracker.record_event("guild_remove")
|
||||
|
||||
"""
|
||||
Handle bot leaving a guild.
|
||||
|
||||
Args:
|
||||
guild: Discord guild that was left
|
||||
|
||||
Raises:
|
||||
EventError: If guild cleanup fails
|
||||
"""
|
||||
start_time = datetime.utcnow()
|
||||
self.tracker.record_event(EventType.GUILD_REMOVE, guild_id=guild.id)
|
||||
|
||||
try:
|
||||
await cleanup_guild_components(self.cog, guild.id)
|
||||
except Exception as e:
|
||||
self.tracker.record_error("guild_remove")
|
||||
logger.error(f"Error cleaning up removed guild {guild.id}: {str(e)}")
|
||||
duration = (datetime.utcnow() - start_time).total_seconds()
|
||||
self.tracker.record_error(EventType.GUILD_REMOVE, str(e), duration)
|
||||
error = f"Error cleaning up removed guild {guild.id}: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise EventError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"GuildEventHandler",
|
||||
"handle_guild_remove",
|
||||
{"guild_id": guild.id},
|
||||
ErrorSeverity.HIGH,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class MessageEventHandler:
|
||||
"""Handles message-related events"""
|
||||
|
||||
def __init__(self, cog: "VideoArchiver", tracker: EventTracker):
|
||||
def __init__(self, cog: "VideoArchiver", tracker: EventTracker) -> None:
|
||||
self.cog = cog
|
||||
self.tracker = tracker
|
||||
|
||||
async def handle_message(self, message: discord.Message) -> None:
|
||||
"""Handle new messages for video processing"""
|
||||
self.tracker.record_event("message")
|
||||
"""
|
||||
Handle new messages for video processing.
|
||||
|
||||
Args:
|
||||
message: Discord message to process
|
||||
"""
|
||||
self.tracker.record_event(
|
||||
EventType.MESSAGE,
|
||||
guild_id=message.guild.id if message.guild else None,
|
||||
channel_id=message.channel.id,
|
||||
message_id=message.id,
|
||||
user_id=message.author.id,
|
||||
)
|
||||
|
||||
# Skip if not ready or if message is from DM/bot
|
||||
if not self.cog.ready.is_set() or message.guild is None or message.author.bot:
|
||||
@@ -99,21 +230,19 @@ class MessageEventHandler:
|
||||
|
||||
async def _process_message_background(self, message: discord.Message) -> None:
|
||||
"""Process message in background to avoid blocking"""
|
||||
start_time = datetime.utcnow()
|
||||
try:
|
||||
await self.cog.processor.process_message(message)
|
||||
except Exception as e:
|
||||
self.tracker.record_error("message_processing")
|
||||
duration = (datetime.utcnow() - start_time).total_seconds()
|
||||
self.tracker.record_error(EventType.MESSAGE_PROCESSING, str(e), duration)
|
||||
await self._handle_processing_error(message, e)
|
||||
|
||||
async def _handle_processing_error(
|
||||
self,
|
||||
message: discord.Message,
|
||||
error: Exception
|
||||
self, message: discord.Message, error: Exception
|
||||
) -> None:
|
||||
"""Handle message processing errors"""
|
||||
logger.error(
|
||||
f"Error processing message {message.id}: {traceback.format_exc()}"
|
||||
)
|
||||
logger.error(f"Error processing message {message.id}: {traceback.format_exc()}")
|
||||
try:
|
||||
log_channel = await self.cog.config_manager.get_channel(
|
||||
message.guild, "log"
|
||||
@@ -126,24 +255,35 @@ class MessageEventHandler:
|
||||
f"Message ID: {message.id}\n"
|
||||
f"Channel: {message.channel.mention}"
|
||||
),
|
||||
response_type="error"
|
||||
response_type=ErrorSeverity.HIGH,
|
||||
)
|
||||
except Exception as log_error:
|
||||
logger.error(f"Failed to log error to guild: {str(log_error)}")
|
||||
|
||||
|
||||
class ReactionEventHandler:
|
||||
"""Handles reaction-related events"""
|
||||
|
||||
def __init__(self, cog: "VideoArchiver", tracker: EventTracker):
|
||||
def __init__(self, cog: "VideoArchiver", tracker: EventTracker) -> None:
|
||||
self.cog = cog
|
||||
self.tracker = tracker
|
||||
|
||||
async def handle_reaction_add(
|
||||
self,
|
||||
payload: discord.RawReactionActionEvent
|
||||
self, payload: discord.RawReactionActionEvent
|
||||
) -> None:
|
||||
"""Handle reactions to messages"""
|
||||
self.tracker.record_event("reaction_add")
|
||||
"""
|
||||
Handle reactions to messages.
|
||||
|
||||
Args:
|
||||
payload: Reaction event payload
|
||||
"""
|
||||
self.tracker.record_event(
|
||||
EventType.REACTION_ADD,
|
||||
guild_id=payload.guild_id,
|
||||
channel_id=payload.channel_id,
|
||||
message_id=payload.message_id,
|
||||
user_id=payload.user_id,
|
||||
)
|
||||
|
||||
if payload.user_id == self.cog.bot.user.id:
|
||||
return
|
||||
@@ -151,47 +291,80 @@ class ReactionEventHandler:
|
||||
try:
|
||||
await self._process_reaction(payload)
|
||||
except Exception as e:
|
||||
self.tracker.record_error("reaction_processing")
|
||||
logger.error(f"Error handling reaction: {e}")
|
||||
self.tracker.record_error(EventType.REACTION_PROCESSING, str(e))
|
||||
logger.error(f"Error handling reaction: {e}", exc_info=True)
|
||||
|
||||
async def _process_reaction(
|
||||
self,
|
||||
payload: discord.RawReactionActionEvent
|
||||
) -> None:
|
||||
"""Process a reaction event"""
|
||||
# Get the channel and message
|
||||
channel = self.cog.bot.get_channel(payload.channel_id)
|
||||
if not channel:
|
||||
return
|
||||
|
||||
message = await channel.fetch_message(payload.message_id)
|
||||
if not message:
|
||||
return
|
||||
async def _process_reaction(self, payload: discord.RawReactionActionEvent) -> None:
|
||||
"""
|
||||
Process a reaction event.
|
||||
|
||||
Args:
|
||||
payload: Reaction event payload
|
||||
|
||||
Raises:
|
||||
EventError: If reaction processing fails
|
||||
"""
|
||||
try:
|
||||
# Get the channel and message
|
||||
channel = self.cog.bot.get_channel(payload.channel_id)
|
||||
if not channel:
|
||||
return
|
||||
|
||||
message = await channel.fetch_message(payload.message_id)
|
||||
if not message:
|
||||
return
|
||||
|
||||
# Check if it's the archived reaction
|
||||
if str(payload.emoji) == REACTIONS["archived"]:
|
||||
# Only process if database is enabled
|
||||
if self.cog.db:
|
||||
user = self.cog.bot.get_user(payload.user_id)
|
||||
asyncio.create_task(
|
||||
handle_archived_reaction(message, user, self.cog.db)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error = f"Failed to process reaction: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise EventError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"ReactionEventHandler",
|
||||
"_process_reaction",
|
||||
{
|
||||
"message_id": payload.message_id,
|
||||
"user_id": payload.user_id,
|
||||
"emoji": str(payload.emoji),
|
||||
},
|
||||
ErrorSeverity.MEDIUM,
|
||||
),
|
||||
)
|
||||
|
||||
# Check if it's the archived reaction
|
||||
if str(payload.emoji) == REACTIONS["archived"]:
|
||||
# Only process if database is enabled
|
||||
if self.cog.db:
|
||||
user = self.cog.bot.get_user(payload.user_id)
|
||||
asyncio.create_task(
|
||||
handle_archived_reaction(message, user, self.cog.db)
|
||||
)
|
||||
|
||||
class EventManager:
|
||||
"""Manages Discord event handling"""
|
||||
|
||||
def __init__(self, cog: "VideoArchiver"):
|
||||
def __init__(self, cog: "VideoArchiver") -> None:
|
||||
self.tracker = EventTracker()
|
||||
self.guild_handler = GuildEventHandler(cog, self.tracker)
|
||||
self.message_handler = MessageEventHandler(cog, self.tracker)
|
||||
self.reaction_handler = ReactionEventHandler(cog, self.tracker)
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
def get_stats(self) -> EventStats:
|
||||
"""Get event statistics"""
|
||||
return self.tracker.get_stats()
|
||||
|
||||
def setup_events(cog: "VideoArchiver") -> None:
|
||||
"""Set up event handlers for the cog"""
|
||||
|
||||
def setup_events(cog: "VideoArchiver") -> EventManager:
|
||||
"""
|
||||
Set up event handlers for the cog.
|
||||
|
||||
Args:
|
||||
cog: VideoArchiver cog instance
|
||||
|
||||
Returns:
|
||||
Configured EventManager instance
|
||||
"""
|
||||
event_manager = EventManager(cog)
|
||||
|
||||
@cog.listener()
|
||||
@@ -209,3 +382,5 @@ def setup_events(cog: "VideoArchiver") -> None:
|
||||
@cog.listener()
|
||||
async def on_raw_reaction_add(payload: discord.RawReactionActionEvent) -> None:
|
||||
await event_manager.reaction_handler.handle_reaction_add(payload)
|
||||
|
||||
return event_manager
|
||||
|
||||
@@ -1,16 +1,102 @@
|
||||
"""Module for handling VideoArchiver initialization"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from ..utils.exceptions import (
|
||||
ComponentError,
|
||||
ErrorContext,
|
||||
ErrorSeverity
|
||||
)
|
||||
from .lifecycle import LifecycleState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import VideoArchiver
|
||||
|
||||
# Re-export initialization functions from lifecycle
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
async def initialize_cog(cog: "VideoArchiver") -> None:
|
||||
"""Initialize all components with proper error handling"""
|
||||
await cog.lifecycle_manager.initialize_cog()
|
||||
"""
|
||||
Initialize all components with proper error handling.
|
||||
|
||||
This is a re-export of lifecycle_manager.initialize_cog with additional
|
||||
error context and logging.
|
||||
|
||||
Args:
|
||||
cog: VideoArchiver cog instance
|
||||
|
||||
Raises:
|
||||
ComponentError: If initialization fails
|
||||
"""
|
||||
try:
|
||||
logger.info("Starting cog initialization...")
|
||||
await cog.lifecycle_manager.initialize_cog()
|
||||
logger.info("Cog initialization completed successfully")
|
||||
except Exception as e:
|
||||
error = f"Failed to initialize cog: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"Initialization",
|
||||
"initialize_cog",
|
||||
{"state": cog.lifecycle_manager.state_tracker.state.name},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
def init_callback(cog: "VideoArchiver", task: asyncio.Task) -> None:
|
||||
"""Handle initialization task completion"""
|
||||
cog.lifecycle_manager.init_callback(task)
|
||||
"""
|
||||
Handle initialization task completion.
|
||||
|
||||
This is a re-export of lifecycle_manager.init_callback with additional
|
||||
error context and logging.
|
||||
|
||||
Args:
|
||||
cog: VideoArchiver cog instance
|
||||
task: Initialization task
|
||||
"""
|
||||
try:
|
||||
logger.debug("Processing initialization task completion...")
|
||||
cog.lifecycle_manager.init_callback(task)
|
||||
|
||||
# Log final state
|
||||
state = cog.lifecycle_manager.state_tracker.state
|
||||
if state == LifecycleState.READY:
|
||||
logger.info("Initialization completed successfully")
|
||||
elif state == LifecycleState.ERROR:
|
||||
logger.error("Initialization failed")
|
||||
else:
|
||||
logger.warning(f"Unexpected state after initialization: {state.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in initialization callback: {str(e)}", exc_info=True)
|
||||
# We don't raise here since this is a callback
|
||||
|
||||
def get_init_status(cog: "VideoArchiver") -> Dict[str, Any]:
|
||||
"""
|
||||
Get initialization status information.
|
||||
|
||||
Args:
|
||||
cog: VideoArchiver cog instance
|
||||
|
||||
Returns:
|
||||
Dictionary containing initialization status
|
||||
"""
|
||||
return {
|
||||
"state": cog.lifecycle_manager.state_tracker.state.name,
|
||||
"ready": cog.ready.is_set(),
|
||||
"components_initialized": all(
|
||||
hasattr(cog, attr) and getattr(cog, attr) is not None
|
||||
for attr in [
|
||||
"processor",
|
||||
"queue_manager",
|
||||
"update_checker",
|
||||
"ffmpeg_mgr",
|
||||
"components",
|
||||
"db"
|
||||
]
|
||||
),
|
||||
"history": cog.lifecycle_manager.state_tracker.get_state_history()
|
||||
}
|
||||
|
||||
@@ -3,82 +3,195 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Optional, Dict, Any, Set, List, Callable
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, Any, Set, List, Callable, TypedDict, ClassVar, Union
|
||||
from enum import Enum, auto
|
||||
from datetime import datetime
|
||||
|
||||
from .cleanup import cleanup_resources, force_cleanup_resources
|
||||
from ..utils.exceptions import VideoArchiverError
|
||||
from ..utils.exceptions import (
|
||||
VideoArchiverError,
|
||||
ErrorContext,
|
||||
ErrorSeverity,
|
||||
ComponentError,
|
||||
CleanupError
|
||||
)
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
class LifecycleState(Enum):
|
||||
"""Possible states in the cog lifecycle"""
|
||||
UNINITIALIZED = "uninitialized"
|
||||
INITIALIZING = "initializing"
|
||||
READY = "ready"
|
||||
UNLOADING = "unloading"
|
||||
ERROR = "error"
|
||||
UNINITIALIZED = auto()
|
||||
INITIALIZING = auto()
|
||||
READY = auto()
|
||||
UNLOADING = auto()
|
||||
ERROR = auto()
|
||||
|
||||
class TaskStatus(Enum):
|
||||
"""Task execution status"""
|
||||
RUNNING = auto()
|
||||
COMPLETED = auto()
|
||||
CANCELLED = auto()
|
||||
FAILED = auto()
|
||||
|
||||
class TaskHistory(TypedDict):
|
||||
"""Type definition for task history entry"""
|
||||
start_time: str
|
||||
end_time: Optional[str]
|
||||
status: str
|
||||
error: Optional[str]
|
||||
duration: float
|
||||
|
||||
class StateHistory(TypedDict):
|
||||
"""Type definition for state history entry"""
|
||||
state: str
|
||||
timestamp: str
|
||||
duration: float
|
||||
details: Optional[Dict[str, Any]]
|
||||
|
||||
class LifecycleStatus(TypedDict):
|
||||
"""Type definition for lifecycle status"""
|
||||
state: str
|
||||
state_history: List[StateHistory]
|
||||
tasks: Dict[str, Any]
|
||||
health: bool
|
||||
|
||||
class TaskManager:
|
||||
"""Manages asyncio tasks"""
|
||||
|
||||
def __init__(self):
|
||||
TASK_TIMEOUT: ClassVar[int] = 30 # Default task timeout in seconds
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._tasks: Dict[str, asyncio.Task] = {}
|
||||
self._task_history: Dict[str, Dict[str, Any]] = {}
|
||||
self._task_history: Dict[str, TaskHistory] = {}
|
||||
|
||||
async def create_task(
|
||||
self,
|
||||
name: str,
|
||||
coro,
|
||||
callback: Optional[Callable] = None
|
||||
coro: Callable[..., Any],
|
||||
callback: Optional[Callable[[asyncio.Task], None]] = None,
|
||||
timeout: Optional[float] = None
|
||||
) -> asyncio.Task:
|
||||
"""Create and track a task"""
|
||||
task = asyncio.create_task(coro)
|
||||
self._tasks[name] = task
|
||||
self._task_history[name] = {
|
||||
"start_time": datetime.utcnow(),
|
||||
"status": "running"
|
||||
}
|
||||
"""
|
||||
Create and track a task.
|
||||
|
||||
Args:
|
||||
name: Task name
|
||||
coro: Coroutine to run
|
||||
callback: Optional completion callback
|
||||
timeout: Optional timeout in seconds
|
||||
|
||||
Returns:
|
||||
Created task
|
||||
|
||||
Raises:
|
||||
ComponentError: If task creation fails
|
||||
"""
|
||||
try:
|
||||
task = asyncio.create_task(coro)
|
||||
self._tasks[name] = task
|
||||
self._task_history[name] = TaskHistory(
|
||||
start_time=datetime.utcnow().isoformat(),
|
||||
end_time=None,
|
||||
status=TaskStatus.RUNNING.name,
|
||||
error=None,
|
||||
duration=0.0
|
||||
)
|
||||
|
||||
if callback:
|
||||
task.add_done_callback(lambda t: self._handle_completion(name, t, callback))
|
||||
else:
|
||||
task.add_done_callback(lambda t: self._handle_completion(name, t))
|
||||
if timeout:
|
||||
asyncio.create_task(self._handle_timeout(name, task, timeout))
|
||||
|
||||
return task
|
||||
if callback:
|
||||
task.add_done_callback(lambda t: self._handle_completion(name, t, callback))
|
||||
else:
|
||||
task.add_done_callback(lambda t: self._handle_completion(name, t))
|
||||
|
||||
return task
|
||||
|
||||
except Exception as e:
|
||||
error = f"Failed to create task {name}: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"TaskManager",
|
||||
"create_task",
|
||||
{"task_name": name},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
async def _handle_timeout(
|
||||
self,
|
||||
name: str,
|
||||
task: asyncio.Task,
|
||||
timeout: float
|
||||
) -> None:
|
||||
"""Handle task timeout"""
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.shield(task), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
if not task.done():
|
||||
logger.warning(f"Task {name} timed out after {timeout}s")
|
||||
task.cancel()
|
||||
self._update_task_history(
|
||||
name,
|
||||
TaskStatus.FAILED,
|
||||
f"Task timed out after {timeout}s"
|
||||
)
|
||||
|
||||
def _handle_completion(
|
||||
self,
|
||||
name: str,
|
||||
task: asyncio.Task,
|
||||
callback: Optional[Callable] = None
|
||||
callback: Optional[Callable[[asyncio.Task], None]] = None
|
||||
) -> None:
|
||||
"""Handle task completion"""
|
||||
try:
|
||||
task.result() # Raises exception if task failed
|
||||
status = "completed"
|
||||
status = TaskStatus.COMPLETED
|
||||
error = None
|
||||
except asyncio.CancelledError:
|
||||
status = "cancelled"
|
||||
status = TaskStatus.CANCELLED
|
||||
error = "Task was cancelled"
|
||||
except Exception as e:
|
||||
status = "failed"
|
||||
logger.error(f"Task {name} failed: {e}")
|
||||
status = TaskStatus.FAILED
|
||||
error = str(e)
|
||||
logger.error(f"Task {name} failed: {error}", exc_info=True)
|
||||
|
||||
self._task_history[name].update({
|
||||
"end_time": datetime.utcnow(),
|
||||
"status": status
|
||||
})
|
||||
self._update_task_history(name, status, error)
|
||||
|
||||
if callback:
|
||||
try:
|
||||
callback(task)
|
||||
except Exception as e:
|
||||
logger.error(f"Task callback error for {name}: {e}")
|
||||
logger.error(f"Task callback error for {name}: {e}", exc_info=True)
|
||||
|
||||
self._tasks.pop(name, None)
|
||||
|
||||
def _update_task_history(
|
||||
self,
|
||||
name: str,
|
||||
status: TaskStatus,
|
||||
error: Optional[str] = None
|
||||
) -> None:
|
||||
"""Update task history entry"""
|
||||
if name in self._task_history:
|
||||
end_time = datetime.utcnow()
|
||||
start_time = datetime.fromisoformat(self._task_history[name]["start_time"])
|
||||
self._task_history[name].update({
|
||||
"end_time": end_time.isoformat(),
|
||||
"status": status.name,
|
||||
"error": error,
|
||||
"duration": (end_time - start_time).total_seconds()
|
||||
})
|
||||
|
||||
async def cancel_task(self, name: str) -> None:
|
||||
"""Cancel a specific task"""
|
||||
"""
|
||||
Cancel a specific task.
|
||||
|
||||
Args:
|
||||
name: Task name to cancel
|
||||
"""
|
||||
if task := self._tasks.get(name):
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
@@ -87,7 +200,7 @@ class TaskManager:
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error cancelling task {name}: {e}")
|
||||
logger.error(f"Error cancelling task {name}: {e}", exc_info=True)
|
||||
|
||||
async def cancel_all_tasks(self) -> None:
|
||||
"""Cancel all tracked tasks"""
|
||||
@@ -95,7 +208,12 @@ class TaskManager:
|
||||
await self.cancel_task(name)
|
||||
|
||||
def get_task_status(self) -> Dict[str, Any]:
|
||||
"""Get status of all tasks"""
|
||||
"""
|
||||
Get status of all tasks.
|
||||
|
||||
Returns:
|
||||
Dictionary containing task status information
|
||||
"""
|
||||
return {
|
||||
"active_tasks": list(self._tasks.keys()),
|
||||
"history": self._task_history.copy()
|
||||
@@ -104,42 +222,80 @@ class TaskManager:
|
||||
class StateTracker:
|
||||
"""Tracks lifecycle state and transitions"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.state = LifecycleState.UNINITIALIZED
|
||||
self.state_history: List[Dict[str, Any]] = []
|
||||
self.state_history: List[StateHistory] = []
|
||||
self._record_state()
|
||||
|
||||
def set_state(self, state: LifecycleState) -> None:
|
||||
"""Set current state"""
|
||||
def set_state(
|
||||
self,
|
||||
state: LifecycleState,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Set current state.
|
||||
|
||||
Args:
|
||||
state: New state
|
||||
details: Optional state transition details
|
||||
"""
|
||||
self.state = state
|
||||
self._record_state()
|
||||
self._record_state(details)
|
||||
|
||||
def _record_state(self) -> None:
|
||||
def _record_state(
|
||||
self,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""Record state transition"""
|
||||
self.state_history.append({
|
||||
"state": self.state.value,
|
||||
"timestamp": datetime.utcnow()
|
||||
})
|
||||
now = datetime.utcnow()
|
||||
duration = 0.0
|
||||
if self.state_history:
|
||||
last_state = datetime.fromisoformat(self.state_history[-1]["timestamp"])
|
||||
duration = (now - last_state).total_seconds()
|
||||
|
||||
def get_state_history(self) -> List[Dict[str, Any]]:
|
||||
self.state_history.append(StateHistory(
|
||||
state=self.state.name,
|
||||
timestamp=now.isoformat(),
|
||||
duration=duration,
|
||||
details=details
|
||||
))
|
||||
|
||||
def get_state_history(self) -> List[StateHistory]:
|
||||
"""Get state transition history"""
|
||||
return self.state_history.copy()
|
||||
|
||||
class LifecycleManager:
|
||||
"""Manages the lifecycle of the VideoArchiver cog"""
|
||||
|
||||
def __init__(self, cog):
|
||||
INIT_TIMEOUT: ClassVar[int] = 60 # 1 minute timeout for initialization
|
||||
UNLOAD_TIMEOUT: ClassVar[int] = 30 # 30 seconds timeout for unloading
|
||||
CLEANUP_TIMEOUT: ClassVar[int] = 15 # 15 seconds timeout for cleanup
|
||||
|
||||
def __init__(self, cog: Any) -> None:
|
||||
self.cog = cog
|
||||
self.task_manager = TaskManager()
|
||||
self.state_tracker = StateTracker()
|
||||
self._cleanup_handlers: Set[Callable] = set()
|
||||
|
||||
def register_cleanup_handler(self, handler: Callable) -> None:
|
||||
"""Register a cleanup handler"""
|
||||
def register_cleanup_handler(
|
||||
self,
|
||||
handler: Union[Callable[[], None], Callable[[], Any]]
|
||||
) -> None:
|
||||
"""
|
||||
Register a cleanup handler.
|
||||
|
||||
Args:
|
||||
handler: Cleanup handler function
|
||||
"""
|
||||
self._cleanup_handlers.add(handler)
|
||||
|
||||
async def initialize_cog(self) -> None:
|
||||
"""Initialize all components with proper error handling"""
|
||||
"""
|
||||
Initialize all components with proper error handling.
|
||||
|
||||
Raises:
|
||||
ComponentError: If initialization fails
|
||||
"""
|
||||
try:
|
||||
# Initialize components in sequence
|
||||
await self.cog.component_manager.initialize_components()
|
||||
@@ -149,24 +305,47 @@ class LifecycleManager:
|
||||
logger.info("VideoArchiver initialization completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during initialization: {str(e)}")
|
||||
error = f"Error during initialization: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
await cleanup_resources(self.cog)
|
||||
raise
|
||||
raise ComponentError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"LifecycleManager",
|
||||
"initialize_cog",
|
||||
None,
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
def init_callback(self, task: asyncio.Task) -> None:
|
||||
"""Handle initialization task completion"""
|
||||
try:
|
||||
task.result()
|
||||
logger.info("Initialization completed successfully")
|
||||
self.state_tracker.set_state(LifecycleState.READY)
|
||||
except asyncio.CancelledError:
|
||||
logger.warning("Initialization was cancelled")
|
||||
self.state_tracker.set_state(
|
||||
LifecycleState.ERROR,
|
||||
{"reason": "cancelled"}
|
||||
)
|
||||
asyncio.create_task(cleanup_resources(self.cog))
|
||||
except Exception as e:
|
||||
logger.error(f"Initialization failed: {str(e)}\n{traceback.format_exc()}")
|
||||
logger.error(f"Initialization failed: {str(e)}", exc_info=True)
|
||||
self.state_tracker.set_state(
|
||||
LifecycleState.ERROR,
|
||||
{"error": str(e)}
|
||||
)
|
||||
asyncio.create_task(cleanup_resources(self.cog))
|
||||
|
||||
async def handle_load(self) -> None:
|
||||
"""Handle cog loading without blocking"""
|
||||
"""
|
||||
Handle cog loading without blocking.
|
||||
|
||||
Raises:
|
||||
VideoArchiverError: If load fails
|
||||
"""
|
||||
try:
|
||||
self.state_tracker.set_state(LifecycleState.INITIALIZING)
|
||||
|
||||
@@ -174,7 +353,8 @@ class LifecycleManager:
|
||||
await self.task_manager.create_task(
|
||||
"initialization",
|
||||
self.initialize_cog(),
|
||||
self.init_callback
|
||||
self.init_callback,
|
||||
timeout=self.INIT_TIMEOUT
|
||||
)
|
||||
logger.info("Initialization started in background")
|
||||
|
||||
@@ -184,14 +364,27 @@ class LifecycleManager:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
force_cleanup_resources(self.cog),
|
||||
timeout=15 # CLEANUP_TIMEOUT
|
||||
timeout=self.CLEANUP_TIMEOUT
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Force cleanup during load error timed out")
|
||||
raise VideoArchiverError(f"Error during cog load: {str(e)}")
|
||||
raise VideoArchiverError(
|
||||
f"Error during cog load: {str(e)}",
|
||||
context=ErrorContext(
|
||||
"LifecycleManager",
|
||||
"handle_load",
|
||||
None,
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
async def handle_unload(self) -> None:
|
||||
"""Clean up when cog is unloaded"""
|
||||
"""
|
||||
Clean up when cog is unloaded.
|
||||
|
||||
Raises:
|
||||
CleanupError: If cleanup fails
|
||||
"""
|
||||
self.state_tracker.set_state(LifecycleState.UNLOADING)
|
||||
|
||||
try:
|
||||
@@ -205,9 +398,10 @@ class LifecycleManager:
|
||||
try:
|
||||
cleanup_task = await self.task_manager.create_task(
|
||||
"cleanup",
|
||||
cleanup_resources(self.cog)
|
||||
cleanup_resources(self.cog),
|
||||
timeout=self.UNLOAD_TIMEOUT
|
||||
)
|
||||
await asyncio.wait_for(cleanup_task, timeout=30) # UNLOAD_TIMEOUT
|
||||
await cleanup_task
|
||||
logger.info("Normal cleanup completed")
|
||||
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
@@ -220,17 +414,50 @@ class LifecycleManager:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
force_cleanup_resources(self.cog),
|
||||
timeout=15 # CLEANUP_TIMEOUT
|
||||
timeout=self.CLEANUP_TIMEOUT
|
||||
)
|
||||
logger.info("Force cleanup completed")
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Force cleanup timed out")
|
||||
error = "Force cleanup timed out"
|
||||
logger.error(error)
|
||||
raise CleanupError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"LifecycleManager",
|
||||
"handle_unload",
|
||||
None,
|
||||
ErrorSeverity.CRITICAL
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during force cleanup: {str(e)}")
|
||||
error = f"Error during force cleanup: {str(e)}"
|
||||
logger.error(error)
|
||||
raise CleanupError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"LifecycleManager",
|
||||
"handle_unload",
|
||||
None,
|
||||
ErrorSeverity.CRITICAL
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cog unload: {str(e)}")
|
||||
self.state_tracker.set_state(LifecycleState.ERROR)
|
||||
error = f"Error during cog unload: {str(e)}"
|
||||
logger.error(error, exc_info=True)
|
||||
self.state_tracker.set_state(
|
||||
LifecycleState.ERROR,
|
||||
{"error": str(e)}
|
||||
)
|
||||
raise CleanupError(
|
||||
error,
|
||||
context=ErrorContext(
|
||||
"LifecycleManager",
|
||||
"handle_unload",
|
||||
None,
|
||||
ErrorSeverity.CRITICAL
|
||||
)
|
||||
)
|
||||
finally:
|
||||
# Clear all references
|
||||
await self._cleanup_references()
|
||||
@@ -244,7 +471,7 @@ class LifecycleManager:
|
||||
else:
|
||||
handler()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in cleanup handler: {e}")
|
||||
logger.error(f"Error in cleanup handler: {e}", exc_info=True)
|
||||
|
||||
async def _cleanup_references(self) -> None:
|
||||
"""Clean up all references"""
|
||||
@@ -257,10 +484,16 @@ class LifecycleManager:
|
||||
self.cog.components.clear()
|
||||
self.cog.db = None
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get current lifecycle status"""
|
||||
return {
|
||||
"state": self.state_tracker.state.value,
|
||||
"state_history": self.state_tracker.get_state_history(),
|
||||
"tasks": self.task_manager.get_task_status()
|
||||
}
|
||||
def get_status(self) -> LifecycleStatus:
|
||||
"""
|
||||
Get current lifecycle status.
|
||||
|
||||
Returns:
|
||||
Dictionary containing lifecycle status information
|
||||
"""
|
||||
return LifecycleStatus(
|
||||
state=self.state_tracker.state.name,
|
||||
state_history=self.state_tracker.get_state_history(),
|
||||
tasks=self.task_manager.get_task_status(),
|
||||
health=self.state_tracker.state == LifecycleState.READY
|
||||
)
|
||||
|
||||
@@ -1,57 +1,116 @@
|
||||
"""Module for handling command responses"""
|
||||
|
||||
import logging
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Union, Dict, Any, TypedDict, ClassVar
|
||||
from datetime import datetime
|
||||
import discord
|
||||
from typing import Optional, Union, Dict, Any
|
||||
from redbot.core.commands import Context
|
||||
|
||||
from ..utils.exceptions import ErrorSeverity
|
||||
|
||||
logger = logging.getLogger("VideoArchiver")
|
||||
|
||||
class ResponseType(Enum):
|
||||
"""Types of responses"""
|
||||
NORMAL = auto()
|
||||
SUCCESS = auto()
|
||||
ERROR = auto()
|
||||
WARNING = auto()
|
||||
INFO = auto()
|
||||
DEBUG = auto()
|
||||
|
||||
class ResponseTheme(TypedDict):
|
||||
"""Type definition for response theme"""
|
||||
emoji: str
|
||||
color: discord.Color
|
||||
|
||||
class ResponseFormat(TypedDict):
|
||||
"""Type definition for formatted response"""
|
||||
content: str
|
||||
color: discord.Color
|
||||
timestamp: str
|
||||
|
||||
class ResponseFormatter:
|
||||
"""Formats responses for consistency"""
|
||||
|
||||
@staticmethod
|
||||
def format_success(message: str) -> Dict[str, Any]:
|
||||
"""Format a success message"""
|
||||
return {
|
||||
"content": f"✅ {message}",
|
||||
"color": discord.Color.green()
|
||||
}
|
||||
THEMES: ClassVar[Dict[ResponseType, ResponseTheme]] = {
|
||||
ResponseType.SUCCESS: ResponseTheme(emoji="✅", color=discord.Color.green()),
|
||||
ResponseType.ERROR: ResponseTheme(emoji="❌", color=discord.Color.red()),
|
||||
ResponseType.WARNING: ResponseTheme(emoji="⚠️", color=discord.Color.gold()),
|
||||
ResponseType.INFO: ResponseTheme(emoji="ℹ️", color=discord.Color.blue()),
|
||||
ResponseType.DEBUG: ResponseTheme(emoji="🔧", color=discord.Color.greyple())
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_error(message: str) -> Dict[str, Any]:
|
||||
"""Format an error message"""
|
||||
return {
|
||||
"content": f"❌ {message}",
|
||||
"color": discord.Color.red()
|
||||
}
|
||||
SEVERITY_MAPPING: ClassVar[Dict[ErrorSeverity, ResponseType]] = {
|
||||
ErrorSeverity.LOW: ResponseType.INFO,
|
||||
ErrorSeverity.MEDIUM: ResponseType.WARNING,
|
||||
ErrorSeverity.HIGH: ResponseType.ERROR,
|
||||
ErrorSeverity.CRITICAL: ResponseType.ERROR
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_warning(message: str) -> Dict[str, Any]:
|
||||
"""Format a warning message"""
|
||||
return {
|
||||
"content": f"⚠️ {message}",
|
||||
"color": discord.Color.yellow()
|
||||
}
|
||||
@classmethod
|
||||
def format_response(
|
||||
cls,
|
||||
message: str,
|
||||
response_type: ResponseType = ResponseType.NORMAL
|
||||
) -> ResponseFormat:
|
||||
"""
|
||||
Format a response message.
|
||||
|
||||
Args:
|
||||
message: Message to format
|
||||
response_type: Type of response
|
||||
|
||||
Returns:
|
||||
Formatted response dictionary
|
||||
"""
|
||||
theme = cls.THEMES.get(response_type)
|
||||
if theme:
|
||||
return ResponseFormat(
|
||||
content=f"{theme['emoji']} {message}",
|
||||
color=theme['color'],
|
||||
timestamp=datetime.utcnow().isoformat()
|
||||
)
|
||||
return ResponseFormat(
|
||||
content=message,
|
||||
color=discord.Color.default(),
|
||||
timestamp=datetime.utcnow().isoformat()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_info(message: str) -> Dict[str, Any]:
|
||||
"""Format an info message"""
|
||||
return {
|
||||
"content": f"ℹ️ {message}",
|
||||
"color": discord.Color.blue()
|
||||
}
|
||||
@classmethod
|
||||
def get_response_type(cls, severity: ErrorSeverity) -> ResponseType:
|
||||
"""
|
||||
Get response type for error severity.
|
||||
|
||||
Args:
|
||||
severity: Error severity level
|
||||
|
||||
Returns:
|
||||
Appropriate response type
|
||||
"""
|
||||
return cls.SEVERITY_MAPPING.get(severity, ResponseType.ERROR)
|
||||
|
||||
class InteractionHandler:
|
||||
"""Handles slash command interactions"""
|
||||
|
||||
@staticmethod
|
||||
async def send_initial_response(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
content: Optional[str] = None,
|
||||
embed: Optional[discord.Embed] = None
|
||||
) -> bool:
|
||||
"""Send initial interaction response"""
|
||||
"""
|
||||
Send initial interaction response.
|
||||
|
||||
Args:
|
||||
interaction: Discord interaction
|
||||
content: Optional message content
|
||||
embed: Optional embed
|
||||
|
||||
Returns:
|
||||
True if response was sent successfully
|
||||
"""
|
||||
try:
|
||||
if not interaction.response.is_done():
|
||||
if embed:
|
||||
@@ -61,16 +120,26 @@ class InteractionHandler:
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending initial interaction response: {e}")
|
||||
logger.error(f"Error sending initial interaction response: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def send_followup(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
content: Optional[str] = None,
|
||||
embed: Optional[discord.Embed] = None
|
||||
) -> bool:
|
||||
"""Send interaction followup"""
|
||||
"""
|
||||
Send interaction followup.
|
||||
|
||||
Args:
|
||||
interaction: Discord interaction
|
||||
content: Optional message content
|
||||
embed: Optional embed
|
||||
|
||||
Returns:
|
||||
True if followup was sent successfully
|
||||
"""
|
||||
try:
|
||||
if embed:
|
||||
await interaction.followup.send(content=content, embed=embed)
|
||||
@@ -78,13 +147,13 @@ class InteractionHandler:
|
||||
await interaction.followup.send(content=content)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending interaction followup: {e}")
|
||||
logger.error(f"Error sending interaction followup: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
class ResponseManager:
|
||||
"""Manages command responses"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.formatter = ResponseFormatter()
|
||||
self.interaction_handler = InteractionHandler()
|
||||
|
||||
@@ -93,25 +162,37 @@ class ResponseManager:
|
||||
ctx: Context,
|
||||
content: Optional[str] = None,
|
||||
embed: Optional[discord.Embed] = None,
|
||||
response_type: str = "normal"
|
||||
response_type: Union[ResponseType, str, ErrorSeverity] = ResponseType.NORMAL
|
||||
) -> None:
|
||||
"""Send a response to a command
|
||||
"""
|
||||
Send a response to a command.
|
||||
|
||||
Args:
|
||||
ctx: Command context
|
||||
content: Optional message content
|
||||
embed: Optional embed
|
||||
response_type: Type of response (normal, success, error, warning, info)
|
||||
response_type: Type of response or error severity
|
||||
"""
|
||||
try:
|
||||
# Format response if type specified
|
||||
if response_type != "normal":
|
||||
format_method = getattr(self.formatter, f"format_{response_type}", None)
|
||||
if format_method and content:
|
||||
formatted = format_method(content)
|
||||
content = formatted["content"]
|
||||
if not embed:
|
||||
embed = discord.Embed(color=formatted["color"])
|
||||
# Convert string response type to enum
|
||||
if isinstance(response_type, str):
|
||||
try:
|
||||
response_type = ResponseType[response_type.upper()]
|
||||
except KeyError:
|
||||
response_type = ResponseType.NORMAL
|
||||
# Convert error severity to response type
|
||||
elif isinstance(response_type, ErrorSeverity):
|
||||
response_type = self.formatter.get_response_type(response_type)
|
||||
|
||||
# Format response
|
||||
if response_type != ResponseType.NORMAL and content:
|
||||
formatted = self.formatter.format_response(content, response_type)
|
||||
content = formatted["content"]
|
||||
if not embed:
|
||||
embed = discord.Embed(
|
||||
color=formatted["color"],
|
||||
timestamp=datetime.fromisoformat(formatted["timestamp"])
|
||||
)
|
||||
|
||||
# Handle response
|
||||
if self._is_interaction(ctx):
|
||||
@@ -120,7 +201,7 @@ class ResponseManager:
|
||||
await self._handle_regular_response(ctx, content, embed)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending response: {e}")
|
||||
logger.error(f"Error sending response: {e}", exc_info=True)
|
||||
await self._send_fallback_response(ctx, content, embed)
|
||||
|
||||
def _is_interaction(self, ctx: Context) -> bool:
|
||||
@@ -151,7 +232,7 @@ class ResponseManager:
|
||||
await self._handle_regular_response(ctx, content, embed)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling interaction response: {e}")
|
||||
logger.error(f"Error handling interaction response: {e}", exc_info=True)
|
||||
await self._send_fallback_response(ctx, content, embed)
|
||||
|
||||
async def _handle_regular_response(
|
||||
@@ -167,7 +248,7 @@ class ResponseManager:
|
||||
else:
|
||||
await ctx.send(content=content)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending regular response: {e}")
|
||||
logger.error(f"Error sending regular response: {e}", exc_info=True)
|
||||
await self._send_fallback_response(ctx, content, embed)
|
||||
|
||||
async def _send_fallback_response(
|
||||
@@ -183,7 +264,7 @@ class ResponseManager:
|
||||
else:
|
||||
await ctx.send(content=content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send fallback response: {e}")
|
||||
logger.error(f"Failed to send fallback response: {e}", exc_info=True)
|
||||
|
||||
# Global response manager instance
|
||||
response_manager = ResponseManager()
|
||||
@@ -192,7 +273,15 @@ async def handle_response(
|
||||
ctx: Context,
|
||||
content: Optional[str] = None,
|
||||
embed: Optional[discord.Embed] = None,
|
||||
response_type: str = "normal"
|
||||
response_type: Union[ResponseType, str, ErrorSeverity] = ResponseType.NORMAL
|
||||
) -> None:
|
||||
"""Helper function to handle responses using the response manager"""
|
||||
"""
|
||||
Helper function to handle responses using the response manager.
|
||||
|
||||
Args:
|
||||
ctx: Command context
|
||||
content: Optional message content
|
||||
embed: Optional embed
|
||||
response_type: Type of response or error severity
|
||||
"""
|
||||
await response_manager.send_response(ctx, content, embed, response_type)
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"""Module for managing VideoArchiver settings"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, List, Optional, Union, TypedDict, ClassVar
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
|
||||
from ..utils.exceptions import (
|
||||
ConfigurationError,
|
||||
ErrorContext,
|
||||
ErrorSeverity
|
||||
)
|
||||
|
||||
class VideoFormat(Enum):
|
||||
"""Supported video formats"""
|
||||
@@ -17,133 +23,177 @@ class VideoQuality(Enum):
|
||||
HIGH = "high" # 1080p
|
||||
ULTRA = "ultra" # 4K
|
||||
|
||||
class SettingCategory(Enum):
|
||||
"""Setting categories"""
|
||||
GENERAL = auto()
|
||||
CHANNELS = auto()
|
||||
PERMISSIONS = auto()
|
||||
VIDEO = auto()
|
||||
MESSAGES = auto()
|
||||
PERFORMANCE = auto()
|
||||
FEATURES = auto()
|
||||
|
||||
class ValidationResult(TypedDict):
|
||||
"""Type definition for validation result"""
|
||||
valid: bool
|
||||
error: Optional[str]
|
||||
details: Dict[str, Any]
|
||||
|
||||
@dataclass
|
||||
class SettingDefinition:
|
||||
"""Defines a setting's properties"""
|
||||
name: str
|
||||
category: str
|
||||
category: SettingCategory
|
||||
default_value: Any
|
||||
description: str
|
||||
data_type: type
|
||||
required: bool = True
|
||||
min_value: Optional[int] = None
|
||||
max_value: Optional[int] = None
|
||||
min_value: Optional[Union[int, float]] = None
|
||||
max_value: Optional[Union[int, float]] = None
|
||||
choices: Optional[List[Any]] = None
|
||||
depends_on: Optional[str] = None
|
||||
validation_func: Optional[callable] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
class SettingCategory(Enum):
|
||||
"""Setting categories"""
|
||||
GENERAL = "general"
|
||||
CHANNELS = "channels"
|
||||
PERMISSIONS = "permissions"
|
||||
VIDEO = "video"
|
||||
MESSAGES = "messages"
|
||||
PERFORMANCE = "performance"
|
||||
FEATURES = "features"
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate setting definition"""
|
||||
if self.choices and self.default_value not in self.choices:
|
||||
raise ConfigurationError(
|
||||
f"Default value {self.default_value} not in choices {self.choices}",
|
||||
context=ErrorContext(
|
||||
"Settings",
|
||||
"definition_validation",
|
||||
{"setting": self.name},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
if self.min_value is not None and self.max_value is not None:
|
||||
if self.min_value > self.max_value:
|
||||
raise ConfigurationError(
|
||||
f"Min value {self.min_value} greater than max value {self.max_value}",
|
||||
context=ErrorContext(
|
||||
"Settings",
|
||||
"definition_validation",
|
||||
{"setting": self.name},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
class Settings:
|
||||
"""Manages VideoArchiver settings"""
|
||||
|
||||
# Setting definitions
|
||||
SETTINGS = {
|
||||
SETTINGS: ClassVar[Dict[str, SettingDefinition]] = {
|
||||
"enabled": SettingDefinition(
|
||||
name="enabled",
|
||||
category=SettingCategory.GENERAL.value,
|
||||
category=SettingCategory.GENERAL,
|
||||
default_value=False,
|
||||
description="Whether the archiver is enabled for this guild",
|
||||
data_type=bool
|
||||
),
|
||||
"archive_channel": SettingDefinition(
|
||||
name="archive_channel",
|
||||
category=SettingCategory.CHANNELS.value,
|
||||
category=SettingCategory.CHANNELS,
|
||||
default_value=None,
|
||||
description="Channel where archived videos are posted",
|
||||
data_type=int,
|
||||
required=False
|
||||
required=False,
|
||||
error_message="Archive channel must be a valid channel ID"
|
||||
),
|
||||
"log_channel": SettingDefinition(
|
||||
name="log_channel",
|
||||
category=SettingCategory.CHANNELS.value,
|
||||
category=SettingCategory.CHANNELS,
|
||||
default_value=None,
|
||||
description="Channel for logging archiver actions",
|
||||
data_type=int,
|
||||
required=False
|
||||
required=False,
|
||||
error_message="Log channel must be a valid channel ID"
|
||||
),
|
||||
"enabled_channels": SettingDefinition(
|
||||
name="enabled_channels",
|
||||
category=SettingCategory.CHANNELS.value,
|
||||
category=SettingCategory.CHANNELS,
|
||||
default_value=[],
|
||||
description="Channels to monitor (empty means all channels)",
|
||||
data_type=list
|
||||
data_type=list,
|
||||
error_message="Enabled channels must be a list of valid channel IDs"
|
||||
),
|
||||
"allowed_roles": SettingDefinition(
|
||||
name="allowed_roles",
|
||||
category=SettingCategory.PERMISSIONS.value,
|
||||
category=SettingCategory.PERMISSIONS,
|
||||
default_value=[],
|
||||
description="Roles allowed to use archiver (empty means all roles)",
|
||||
data_type=list
|
||||
data_type=list,
|
||||
error_message="Allowed roles must be a list of valid role IDs"
|
||||
),
|
||||
"video_format": SettingDefinition(
|
||||
name="video_format",
|
||||
category=SettingCategory.VIDEO.value,
|
||||
category=SettingCategory.VIDEO,
|
||||
default_value=VideoFormat.MP4.value,
|
||||
description="Format for archived videos",
|
||||
data_type=str,
|
||||
choices=[format.value for format in VideoFormat]
|
||||
choices=[format.value for format in VideoFormat],
|
||||
error_message=f"Video format must be one of: {', '.join(f.value for f in VideoFormat)}"
|
||||
),
|
||||
"video_quality": SettingDefinition(
|
||||
name="video_quality",
|
||||
category=SettingCategory.VIDEO.value,
|
||||
category=SettingCategory.VIDEO,
|
||||
default_value=VideoQuality.HIGH.value,
|
||||
description="Quality preset for archived videos",
|
||||
data_type=str,
|
||||
choices=[quality.value for quality in VideoQuality]
|
||||
choices=[quality.value for quality in VideoQuality],
|
||||
error_message=f"Video quality must be one of: {', '.join(q.value for q in VideoQuality)}"
|
||||
),
|
||||
"max_file_size": SettingDefinition(
|
||||
name="max_file_size",
|
||||
category=SettingCategory.VIDEO.value,
|
||||
category=SettingCategory.VIDEO,
|
||||
default_value=8,
|
||||
description="Maximum file size in MB",
|
||||
data_type=int,
|
||||
min_value=1,
|
||||
max_value=100
|
||||
max_value=100,
|
||||
error_message="Max file size must be between 1 and 100 MB"
|
||||
),
|
||||
"message_duration": SettingDefinition(
|
||||
name="message_duration",
|
||||
category=SettingCategory.MESSAGES.value,
|
||||
category=SettingCategory.MESSAGES,
|
||||
default_value=30,
|
||||
description="Duration to show status messages (seconds)",
|
||||
data_type=int,
|
||||
min_value=5,
|
||||
max_value=300
|
||||
max_value=300,
|
||||
error_message="Message duration must be between 5 and 300 seconds"
|
||||
),
|
||||
"message_template": SettingDefinition(
|
||||
name="message_template",
|
||||
category=SettingCategory.MESSAGES.value,
|
||||
category=SettingCategory.MESSAGES,
|
||||
default_value="{author} archived a video from {channel}",
|
||||
description="Template for archive messages",
|
||||
data_type=str
|
||||
data_type=str,
|
||||
error_message="Message template must contain {author} and {channel} placeholders"
|
||||
),
|
||||
"concurrent_downloads": SettingDefinition(
|
||||
name="concurrent_downloads",
|
||||
category=SettingCategory.PERFORMANCE.value,
|
||||
category=SettingCategory.PERFORMANCE,
|
||||
default_value=2,
|
||||
description="Maximum concurrent downloads",
|
||||
data_type=int,
|
||||
min_value=1,
|
||||
max_value=5
|
||||
max_value=5,
|
||||
error_message="Concurrent downloads must be between 1 and 5"
|
||||
),
|
||||
"enabled_sites": SettingDefinition(
|
||||
name="enabled_sites",
|
||||
category=SettingCategory.FEATURES.value,
|
||||
category=SettingCategory.FEATURES,
|
||||
default_value=None,
|
||||
description="Sites to enable archiving for (None means all sites)",
|
||||
data_type=list,
|
||||
required=False
|
||||
required=False,
|
||||
error_message="Enabled sites must be a list of valid site identifiers"
|
||||
),
|
||||
"use_database": SettingDefinition(
|
||||
name="use_database",
|
||||
category=SettingCategory.FEATURES.value,
|
||||
category=SettingCategory.FEATURES,
|
||||
default_value=False,
|
||||
description="Enable database tracking of archived videos",
|
||||
data_type=bool
|
||||
@@ -152,12 +202,28 @@ class Settings:
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, setting: str) -> Optional[SettingDefinition]:
|
||||
"""Get definition for a setting"""
|
||||
"""
|
||||
Get definition for a setting.
|
||||
|
||||
Args:
|
||||
setting: Setting name
|
||||
|
||||
Returns:
|
||||
Setting definition or None if not found
|
||||
"""
|
||||
return cls.SETTINGS.get(setting)
|
||||
|
||||
@classmethod
|
||||
def get_settings_by_category(cls, category: str) -> Dict[str, SettingDefinition]:
|
||||
"""Get all settings in a category"""
|
||||
def get_settings_by_category(cls, category: SettingCategory) -> Dict[str, SettingDefinition]:
|
||||
"""
|
||||
Get all settings in a category.
|
||||
|
||||
Args:
|
||||
category: Setting category
|
||||
|
||||
Returns:
|
||||
Dictionary of settings in the category
|
||||
"""
|
||||
return {
|
||||
name: definition
|
||||
for name, definition in cls.SETTINGS.items()
|
||||
@@ -165,36 +231,109 @@ class Settings:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate_setting(cls, setting: str, value: Any) -> bool:
|
||||
"""Validate a setting value"""
|
||||
def validate_setting(cls, setting: str, value: Any) -> ValidationResult:
|
||||
"""
|
||||
Validate a setting value.
|
||||
|
||||
Args:
|
||||
setting: Setting name
|
||||
value: Value to validate
|
||||
|
||||
Returns:
|
||||
Validation result dictionary
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If setting definition is not found
|
||||
"""
|
||||
definition = cls.get_setting_definition(setting)
|
||||
if not definition:
|
||||
return False
|
||||
raise ConfigurationError(
|
||||
f"Unknown setting: {setting}",
|
||||
context=ErrorContext(
|
||||
"Settings",
|
||||
"validation",
|
||||
{"setting": setting},
|
||||
ErrorSeverity.HIGH
|
||||
)
|
||||
)
|
||||
|
||||
details = {
|
||||
"setting": setting,
|
||||
"value": value,
|
||||
"type": type(value).__name__,
|
||||
"expected_type": definition.data_type.__name__
|
||||
}
|
||||
|
||||
# Check type
|
||||
if not isinstance(value, definition.data_type):
|
||||
return False
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
error=f"Invalid type: expected {definition.data_type.__name__}, got {type(value).__name__}",
|
||||
details=details
|
||||
)
|
||||
|
||||
# Check required
|
||||
if definition.required and value is None:
|
||||
return False
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
error="Required setting cannot be None",
|
||||
details=details
|
||||
)
|
||||
|
||||
# Check choices
|
||||
if definition.choices and value not in definition.choices:
|
||||
return False
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
error=f"Value must be one of: {', '.join(map(str, definition.choices))}",
|
||||
details=details
|
||||
)
|
||||
|
||||
# Check numeric bounds
|
||||
if isinstance(value, (int, float)):
|
||||
if definition.min_value is not None and value < definition.min_value:
|
||||
return False
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
error=f"Value must be at least {definition.min_value}",
|
||||
details=details
|
||||
)
|
||||
if definition.max_value is not None and value > definition.max_value:
|
||||
return False
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
error=f"Value must be at most {definition.max_value}",
|
||||
details=details
|
||||
)
|
||||
|
||||
return True
|
||||
# Custom validation
|
||||
if definition.validation_func:
|
||||
try:
|
||||
result = definition.validation_func(value)
|
||||
if not result:
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
error=definition.error_message or "Validation failed",
|
||||
details=details
|
||||
)
|
||||
except Exception as e:
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
error=str(e),
|
||||
details=details
|
||||
)
|
||||
|
||||
return ValidationResult(
|
||||
valid=True,
|
||||
error=None,
|
||||
details=details
|
||||
)
|
||||
|
||||
@property
|
||||
def default_guild_settings(self) -> Dict[str, Any]:
|
||||
"""Default settings for guild configuration"""
|
||||
"""
|
||||
Default settings for guild configuration.
|
||||
|
||||
Returns:
|
||||
Dictionary of default settings
|
||||
"""
|
||||
return {
|
||||
name: definition.default_value
|
||||
for name, definition in self.SETTINGS.items()
|
||||
@@ -202,14 +341,22 @@ class Settings:
|
||||
|
||||
@classmethod
|
||||
def get_setting_help(cls, setting: str) -> Optional[str]:
|
||||
"""Get help text for a setting"""
|
||||
"""
|
||||
Get help text for a setting.
|
||||
|
||||
Args:
|
||||
setting: Setting name
|
||||
|
||||
Returns:
|
||||
Help text or None if setting not found
|
||||
"""
|
||||
definition = cls.get_setting_definition(setting)
|
||||
if not definition:
|
||||
return None
|
||||
|
||||
help_text = [
|
||||
f"Setting: {definition.name}",
|
||||
f"Category: {definition.category}",
|
||||
f"Category: {definition.category.name}",
|
||||
f"Description: {definition.description}",
|
||||
f"Type: {definition.data_type.__name__}",
|
||||
f"Required: {definition.required}",
|
||||
@@ -224,5 +371,7 @@ class Settings:
|
||||
help_text.append(f"Maximum: {definition.max_value}")
|
||||
if definition.depends_on:
|
||||
help_text.append(f"Depends on: {definition.depends_on}")
|
||||
if definition.error_message:
|
||||
help_text.append(f"Error: {definition.error_message}")
|
||||
|
||||
return "\n".join(help_text)
|
||||
|
||||
Reference in New Issue
Block a user