Fix linting and formatting issues
- Add missing imports: json, csv, io, model_to_dict - Remove unused imports: Any, Dict, Type, invalidate_cache - Remove redundant f-string prefixes from static error messages - Format code with black - All ruff and black checks pass - All 76 unit tests pass (9 skipped) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
408b187305
commit
2189aea8da
@ -3,20 +3,25 @@ Base Service Class - Dependency Injection Version
|
||||
Provides common functionality with configurable dependencies.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Any, Dict, TypeVar, Type
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
from .interfaces import AbstractPlayerRepository, AbstractTeamRepository, AbstractCacheService
|
||||
from .interfaces import (
|
||||
AbstractPlayerRepository,
|
||||
AbstractTeamRepository,
|
||||
AbstractCacheService,
|
||||
)
|
||||
from .mocks import MockCacheService
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
logger = logging.getLogger("discord_app")
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ServiceConfig:
|
||||
"""Configuration for service dependencies."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player_repo: Optional[AbstractPlayerRepository] = None,
|
||||
@ -34,10 +39,10 @@ _default_config = ServiceConfig()
|
||||
|
||||
class BaseService:
|
||||
"""Base class for all services with dependency injection support."""
|
||||
|
||||
|
||||
# Subclasses should override these
|
||||
cache_patterns = []
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[ServiceConfig] = None,
|
||||
@ -47,7 +52,7 @@ class BaseService:
|
||||
):
|
||||
"""
|
||||
Initialize service with dependencies.
|
||||
|
||||
|
||||
Args:
|
||||
config: Optional ServiceConfig containing all dependencies
|
||||
player_repo: Override for player repository
|
||||
@ -63,163 +68,170 @@ class BaseService:
|
||||
self._player_repo = player_repo
|
||||
self._team_repo = team_repo
|
||||
self._cache = cache
|
||||
|
||||
|
||||
# Lazy imports for defaults (avoids circular imports)
|
||||
self._using_defaults = (
|
||||
self._player_repo is None and
|
||||
self._team_repo is None and
|
||||
self._cache is None
|
||||
self._player_repo is None
|
||||
and self._team_repo is None
|
||||
and self._cache is None
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def player_repo(self) -> AbstractPlayerRepository:
|
||||
"""Get player repository, importing from db_engine if not set."""
|
||||
if self._player_repo is None:
|
||||
from ..db_engine import Player
|
||||
|
||||
class DefaultPlayerRepo:
|
||||
def select_season(self, season):
|
||||
return Player.select_season(season)
|
||||
|
||||
|
||||
def get_by_id(self, player_id):
|
||||
return Player.get_or_none(Player.id == player_id)
|
||||
|
||||
|
||||
def get_or_none(self, *conditions):
|
||||
return Player.get_or_none(*conditions)
|
||||
|
||||
|
||||
def update(self, data, *conditions):
|
||||
return Player.update(data).where(*conditions).execute()
|
||||
|
||||
|
||||
def insert_many(self, data):
|
||||
return Player.insert_many(data).execute()
|
||||
|
||||
|
||||
def delete_by_id(self, player_id):
|
||||
player = Player.get_by_id(player_id)
|
||||
if player:
|
||||
return player.delete_instance()
|
||||
return 0
|
||||
|
||||
|
||||
self._player_repo = DefaultPlayerRepo()
|
||||
return self._player_repo
|
||||
|
||||
|
||||
@property
|
||||
def team_repo(self) -> AbstractTeamRepository:
|
||||
"""Get team repository, importing from db_engine if not set."""
|
||||
if self._team_repo is None:
|
||||
from ..db_engine import Team
|
||||
|
||||
|
||||
class DefaultTeamRepo:
|
||||
def select_season(self, season):
|
||||
return Team.select_season(season)
|
||||
|
||||
|
||||
def get_by_id(self, team_id):
|
||||
return Team.get_by_id(team_id)
|
||||
|
||||
|
||||
def get_or_none(self, *conditions):
|
||||
return Team.get_or_none(*conditions)
|
||||
|
||||
|
||||
def update(self, data, *conditions):
|
||||
return Team.update(data).where(*conditions).execute()
|
||||
|
||||
|
||||
def insert_many(self, data):
|
||||
return Team.insert_many(data).execute()
|
||||
|
||||
|
||||
def delete_by_id(self, team_id):
|
||||
team = Team.get_by_id(team_id)
|
||||
if team:
|
||||
return team.delete_instance()
|
||||
return 0
|
||||
|
||||
|
||||
self._team_repo = DefaultTeamRepo()
|
||||
return self._team_repo
|
||||
|
||||
|
||||
@property
|
||||
def cache(self) -> AbstractCacheService:
|
||||
"""Get cache service, importing from dependencies if not set."""
|
||||
if self._cache is None:
|
||||
try:
|
||||
from ..dependencies import redis_client, invalidate_cache
|
||||
|
||||
from ..dependencies import redis_client
|
||||
|
||||
class DefaultCache:
|
||||
def get(self, key: str):
|
||||
if redis_client is None:
|
||||
return None
|
||||
return redis_client.get(key)
|
||||
|
||||
|
||||
def set(self, key: str, value: str, ttl: int = 300):
|
||||
if redis_client is None:
|
||||
return False
|
||||
redis_client.setex(key, ttl, value)
|
||||
return True
|
||||
|
||||
|
||||
def setex(self, key: str, ttl: int, value: str):
|
||||
return self.set(key, value, ttl)
|
||||
|
||||
|
||||
def keys(self, pattern: str):
|
||||
if redis_client is None:
|
||||
return []
|
||||
return redis_client.keys(pattern)
|
||||
|
||||
|
||||
def delete(self, *keys: str):
|
||||
if redis_client is None:
|
||||
return 0
|
||||
return redis_client.delete(*keys)
|
||||
|
||||
|
||||
def invalidate_pattern(self, pattern: str):
|
||||
if redis_client is None:
|
||||
return 0
|
||||
keys = self.keys(pattern)
|
||||
return self.delete(*keys)
|
||||
|
||||
|
||||
def exists(self, key: str):
|
||||
if redis_client is None:
|
||||
return False
|
||||
return redis_client.exists(key)
|
||||
|
||||
|
||||
self._cache = DefaultCache()
|
||||
except ImportError:
|
||||
# Fall back to mock if dependencies not available
|
||||
self._cache = MockCacheService()
|
||||
|
||||
|
||||
return self._cache
|
||||
|
||||
|
||||
def close_db(self):
|
||||
"""Safely close database connection (for non-injected repos)."""
|
||||
if self._using_defaults:
|
||||
try:
|
||||
from ..db_engine import db
|
||||
|
||||
db.close()
|
||||
except Exception:
|
||||
pass # Connection may already be closed
|
||||
|
||||
|
||||
def invalidate_cache_for(self, entity_type: str, entity_id: Optional[int] = None):
|
||||
"""Invalidate cache entries for an entity."""
|
||||
if entity_id:
|
||||
self.cache.invalidate_pattern(f"{entity_type}*{entity_id}*")
|
||||
else:
|
||||
self.cache.invalidate_pattern(f"{entity_type}*")
|
||||
|
||||
|
||||
def invalidate_related_cache(self, patterns: list):
|
||||
"""Invalidate multiple cache patterns."""
|
||||
for pattern in patterns:
|
||||
self.cache.invalidate_pattern(pattern)
|
||||
|
||||
def handle_error(self, operation: str, error: Exception, rethrow: bool = True) -> dict:
|
||||
|
||||
def handle_error(
|
||||
self, operation: str, error: Exception, rethrow: bool = True
|
||||
) -> dict:
|
||||
"""Handle errors consistently."""
|
||||
logger.error(f"{operation}: {error}")
|
||||
if rethrow:
|
||||
try:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=500, detail=f"{operation}: {str(error)}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"{operation}: {str(error)}"
|
||||
)
|
||||
except ImportError:
|
||||
# For testing without FastAPI
|
||||
raise RuntimeError(f"{operation}: {str(error)}")
|
||||
return {"error": operation, "detail": str(error)}
|
||||
|
||||
|
||||
@classmethod
|
||||
def log_error(cls, operation: str, error: Exception) -> None:
|
||||
"""Class method for logging errors without needing an instance."""
|
||||
logger.error(f"{operation}: {error}")
|
||||
|
||||
|
||||
def require_auth(self, token: str) -> bool:
|
||||
"""Validate authentication token."""
|
||||
try:
|
||||
@ -227,56 +239,60 @@ class BaseService:
|
||||
from ..dependencies import valid_token
|
||||
|
||||
if not valid_token(token):
|
||||
logger.warning(f"Unauthorized access attempt with token: {token[:10]}...")
|
||||
logger.warning(
|
||||
f"Unauthorized access attempt with token: {token[:10]}..."
|
||||
)
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
except ImportError:
|
||||
# For testing without FastAPI - accept "valid_token" as test token
|
||||
if token != "valid_token":
|
||||
logger.warning(f"Unauthorized access attempt with token: {token[:10] if len(token) >= 10 else token}...")
|
||||
logger.warning(
|
||||
f"Unauthorized access attempt with token: {token[:10] if len(token) >= 10 else token}..."
|
||||
)
|
||||
error = RuntimeError("Unauthorized")
|
||||
error.status_code = 401 # Add status_code for test compatibility
|
||||
raise error
|
||||
return True
|
||||
|
||||
|
||||
def format_csv_response(self, headers: list, rows: list) -> str:
|
||||
"""Format data as CSV."""
|
||||
from pandas import DataFrame
|
||||
|
||||
all_data = [headers] + rows
|
||||
return DataFrame(all_data).to_csv(header=False, index=False)
|
||||
|
||||
|
||||
def parse_query_params(self, params: dict, remove_none: bool = True) -> dict:
|
||||
"""Parse and clean query parameters."""
|
||||
if remove_none:
|
||||
return {k: v for k, v in params.items() if v is not None and v != [] and v != ""}
|
||||
return {
|
||||
k: v for k, v in params.items() if v is not None and v != [] and v != ""
|
||||
}
|
||||
return params
|
||||
|
||||
def with_cache(
|
||||
self,
|
||||
key: str,
|
||||
ttl: int = 300,
|
||||
fallback: Optional[callable] = None
|
||||
):
|
||||
|
||||
def with_cache(self, key: str, ttl: int = 300, fallback: Optional[callable] = None):
|
||||
"""
|
||||
Decorator-style cache wrapper for methods.
|
||||
|
||||
|
||||
Usage:
|
||||
@service.with_cache("player:123", ttl=600)
|
||||
def get_player(self):
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Try cache first
|
||||
cached = self.cache.get(key)
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
|
||||
|
||||
# Execute and cache result
|
||||
result = func(*args, **kwargs)
|
||||
if result is not None:
|
||||
import json
|
||||
self.cache.set(key, json.dumps(result, default=str), ttl)
|
||||
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@ -3,6 +3,8 @@ Player Service - Dependency Injection Version
|
||||
Business logic for player operations with injectable dependencies.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import io
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
||||
|
||||
@ -27,18 +29,14 @@ except ImportError:
|
||||
self.detail = detail
|
||||
super().__init__(detail)
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
logger = logging.getLogger("discord_app")
|
||||
|
||||
|
||||
class PlayerService(BaseService):
|
||||
"""Service for player-related operations with dependency injection."""
|
||||
|
||||
cache_patterns = [
|
||||
"players*",
|
||||
"players-search*",
|
||||
"player*",
|
||||
"team-roster*"
|
||||
]
|
||||
cache_patterns = ["players*", "players-search*", "player*", "team-roster*"]
|
||||
|
||||
# Class-level repository for dependency injection
|
||||
_injected_repo: Optional[AbstractPlayerRepository] = None
|
||||
@ -46,8 +44,8 @@ class PlayerService(BaseService):
|
||||
def __init__(
|
||||
self,
|
||||
player_repo: Optional[AbstractPlayerRepository] = None,
|
||||
config: Optional['ServiceConfig'] = None,
|
||||
**kwargs
|
||||
config: Optional["ServiceConfig"] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize PlayerService with optional repository.
|
||||
@ -75,10 +73,10 @@ class PlayerService(BaseService):
|
||||
return cls._get_real_repo()
|
||||
|
||||
@classmethod
|
||||
def _get_real_repo(cls) -> 'RealPlayerRepository':
|
||||
def _get_real_repo(cls) -> "RealPlayerRepository":
|
||||
"""Get a real DB repository for production use."""
|
||||
return RealPlayerRepository(Player)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_players(
|
||||
cls,
|
||||
@ -90,7 +88,7 @@ class PlayerService(BaseService):
|
||||
is_injured: Optional[bool] = None,
|
||||
sort: Optional[str] = None,
|
||||
short_output: bool = False,
|
||||
as_csv: bool = False
|
||||
as_csv: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get players with filtering and sorting.
|
||||
@ -124,7 +122,7 @@ class PlayerService(BaseService):
|
||||
pos=pos,
|
||||
strat_code=strat_code,
|
||||
name=name,
|
||||
is_injured=is_injured
|
||||
is_injured=is_injured,
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
@ -137,18 +135,17 @@ class PlayerService(BaseService):
|
||||
if as_csv:
|
||||
return cls._format_player_csv(players_data)
|
||||
else:
|
||||
return {
|
||||
"count": len(players_data),
|
||||
"players": players_data
|
||||
}
|
||||
return {"count": len(players_data), "players": players_data}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching players: {e}")
|
||||
try:
|
||||
raise HTTPException(status_code=500, detail=f"Error fetching players: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error fetching players: {str(e)}"
|
||||
)
|
||||
except ImportError:
|
||||
raise RuntimeError(f"Error fetching players: {str(e)}")
|
||||
|
||||
|
||||
@classmethod
|
||||
def _apply_player_filters(
|
||||
cls,
|
||||
@ -157,10 +154,10 @@ class PlayerService(BaseService):
|
||||
pos: Optional[List[str]] = None,
|
||||
strat_code: Optional[List[str]] = None,
|
||||
name: Optional[str] = None,
|
||||
is_injured: Optional[bool] = None
|
||||
is_injured: Optional[bool] = None,
|
||||
) -> QueryResult:
|
||||
"""Apply player filters in a repo-agnostic way."""
|
||||
|
||||
|
||||
# Check if repo supports where() method (real DB)
|
||||
# Only use DB-native filtering if:
|
||||
# 1. Query has where() method
|
||||
@ -169,34 +166,34 @@ class PlayerService(BaseService):
|
||||
for item in query:
|
||||
first_item = item
|
||||
break
|
||||
|
||||
|
||||
# Use DB-native filtering only for real Peewee models
|
||||
if first_item is not None and not isinstance(first_item, dict):
|
||||
try:
|
||||
if team_id:
|
||||
query = query.where(Player.team_id << team_id)
|
||||
|
||||
|
||||
if strat_code:
|
||||
code_list = [x.lower() for x in strat_code]
|
||||
query = query.where(peewee_fn.Lower(Player.strat_code) << code_list)
|
||||
|
||||
|
||||
if name:
|
||||
query = query.where(peewee_fn.lower(Player.name) == name.lower())
|
||||
|
||||
|
||||
if pos:
|
||||
p_list = [x.upper() for x in pos]
|
||||
pos_conditions = (
|
||||
(Player.pos_1 << p_list) |
|
||||
(Player.pos_2 << p_list) |
|
||||
(Player.pos_3 << p_list) |
|
||||
(Player.pos_4 << p_list) |
|
||||
(Player.pos_5 << p_list) |
|
||||
(Player.pos_6 << p_list) |
|
||||
(Player.pos_7 << p_list) |
|
||||
(Player.pos_8 << p_list)
|
||||
(Player.pos_1 << p_list)
|
||||
| (Player.pos_2 << p_list)
|
||||
| (Player.pos_3 << p_list)
|
||||
| (Player.pos_4 << p_list)
|
||||
| (Player.pos_5 << p_list)
|
||||
| (Player.pos_6 << p_list)
|
||||
| (Player.pos_7 << p_list)
|
||||
| (Player.pos_8 << p_list)
|
||||
)
|
||||
query = query.where(pos_conditions)
|
||||
|
||||
|
||||
if is_injured is not None:
|
||||
if is_injured:
|
||||
query = query.where(Player.il_return.is_null(False))
|
||||
@ -208,55 +205,54 @@ class PlayerService(BaseService):
|
||||
else:
|
||||
# Use Python filtering for mocks
|
||||
def matches(player):
|
||||
if team_id and player.get('team_id') not in team_id:
|
||||
if team_id and player.get("team_id") not in team_id:
|
||||
return False
|
||||
if strat_code:
|
||||
code_list = [s.lower() for s in strat_code]
|
||||
player_code = (player.get('strat_code') or '').lower()
|
||||
player_code = (player.get("strat_code") or "").lower()
|
||||
if player_code not in code_list:
|
||||
return False
|
||||
if name and (player.get('name') or '').lower() != name.lower():
|
||||
if name and (player.get("name") or "").lower() != name.lower():
|
||||
return False
|
||||
if pos:
|
||||
p_list = [p.upper() for p in pos]
|
||||
player_pos = [
|
||||
player.get(f'pos_{i}') for i in range(1, 9)
|
||||
if player.get(f'pos_{i}')
|
||||
player.get(f"pos_{i}")
|
||||
for i in range(1, 9)
|
||||
if player.get(f"pos_{i}")
|
||||
]
|
||||
if not any(p in p_list for p in player_pos):
|
||||
return False
|
||||
if is_injured is not None:
|
||||
has_injury = player.get('il_return') is not None
|
||||
has_injury = player.get("il_return") is not None
|
||||
if is_injured and not has_injury:
|
||||
return False
|
||||
if not is_injured and has_injury:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# Filter in memory
|
||||
filtered = [p for p in query if matches(p)]
|
||||
query = InMemoryQueryResult(filtered)
|
||||
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@classmethod
|
||||
def _apply_player_sort(
|
||||
cls,
|
||||
query: QueryResult,
|
||||
sort: Optional[str] = None
|
||||
cls, query: QueryResult, sort: Optional[str] = None
|
||||
) -> QueryResult:
|
||||
"""Apply player sorting in a repo-agnostic way."""
|
||||
|
||||
|
||||
# Check if items are Peewee models (not dicts)
|
||||
first_item = None
|
||||
for item in query:
|
||||
first_item = item
|
||||
break
|
||||
|
||||
|
||||
# Use DB-native sorting only for real Peewee models
|
||||
if first_item is not None and not isinstance(first_item, dict):
|
||||
try:
|
||||
|
||||
|
||||
if sort == "cost-asc":
|
||||
query = query.order_by(Player.wara)
|
||||
elif sort == "cost-desc":
|
||||
@ -270,13 +266,14 @@ class PlayerService(BaseService):
|
||||
except ImportError:
|
||||
# Fall back to Python sorting if DB not available
|
||||
pass
|
||||
|
||||
|
||||
# Use Python sorting for mocks or if DB sort failed
|
||||
if not hasattr(query, 'order_by') or isinstance(query, InMemoryQueryResult):
|
||||
if not hasattr(query, "order_by") or isinstance(query, InMemoryQueryResult):
|
||||
|
||||
def get_sort_key(player):
|
||||
name = player.get('name', '')
|
||||
wara = player.get('wara', 0)
|
||||
player_id = player.get('id', 0)
|
||||
name = player.get("name", "")
|
||||
wara = player.get("wara", 0)
|
||||
player_id = player.get("id", 0)
|
||||
|
||||
if sort == "cost-asc":
|
||||
return (wara, name, player_id)
|
||||
@ -290,29 +287,27 @@ class PlayerService(BaseService):
|
||||
return (player_id,)
|
||||
|
||||
# Use reverse for descending name sort
|
||||
reverse_sort = (sort == "name-desc")
|
||||
reverse_sort = sort == "name-desc"
|
||||
sorted_list = sorted(list(query), key=get_sort_key, reverse=reverse_sort)
|
||||
query = InMemoryQueryResult(sorted_list)
|
||||
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@classmethod
|
||||
def _query_to_player_dicts(
|
||||
cls,
|
||||
query: QueryResult,
|
||||
short_output: bool = False
|
||||
cls, query: QueryResult, short_output: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Convert query results to list of player dicts."""
|
||||
|
||||
|
||||
# Check if we have DB models or dicts
|
||||
first_item = None
|
||||
for item in query:
|
||||
first_item = item
|
||||
break
|
||||
|
||||
|
||||
if first_item is None:
|
||||
return []
|
||||
|
||||
|
||||
# If items are already dicts (from mock)
|
||||
if isinstance(first_item, dict):
|
||||
players_data = list(query)
|
||||
@ -320,33 +315,33 @@ class PlayerService(BaseService):
|
||||
return players_data
|
||||
# Add computed fields if needed
|
||||
return players_data
|
||||
|
||||
|
||||
# If items are DB models (from real repo)
|
||||
|
||||
|
||||
players_data = []
|
||||
for player in query:
|
||||
player_dict = model_to_dict(player, recurse=not short_output)
|
||||
players_data.append(player_dict)
|
||||
|
||||
|
||||
return players_data
|
||||
|
||||
|
||||
@classmethod
|
||||
def search_players(
|
||||
cls,
|
||||
query_str: str,
|
||||
season: Optional[int] = None,
|
||||
limit: int = 10,
|
||||
short_output: bool = False
|
||||
short_output: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Search players by name with fuzzy matching.
|
||||
|
||||
|
||||
Args:
|
||||
query_str: Search query
|
||||
season: Season to search (None/0 for all)
|
||||
limit: Maximum results
|
||||
short_output: Exclude related data
|
||||
|
||||
|
||||
Returns:
|
||||
Dict with count and matching players
|
||||
"""
|
||||
@ -363,46 +358,49 @@ class PlayerService(BaseService):
|
||||
|
||||
# Convert to dicts if needed
|
||||
all_player_dicts = cls._query_to_player_dicts(
|
||||
InMemoryQueryResult(all_players),
|
||||
short_output=True
|
||||
InMemoryQueryResult(all_players), short_output=True
|
||||
)
|
||||
|
||||
|
||||
# Sort by relevance (exact matches first)
|
||||
exact_matches = []
|
||||
partial_matches = []
|
||||
|
||||
|
||||
for player in all_player_dicts:
|
||||
name_lower = player.get('name', '').lower()
|
||||
|
||||
name_lower = player.get("name", "").lower()
|
||||
|
||||
if name_lower == query_lower:
|
||||
exact_matches.append(player)
|
||||
elif query_lower in name_lower:
|
||||
partial_matches.append(player)
|
||||
|
||||
|
||||
# Sort by season within each group (newest first)
|
||||
if search_all_seasons:
|
||||
exact_matches.sort(key=lambda p: p.get('season', 0), reverse=True)
|
||||
partial_matches.sort(key=lambda p: p.get('season', 0), reverse=True)
|
||||
|
||||
exact_matches.sort(key=lambda p: p.get("season", 0), reverse=True)
|
||||
partial_matches.sort(key=lambda p: p.get("season", 0), reverse=True)
|
||||
|
||||
# Combine and limit
|
||||
results = (exact_matches + partial_matches)[:limit]
|
||||
|
||||
|
||||
return {
|
||||
"count": len(results),
|
||||
"total_matches": len(exact_matches + partial_matches),
|
||||
"all_seasons": search_all_seasons,
|
||||
"players": results
|
||||
"players": results,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
cls.log_error("Error searching players", e)
|
||||
try:
|
||||
raise HTTPException(status_code=500, detail=f"Error searching players: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error searching players: {str(e)}"
|
||||
)
|
||||
except ImportError:
|
||||
raise RuntimeError(f"Error searching players: {str(e)}")
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_player(cls, player_id: int, short_output: bool = False) -> Optional[Dict[str, Any]]:
|
||||
def get_player(
|
||||
cls, player_id: int, short_output: bool = False
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single player by ID."""
|
||||
try:
|
||||
repo = cls._get_player_repo()
|
||||
@ -413,26 +411,31 @@ class PlayerService(BaseService):
|
||||
except Exception as e:
|
||||
cls.log_error(f"Error fetching player {player_id}", e)
|
||||
try:
|
||||
raise HTTPException(status_code=500, detail=f"Error fetching player {player_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error fetching player {player_id}: {str(e)}",
|
||||
)
|
||||
except ImportError:
|
||||
raise RuntimeError(f"Error fetching player {player_id}: {str(e)}")
|
||||
|
||||
|
||||
@classmethod
|
||||
def _player_to_dict(cls, player, recurse: bool = True) -> Dict[str, Any]:
|
||||
"""Convert player to dict."""
|
||||
# If already a dict, return as-is
|
||||
if isinstance(player, dict):
|
||||
return player
|
||||
|
||||
|
||||
# Try to convert Peewee model
|
||||
try:
|
||||
return model_to_dict(player, recurse=recurse)
|
||||
except ImportError:
|
||||
# Fall back to basic dict conversion
|
||||
return dict(player)
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_player(cls, player_id: int, data: Dict[str, Any], token: str) -> Dict[str, Any]:
|
||||
def update_player(
|
||||
cls, player_id: int, data: Dict[str, Any], token: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Update a player (full update via PUT)."""
|
||||
temp_service = cls()
|
||||
temp_service.require_auth(token)
|
||||
@ -441,7 +444,9 @@ class PlayerService(BaseService):
|
||||
# Verify player exists
|
||||
repo = cls._get_player_repo()
|
||||
if not repo.get_by_id(player_id):
|
||||
raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Player ID {player_id} not found"
|
||||
)
|
||||
|
||||
# Execute update
|
||||
repo.update(data, player_id=player_id)
|
||||
@ -450,10 +455,14 @@ class PlayerService(BaseService):
|
||||
|
||||
except Exception as e:
|
||||
cls.log_error(f"Error updating player {player_id}", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error updating player {player_id}: {str(e)}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error updating player {player_id}: {str(e)}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def patch_player(cls, player_id: int, data: Dict[str, Any], token: str) -> Dict[str, Any]:
|
||||
def patch_player(
|
||||
cls, player_id: int, data: Dict[str, Any], token: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Patch a player (partial update)."""
|
||||
temp_service = cls()
|
||||
temp_service.require_auth(token)
|
||||
@ -462,7 +471,9 @@ class PlayerService(BaseService):
|
||||
repo = cls._get_player_repo()
|
||||
player = repo.get_by_id(player_id)
|
||||
if not player:
|
||||
raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Player ID {player_id} not found"
|
||||
)
|
||||
|
||||
# Apply updates using repo
|
||||
repo.update(data, player_id=player_id)
|
||||
@ -471,10 +482,14 @@ class PlayerService(BaseService):
|
||||
|
||||
except Exception as e:
|
||||
cls.log_error(f"Error patching player {player_id}", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error patching player {player_id}: {str(e)}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error patching player {player_id}: {str(e)}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_players(cls, players_data: List[Dict[str, Any]], token: str) -> Dict[str, Any]:
|
||||
def create_players(
|
||||
cls, players_data: List[Dict[str, Any]], token: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Create multiple players."""
|
||||
temp_service = cls()
|
||||
temp_service.require_auth(token)
|
||||
@ -484,13 +499,12 @@ class PlayerService(BaseService):
|
||||
repo = cls._get_player_repo()
|
||||
for player in players_data:
|
||||
dupe = repo.get_or_none(
|
||||
season=player.get("season"),
|
||||
name=player.get("name")
|
||||
season=player.get("season"), name=player.get("name")
|
||||
)
|
||||
if dupe:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Player {player.get('name')} already exists in Season {player.get('season')}"
|
||||
detail=f"Player {player.get('name')} already exists in Season {player.get('season')}",
|
||||
)
|
||||
|
||||
# Insert in batches
|
||||
@ -499,9 +513,11 @@ class PlayerService(BaseService):
|
||||
return {"message": f"Inserted {len(players_data)} players"}
|
||||
|
||||
except Exception as e:
|
||||
cls.log_error(f"Error creating players", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error creating players: {str(e)}")
|
||||
|
||||
cls.log_error("Error creating players", e)
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error creating players: {str(e)}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def delete_player(cls, player_id: int, token: str) -> Dict[str, str]:
|
||||
"""Delete a player."""
|
||||
@ -511,7 +527,9 @@ class PlayerService(BaseService):
|
||||
try:
|
||||
repo = cls._get_player_repo()
|
||||
if not repo.get_by_id(player_id):
|
||||
raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Player ID {player_id} not found"
|
||||
)
|
||||
|
||||
repo.delete_by_id(player_id)
|
||||
|
||||
@ -519,8 +537,10 @@ class PlayerService(BaseService):
|
||||
|
||||
except Exception as e:
|
||||
cls.log_error(f"Error deleting player {player_id}", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error deleting player {player_id}: {str(e)}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error deleting player {player_id}: {str(e)}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _format_player_csv(cls, players: List[Dict]) -> str:
|
||||
"""Format player list as CSV - works with both real DB and mocks."""
|
||||
@ -543,52 +563,52 @@ class InMemoryQueryResult:
|
||||
In-memory query result for mock repositories.
|
||||
Supports filtering, sorting, and iteration.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, items: List[Dict[str, Any]]):
|
||||
self._items = list(items)
|
||||
|
||||
def where(self, *conditions) -> 'InMemoryQueryResult':
|
||||
|
||||
def where(self, *conditions) -> "InMemoryQueryResult":
|
||||
"""Apply filter conditions (no-op for compatibility)."""
|
||||
return self
|
||||
|
||||
def order_by(self, *fields) -> 'InMemoryQueryResult':
|
||||
|
||||
def order_by(self, *fields) -> "InMemoryQueryResult":
|
||||
"""Apply sort (no-op, sorting done by service)."""
|
||||
return self
|
||||
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self._items)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
|
||||
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._items[index]
|
||||
|
||||
|
||||
class RealPlayerRepository:
|
||||
"""Real database repository implementation."""
|
||||
|
||||
|
||||
def __init__(self, model_class):
|
||||
self._model = model_class
|
||||
|
||||
|
||||
def select_season(self, season: int):
|
||||
"""Return query for season."""
|
||||
return self._model.select().where(self._model.season == season)
|
||||
|
||||
|
||||
def get_by_id(self, player_id: int):
|
||||
"""Get player by ID."""
|
||||
return self._model.get_or_none(self._model.id == player_id)
|
||||
|
||||
|
||||
def get_or_none(self, **conditions):
|
||||
"""Get player matching conditions."""
|
||||
try:
|
||||
return self._model.get_or_none(**conditions)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def update(self, data: Dict, player_id: int) -> int:
|
||||
"""Update player."""
|
||||
return Player.update(**data).where(Player.id == player_id).execute()
|
||||
|
||||
@ -10,6 +10,7 @@ import copy
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any, Literal, TYPE_CHECKING
|
||||
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from .base import BaseService
|
||||
from .interfaces import AbstractTeamRepository
|
||||
@ -28,17 +29,14 @@ except ImportError:
|
||||
self.detail = detail
|
||||
super().__init__(detail)
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
logger = logging.getLogger("discord_app")
|
||||
|
||||
|
||||
class TeamService(BaseService):
|
||||
"""Service for team-related operations."""
|
||||
|
||||
cache_patterns = [
|
||||
"teams*",
|
||||
"team*",
|
||||
"team-roster*"
|
||||
]
|
||||
cache_patterns = ["teams*", "team*", "team-roster*"]
|
||||
|
||||
# Class-level repository for dependency injection
|
||||
_injected_repo: Optional[AbstractTeamRepository] = None
|
||||
@ -46,8 +44,8 @@ class TeamService(BaseService):
|
||||
def __init__(
|
||||
self,
|
||||
team_repo: Optional[AbstractTeamRepository] = None,
|
||||
config: Optional['ServiceConfig'] = None,
|
||||
**kwargs
|
||||
config: Optional["ServiceConfig"] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize TeamService with optional repository.
|
||||
@ -75,11 +73,12 @@ class TeamService(BaseService):
|
||||
return cls._get_real_repo()
|
||||
|
||||
@classmethod
|
||||
def _get_real_repo(cls) -> 'RealTeamRepository':
|
||||
def _get_real_repo(cls) -> "RealTeamRepository":
|
||||
"""Get a real DB repository for production use."""
|
||||
from ..db_engine import Team # Lazy import to avoid loading DB in tests
|
||||
|
||||
return RealTeamRepository(Team)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_teams(
|
||||
cls,
|
||||
@ -89,7 +88,7 @@ class TeamService(BaseService):
|
||||
team_abbrev: Optional[List[str]] = None,
|
||||
active_only: bool = False,
|
||||
short_output: bool = False,
|
||||
as_csv: bool = False
|
||||
as_csv: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get teams with filtering.
|
||||
@ -118,21 +117,27 @@ class TeamService(BaseService):
|
||||
|
||||
# Apply filters
|
||||
if manager_id:
|
||||
teams_list = [t for t in teams_list
|
||||
if cls._team_has_manager(t, manager_id)]
|
||||
teams_list = [
|
||||
t for t in teams_list if cls._team_has_manager(t, manager_id)
|
||||
]
|
||||
|
||||
if owner_id:
|
||||
teams_list = [t for t in teams_list
|
||||
if cls._team_has_owner(t, owner_id)]
|
||||
teams_list = [t for t in teams_list if cls._team_has_owner(t, owner_id)]
|
||||
|
||||
if team_abbrev:
|
||||
abbrev_list = [x.lower() for x in team_abbrev]
|
||||
teams_list = [t for t in teams_list
|
||||
if cls._get_team_field(t, 'abbrev', '').lower() in abbrev_list]
|
||||
teams_list = [
|
||||
t
|
||||
for t in teams_list
|
||||
if cls._get_team_field(t, "abbrev", "").lower() in abbrev_list
|
||||
]
|
||||
|
||||
if active_only:
|
||||
teams_list = [t for t in teams_list
|
||||
if not cls._get_team_field(t, 'abbrev', '').endswith(('IL', 'MiL'))]
|
||||
teams_list = [
|
||||
t
|
||||
for t in teams_list
|
||||
if not cls._get_team_field(t, "abbrev", "").endswith(("IL", "MiL"))
|
||||
]
|
||||
|
||||
# Convert to dicts
|
||||
teams_data = [cls._team_to_dict(t, short_output) for t in teams_list]
|
||||
@ -140,18 +145,15 @@ class TeamService(BaseService):
|
||||
if as_csv:
|
||||
return cls._format_team_csv(teams_data)
|
||||
|
||||
return {
|
||||
"count": len(teams_data),
|
||||
"teams": teams_data
|
||||
}
|
||||
return {"count": len(teams_data), "teams": teams_data}
|
||||
|
||||
except Exception as e:
|
||||
temp_service = cls()
|
||||
temp_service.handle_error(f"Error fetching teams", e)
|
||||
temp_service.handle_error("Error fetching teams", e)
|
||||
finally:
|
||||
temp_service = cls()
|
||||
temp_service.close_db()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_team(cls, team_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single team by ID."""
|
||||
@ -167,13 +169,10 @@ class TeamService(BaseService):
|
||||
finally:
|
||||
temp_service = cls()
|
||||
temp_service.close_db()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_team_roster(
|
||||
cls,
|
||||
team_id: int,
|
||||
which: Literal['current', 'next'],
|
||||
sort: Optional[str] = None
|
||||
cls, team_id: int, which: Literal["current", "next"], sort: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get team roster with IL lists.
|
||||
@ -192,26 +191,28 @@ class TeamService(BaseService):
|
||||
|
||||
team = Team.get_by_id(team_id)
|
||||
|
||||
if which == 'current':
|
||||
if which == "current":
|
||||
full_roster = team.get_this_week()
|
||||
else:
|
||||
full_roster = team.get_next_week()
|
||||
|
||||
# Deep copy and convert to dicts
|
||||
result = {
|
||||
'active': {'players': []},
|
||||
'shortil': {'players': []},
|
||||
'longil': {'players': []}
|
||||
"active": {"players": []},
|
||||
"shortil": {"players": []},
|
||||
"longil": {"players": []},
|
||||
}
|
||||
|
||||
for section in ['active', 'shortil', 'longil']:
|
||||
players = copy.deepcopy(full_roster[section]['players'])
|
||||
result[section]['players'] = [model_to_dict(p) for p in players]
|
||||
for section in ["active", "shortil", "longil"]:
|
||||
players = copy.deepcopy(full_roster[section]["players"])
|
||||
result[section]["players"] = [model_to_dict(p) for p in players]
|
||||
|
||||
# Apply sorting
|
||||
if sort == 'wara-desc':
|
||||
for section in ['active', 'shortil', 'longil']:
|
||||
result[section]['players'].sort(key=lambda p: p.get("wara", 0), reverse=True)
|
||||
if sort == "wara-desc":
|
||||
for section in ["active", "shortil", "longil"]:
|
||||
result[section]["players"].sort(
|
||||
key=lambda p: p.get("wara", 0), reverse=True
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@ -221,9 +222,11 @@ class TeamService(BaseService):
|
||||
finally:
|
||||
temp_service = cls()
|
||||
temp_service.close_db()
|
||||
|
||||
|
||||
@classmethod
|
||||
def update_team(cls, team_id: int, data: Dict[str, Any], token: str) -> Dict[str, Any]:
|
||||
def update_team(
|
||||
cls, team_id: int, data: Dict[str, Any], token: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Update a team (partial update)."""
|
||||
temp_service = cls()
|
||||
temp_service.require_auth(token)
|
||||
@ -232,7 +235,9 @@ class TeamService(BaseService):
|
||||
repo = cls._get_team_repo()
|
||||
team = repo.get_by_id(team_id)
|
||||
if not team:
|
||||
raise HTTPException(status_code=404, detail=f"Team ID {team_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Team ID {team_id} not found"
|
||||
)
|
||||
|
||||
# Apply updates using repo
|
||||
repo.update(data, team_id=team_id)
|
||||
@ -244,9 +249,11 @@ class TeamService(BaseService):
|
||||
finally:
|
||||
temp_service.invalidate_related_cache(cls.cache_patterns)
|
||||
temp_service.close_db()
|
||||
|
||||
|
||||
@classmethod
|
||||
def create_teams(cls, teams_data: List[Dict[str, Any]], token: str) -> Dict[str, str]:
|
||||
def create_teams(
|
||||
cls, teams_data: List[Dict[str, Any]], token: str
|
||||
) -> Dict[str, str]:
|
||||
"""Create multiple teams."""
|
||||
temp_service = cls()
|
||||
temp_service.require_auth(token)
|
||||
@ -256,13 +263,12 @@ class TeamService(BaseService):
|
||||
repo = cls._get_team_repo()
|
||||
for team in teams_data:
|
||||
dupe = repo.get_or_none(
|
||||
season=team.get("season"),
|
||||
abbrev=team.get("abbrev")
|
||||
season=team.get("season"), abbrev=team.get("abbrev")
|
||||
)
|
||||
if dupe:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Team {team.get('abbrev')} already exists in Season {team.get('season')}"
|
||||
detail=f"Team {team.get('abbrev')} already exists in Season {team.get('season')}",
|
||||
)
|
||||
|
||||
# Insert teams
|
||||
@ -271,11 +277,11 @@ class TeamService(BaseService):
|
||||
return {"message": f"Inserted {len(teams_data)} teams"}
|
||||
|
||||
except Exception as e:
|
||||
temp_service.handle_error(f"Error creating teams", e)
|
||||
temp_service.handle_error("Error creating teams", e)
|
||||
finally:
|
||||
temp_service.invalidate_related_cache(cls.cache_patterns)
|
||||
temp_service.close_db()
|
||||
|
||||
|
||||
@classmethod
|
||||
def delete_team(cls, team_id: int, token: str) -> Dict[str, str]:
|
||||
"""Delete a team."""
|
||||
@ -285,7 +291,9 @@ class TeamService(BaseService):
|
||||
try:
|
||||
repo = cls._get_team_repo()
|
||||
if not repo.get_by_id(team_id):
|
||||
raise HTTPException(status_code=404, detail=f"Team ID {team_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Team ID {team_id} not found"
|
||||
)
|
||||
|
||||
repo.delete_by_id(team_id)
|
||||
|
||||
@ -301,17 +309,25 @@ class TeamService(BaseService):
|
||||
@classmethod
|
||||
def _team_has_manager(cls, team, manager_ids: List[int]) -> bool:
|
||||
"""Check if team has any of the specified managers."""
|
||||
team_dict = team if isinstance(team, dict) else cls._team_to_dict(team, short_output=True)
|
||||
manager1 = team_dict.get('manager1_id')
|
||||
manager2 = team_dict.get('manager2_id')
|
||||
team_dict = (
|
||||
team
|
||||
if isinstance(team, dict)
|
||||
else cls._team_to_dict(team, short_output=True)
|
||||
)
|
||||
manager1 = team_dict.get("manager1_id")
|
||||
manager2 = team_dict.get("manager2_id")
|
||||
return manager1 in manager_ids or manager2 in manager_ids
|
||||
|
||||
@classmethod
|
||||
def _team_has_owner(cls, team, owner_ids: List[int]) -> bool:
|
||||
"""Check if team has any of the specified owners."""
|
||||
team_dict = team if isinstance(team, dict) else cls._team_to_dict(team, short_output=True)
|
||||
gmid = team_dict.get('gmid')
|
||||
gmid2 = team_dict.get('gmid2')
|
||||
team_dict = (
|
||||
team
|
||||
if isinstance(team, dict)
|
||||
else cls._team_to_dict(team, short_output=True)
|
||||
)
|
||||
gmid = team_dict.get("gmid")
|
||||
gmid2 = team_dict.get("gmid2")
|
||||
return gmid in owner_ids or gmid2 in owner_ids
|
||||
|
||||
@classmethod
|
||||
@ -341,14 +357,16 @@ class TeamService(BaseService):
|
||||
from ..db_engine import query_to_csv, Team # Lazy import - CSV needs DB
|
||||
|
||||
# Get team IDs from the list
|
||||
team_ids = [t.get('id') for t in teams if t.get('id')]
|
||||
team_ids = [t.get("id") for t in teams if t.get("id")]
|
||||
|
||||
if not team_ids:
|
||||
return ""
|
||||
|
||||
# Query for CSV formatting
|
||||
query = Team.select().where(Team.id << team_ids)
|
||||
return query_to_csv(query, exclude=[Team.division_legacy, Team.mascot, Team.gsheet])
|
||||
return query_to_csv(
|
||||
query, exclude=[Team.division_legacy, Team.mascot, Team.gsheet]
|
||||
)
|
||||
|
||||
|
||||
class RealTeamRepository:
|
||||
@ -377,11 +395,13 @@ class RealTeamRepository:
|
||||
def update(self, data: Dict, team_id: int) -> int:
|
||||
"""Update team."""
|
||||
from ..db_engine import Team # Lazy import - only used in production
|
||||
|
||||
return Team.update(**data).where(Team.id == team_id).execute()
|
||||
|
||||
def insert_many(self, data: List[Dict]) -> int:
|
||||
"""Insert multiple teams."""
|
||||
from ..db_engine import Team, db # Lazy import - only used in production
|
||||
|
||||
with db.atomic():
|
||||
Team.insert_many(data).on_conflict_ignore().execute()
|
||||
return len(data)
|
||||
@ -389,4 +409,5 @@ class RealTeamRepository:
|
||||
def delete_by_id(self, team_id: int) -> int:
|
||||
"""Delete team by ID."""
|
||||
from ..db_engine import Team # Lazy import - only used in production
|
||||
|
||||
return Team.delete().where(Team.id == team_id).execute()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user