strat-gameplay-webapp/backend/app/services/position_rating_service.py
Cal Corum a4b99ee53e CLAUDE: Replace black and flake8 with ruff for formatting and linting
Migrated to ruff for faster, modern code formatting and linting:

Configuration changes:
- pyproject.toml: Added ruff 0.8.6, removed black/flake8
- Configured ruff with black-compatible formatting (88 chars)
- Enabled comprehensive linting rules (pycodestyle, pyflakes, isort,
  pyupgrade, bugbear, comprehensions, simplify, return)
- Updated CLAUDE.md: Changed code quality commands to use ruff

Code improvements (490 auto-fixes):
- Modernized type hints: List[T] → list[T], Dict[K,V] → dict[K,V],
  Optional[T] → T | None
- Sorted all imports (isort integration)
- Removed unused imports
- Fixed whitespace issues
- Reformatted 38 files for consistency

Bug fixes:
- app/core/play_resolver.py: Fixed type hint bug (any → Any)
- tests/unit/core/test_runner_advancement.py: Removed obsolete random mock

Testing:
- All 739 unit tests passing (100%)
- No regressions introduced

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:33:21 -06:00

187 lines
6.1 KiB
Python

"""
Position rating service with Redis caching.
Provides cached access to position ratings with automatic
expiration and fallback to API.
Author: Claude
Date: 2025-11-03
Phase: 3E-Final
"""
import json
import logging
from redis.exceptions import RedisError
from app.models.player_models import PositionRating
from app.services.pd_api_client import pd_api_client
logger = logging.getLogger(f"{__name__}.PositionRatingService")
# Redis key pattern: "position_ratings:{card_id}"
# TTL: 86400 seconds (24 hours)
REDIS_KEY_PREFIX = "position_ratings"
REDIS_TTL_SECONDS = 86400 # 24 hours
class PositionRatingService:
"""Service for position rating lookup with caching."""
def __init__(self, use_cache: bool = True):
"""
Initialize position rating service.
Args:
use_cache: Whether to use caching (default True)
"""
self.use_cache = use_cache
def _get_redis_key(self, card_id: int) -> str:
"""
Get Redis key for card ratings.
Args:
card_id: PD card ID
Returns:
Redis key string
"""
return f"{REDIS_KEY_PREFIX}:{card_id}"
async def get_ratings_for_card(
self, card_id: int, league_id: str
) -> list[PositionRating]:
"""
Get all position ratings for a card with Redis caching.
Args:
card_id: PD card ID
league_id: League identifier ('pd')
Returns:
List of PositionRating objects
"""
# Only cache for PD league
if league_id != "pd":
logger.debug(f"Skipping cache for non-PD league: {league_id}")
return []
# Try Redis cache first
if self.use_cache:
try:
from app.services import redis_client
if redis_client.is_connected:
redis_key = self._get_redis_key(card_id)
cached_json = await redis_client.client.get(redis_key)
if cached_json:
logger.debug(f"Redis cache hit for card {card_id}")
cached_data = json.loads(cached_json)
return [PositionRating(**data) for data in cached_data]
logger.debug(f"Redis cache miss for card {card_id}")
else:
logger.warning("Redis not connected, skipping cache")
except RedisError as e:
logger.warning(
f"Redis error for card {card_id}: {e}, falling back to API"
)
except Exception as e:
logger.error(f"Unexpected cache error for card {card_id}: {e}")
# Cache miss or error - fetch from API
logger.debug(f"Fetching ratings from API for card {card_id}")
try:
# Note: card_id maps to player_id in PD API terminology
ratings = await pd_api_client.get_position_ratings(player_id=card_id)
# Cache the results in Redis with TTL
if self.use_cache and ratings:
try:
from app.services import redis_client
if redis_client.is_connected:
redis_key = self._get_redis_key(card_id)
ratings_json = json.dumps([r.model_dump() for r in ratings])
await redis_client.client.setex(
redis_key, REDIS_TTL_SECONDS, ratings_json
)
logger.debug(
f"Cached {len(ratings)} ratings for card {card_id} (TTL: {REDIS_TTL_SECONDS}s)"
)
except RedisError as e:
logger.warning(f"Failed to cache ratings for card {card_id}: {e}")
return ratings
except Exception as e:
logger.error(f"Failed to fetch ratings for card {card_id}: {e}")
return [] # Return empty list on error
async def get_rating_for_position(
self, card_id: int, position: str, league_id: str
) -> PositionRating | None:
"""
Get rating for specific position.
Args:
card_id: PD card ID
position: Position code (SS, LF, etc.)
league_id: League identifier
Returns:
PositionRating if found, None otherwise
"""
ratings = await self.get_ratings_for_card(card_id, league_id)
for rating in ratings:
if rating.position == position:
return rating
logger.warning(f"No rating found for card {card_id} at position {position}")
return None
async def clear_cache(self, card_id: int | None = None) -> None:
"""
Clear Redis cache for position ratings.
Args:
card_id: If provided, clear only this card's cache.
If None, clear all position rating caches.
"""
try:
from app.services import redis_client
if not redis_client.is_connected:
logger.warning("Redis not connected, cannot clear cache")
return
if card_id is not None:
# Clear specific card
redis_key = self._get_redis_key(card_id)
await redis_client.client.delete(redis_key)
logger.info(f"Cleared cache for card {card_id}")
else:
# Clear all position rating caches
pattern = f"{REDIS_KEY_PREFIX}:*"
keys = []
async for key in redis_client.client.scan_iter(match=pattern):
keys.append(key)
if keys:
await redis_client.client.delete(*keys)
logger.info(f"Cleared {len(keys)} position rating cache entries")
else:
logger.info("No position rating cache entries to clear")
except RedisError as e:
logger.error(f"Failed to clear cache: {e}")
except Exception as e:
logger.error(f"Unexpected error clearing cache: {e}")
# Singleton instance
position_rating_service = PositionRatingService()