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