- Add /weather command with smart team resolution and D20 rolling system - Add /charts command with autocomplete and category organization - Implement ChartService for JSON-based chart management - Add comprehensive test coverage for new commands - Update test factories with complete model fixtures - Enhance voice channel tracker with improved logging - Update PRE_LAUNCH_ROADMAP.md to reflect completed features - Minor improvements to imports and service initialization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
258 lines
7.1 KiB
Python
258 lines
7.1 KiB
Python
"""
|
|
Chart Service for managing gameplay charts and infographics.
|
|
|
|
This service handles loading, saving, and managing chart definitions
|
|
from the JSON configuration file.
|
|
"""
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, List, Any
|
|
from dataclasses import dataclass
|
|
|
|
from exceptions import BotException
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class Chart:
|
|
"""Represents a gameplay chart or infographic."""
|
|
key: str
|
|
name: str
|
|
category: str
|
|
description: str
|
|
urls: List[str]
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert chart to dictionary (excluding key)."""
|
|
return {
|
|
'name': self.name,
|
|
'category': self.category,
|
|
'description': self.description,
|
|
'urls': self.urls
|
|
}
|
|
|
|
|
|
class ChartService:
|
|
"""Service for managing gameplay charts and infographics."""
|
|
|
|
CHARTS_FILE = Path(__file__).parent.parent / 'storage' / 'charts.json'
|
|
|
|
def __init__(self):
|
|
"""Initialize the chart service."""
|
|
self._charts: Dict[str, Chart] = {}
|
|
self._categories: Dict[str, str] = {}
|
|
self._load_charts()
|
|
|
|
def _load_charts(self) -> None:
|
|
"""Load charts from JSON file."""
|
|
try:
|
|
if not self.CHARTS_FILE.exists():
|
|
logger.warning(f"Charts file not found: {self.CHARTS_FILE}")
|
|
self._charts = {}
|
|
self._categories = {}
|
|
return
|
|
|
|
with open(self.CHARTS_FILE, 'r') as f:
|
|
data = json.load(f)
|
|
|
|
# Load categories
|
|
self._categories = data.get('categories', {})
|
|
|
|
# Load charts
|
|
charts_data = data.get('charts', {})
|
|
for key, chart_data in charts_data.items():
|
|
self._charts[key] = Chart(
|
|
key=key,
|
|
name=chart_data['name'],
|
|
category=chart_data['category'],
|
|
description=chart_data.get('description', ''),
|
|
urls=chart_data.get('urls', [])
|
|
)
|
|
|
|
logger.info(f"Loaded {len(self._charts)} charts from {self.CHARTS_FILE}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to load charts: {e}", exc_info=True)
|
|
self._charts = {}
|
|
self._categories = {}
|
|
|
|
def _save_charts(self) -> None:
|
|
"""Save charts to JSON file."""
|
|
try:
|
|
# Ensure data directory exists
|
|
self.CHARTS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Build data structure
|
|
data = {
|
|
'charts': {
|
|
key: chart.to_dict()
|
|
for key, chart in self._charts.items()
|
|
},
|
|
'categories': self._categories
|
|
}
|
|
|
|
# Write to file
|
|
with open(self.CHARTS_FILE, 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
logger.info(f"Saved {len(self._charts)} charts to {self.CHARTS_FILE}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to save charts: {e}", exc_info=True)
|
|
raise BotException(f"Failed to save charts: {str(e)}")
|
|
|
|
def get_chart(self, chart_key: str) -> Optional[Chart]:
|
|
"""
|
|
Get a chart by its key.
|
|
|
|
Args:
|
|
chart_key: The chart key/identifier
|
|
|
|
Returns:
|
|
Chart object if found, None otherwise
|
|
"""
|
|
return self._charts.get(chart_key)
|
|
|
|
def get_all_charts(self) -> List[Chart]:
|
|
"""
|
|
Get all available charts.
|
|
|
|
Returns:
|
|
List of all Chart objects
|
|
"""
|
|
return list(self._charts.values())
|
|
|
|
def get_charts_by_category(self, category: str) -> List[Chart]:
|
|
"""
|
|
Get all charts in a specific category.
|
|
|
|
Args:
|
|
category: The category to filter by
|
|
|
|
Returns:
|
|
List of charts in the specified category
|
|
"""
|
|
return [
|
|
chart for chart in self._charts.values()
|
|
if chart.category == category
|
|
]
|
|
|
|
def get_chart_keys(self) -> List[str]:
|
|
"""
|
|
Get all chart keys for autocomplete.
|
|
|
|
Returns:
|
|
Sorted list of chart keys
|
|
"""
|
|
return sorted(self._charts.keys())
|
|
|
|
def get_categories(self) -> Dict[str, str]:
|
|
"""
|
|
Get all categories.
|
|
|
|
Returns:
|
|
Dictionary mapping category keys to display names
|
|
"""
|
|
return self._categories.copy()
|
|
|
|
def add_chart(self, key: str, name: str, category: str,
|
|
urls: List[str], description: str = "") -> None:
|
|
"""
|
|
Add a new chart.
|
|
|
|
Args:
|
|
key: Unique identifier for the chart
|
|
name: Display name for the chart
|
|
category: Category the chart belongs to
|
|
urls: List of image URLs for the chart
|
|
description: Optional description of the chart
|
|
|
|
Raises:
|
|
BotException: If chart key already exists
|
|
"""
|
|
if key in self._charts:
|
|
raise BotException(f"Chart '{key}' already exists")
|
|
|
|
self._charts[key] = Chart(
|
|
key=key,
|
|
name=name,
|
|
category=category,
|
|
description=description,
|
|
urls=urls
|
|
)
|
|
self._save_charts()
|
|
logger.info(f"Added chart: {key}")
|
|
|
|
def update_chart(self, key: str, name: Optional[str] = None,
|
|
category: Optional[str] = None, urls: Optional[List[str]] = None,
|
|
description: Optional[str] = None) -> None:
|
|
"""
|
|
Update an existing chart.
|
|
|
|
Args:
|
|
key: Chart key to update
|
|
name: New name (optional)
|
|
category: New category (optional)
|
|
urls: New URLs (optional)
|
|
description: New description (optional)
|
|
|
|
Raises:
|
|
BotException: If chart doesn't exist
|
|
"""
|
|
if key not in self._charts:
|
|
raise BotException(f"Chart '{key}' not found")
|
|
|
|
chart = self._charts[key]
|
|
|
|
if name is not None:
|
|
chart.name = name
|
|
if category is not None:
|
|
chart.category = category
|
|
if urls is not None:
|
|
chart.urls = urls
|
|
if description is not None:
|
|
chart.description = description
|
|
|
|
self._save_charts()
|
|
logger.info(f"Updated chart: {key}")
|
|
|
|
def remove_chart(self, key: str) -> None:
|
|
"""
|
|
Remove a chart.
|
|
|
|
Args:
|
|
key: Chart key to remove
|
|
|
|
Raises:
|
|
BotException: If chart doesn't exist
|
|
"""
|
|
if key not in self._charts:
|
|
raise BotException(f"Chart '{key}' not found")
|
|
|
|
del self._charts[key]
|
|
self._save_charts()
|
|
logger.info(f"Removed chart: {key}")
|
|
|
|
def reload_charts(self) -> None:
|
|
"""Reload charts from the JSON file."""
|
|
self._load_charts()
|
|
|
|
|
|
# Global chart service instance
|
|
_chart_service: Optional[ChartService] = None
|
|
|
|
|
|
def get_chart_service() -> ChartService:
|
|
"""
|
|
Get the global chart service instance.
|
|
|
|
Returns:
|
|
ChartService instance
|
|
"""
|
|
global _chart_service
|
|
if _chart_service is None:
|
|
_chart_service = ChartService()
|
|
return _chart_service
|