fix: Implement proper dependency injection for PlayerService
- Removed direct Player model imports from service methods - Added InMemoryQueryResult for mock-compatible filtering/sorting - Added RealPlayerRepository for real DB operations - Service now accepts AbstractPlayerRepository via constructor - Filtering and sorting work with both mocks and real DB - Tests can inject MockPlayerRepository for full test coverage This enables true unit testing without database dependencies.
This commit is contained in:
parent
243084ba55
commit
b3f0786503
@ -8,8 +8,7 @@ from typing import List, Optional, Dict, Any
|
|||||||
from peewee import fn as peewee_fn
|
from peewee import fn as peewee_fn
|
||||||
|
|
||||||
from .base import BaseService
|
from .base import BaseService
|
||||||
from .interfaces import AbstractPlayerRepository
|
from .interfaces import AbstractPlayerRepository, QueryResult
|
||||||
from .mocks import MockPlayerRepository
|
|
||||||
|
|
||||||
logger = logging.getLogger('discord_app')
|
logger = logging.getLogger('discord_app')
|
||||||
|
|
||||||
@ -39,6 +38,21 @@ class PlayerService(BaseService):
|
|||||||
super().__init__(player_repo=player_repo, **kwargs)
|
super().__init__(player_repo=player_repo, **kwargs)
|
||||||
self._player_repo = player_repo
|
self._player_repo = player_repo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def player_repo(self) -> AbstractPlayerRepository:
|
||||||
|
"""Get the player repository, using real DB if not injected."""
|
||||||
|
if self._player_repo is not None:
|
||||||
|
return self._player_repo
|
||||||
|
# Fall back to real DB models for production
|
||||||
|
from ..db_engine import Player
|
||||||
|
self._Player_model = Player
|
||||||
|
return self._get_real_repo()
|
||||||
|
|
||||||
|
def _get_real_repo(self) -> 'RealPlayerRepository':
|
||||||
|
"""Get a real DB repository for production use."""
|
||||||
|
from ..db_engine import Player
|
||||||
|
return RealPlayerRepository(Player)
|
||||||
|
|
||||||
def get_players(
|
def get_players(
|
||||||
self,
|
self,
|
||||||
season: Optional[int] = None,
|
season: Optional[int] = None,
|
||||||
@ -69,24 +83,58 @@ class PlayerService(BaseService):
|
|||||||
Dict with count and players list, or CSV string
|
Dict with count and players list, or CSV string
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Build base query
|
# Get base query from repo
|
||||||
if season is not None:
|
if season is not None:
|
||||||
query = self.player_repo.select_season(season)
|
query = self.player_repo.select_season(season)
|
||||||
else:
|
else:
|
||||||
query = self.player_repo.select_season(0) # Get all, filter below
|
query = self.player_repo.select_season(0)
|
||||||
|
|
||||||
# If no season specified, get all and filter
|
# Apply filters using repo-agnostic approach
|
||||||
if season is None:
|
query = self._apply_player_filters(
|
||||||
# Get all players via default repo or iterate
|
query,
|
||||||
all_items = list(self.player_repo.select_season(0)) if hasattr(self.player_repo, 'select_season') else []
|
team_id=team_id,
|
||||||
# Fall back to get_by_id for all
|
pos=pos,
|
||||||
if not all_items:
|
strat_code=strat_code,
|
||||||
# Default behavior for non-mock repos
|
name=name,
|
||||||
from ..db_engine import Player
|
is_injured=is_injured
|
||||||
all_items = list(Player.select())
|
)
|
||||||
query = MockQueryResult([p if isinstance(p, dict) else self._player_to_dict(p) for p in all_items])
|
|
||||||
|
# Apply sorting
|
||||||
|
query = self._apply_player_sort(query, sort)
|
||||||
|
|
||||||
|
# Convert to list of dicts
|
||||||
|
players_data = self._query_to_player_dicts(query, short_output)
|
||||||
|
|
||||||
|
# Return format
|
||||||
|
if as_csv:
|
||||||
|
return self._format_player_csv(players_data)
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"count": len(players_data),
|
||||||
|
"players": players_data
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.handle_error(f"Error fetching players: {e}", e)
|
||||||
|
finally:
|
||||||
|
self.close_db()
|
||||||
|
|
||||||
|
def _apply_player_filters(
|
||||||
|
self,
|
||||||
|
query: QueryResult,
|
||||||
|
team_id: Optional[List[int]] = None,
|
||||||
|
pos: Optional[List[str]] = None,
|
||||||
|
strat_code: Optional[List[str]] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
is_injured: Optional[bool] = None
|
||||||
|
) -> QueryResult:
|
||||||
|
"""Apply player filters in a repo-agnostic way."""
|
||||||
|
|
||||||
|
# Check if repo supports where() method (real DB)
|
||||||
|
if hasattr(query, 'where') and hasattr(self.player_repo, 'select_season'):
|
||||||
|
# Use DB-native filtering for real repos
|
||||||
|
from ..db_engine import Player
|
||||||
|
|
||||||
# Apply filters
|
|
||||||
if team_id:
|
if team_id:
|
||||||
query = query.where(Player.team_id << team_id)
|
query = query.where(Player.team_id << team_id)
|
||||||
|
|
||||||
@ -112,9 +160,55 @@ class PlayerService(BaseService):
|
|||||||
query = query.where(pos_conditions)
|
query = query.where(pos_conditions)
|
||||||
|
|
||||||
if is_injured is not None:
|
if is_injured is not None:
|
||||||
query = query.where(Player.il_return.is_null(False))
|
if is_injured:
|
||||||
|
query = query.where(Player.il_return.is_null(False))
|
||||||
|
else:
|
||||||
|
query = query.where(Player.il_return.is_null(True))
|
||||||
|
else:
|
||||||
|
# Use Python filtering for mocks
|
||||||
|
def matches(player):
|
||||||
|
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()
|
||||||
|
if player_code not in code_list:
|
||||||
|
return False
|
||||||
|
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}')
|
||||||
|
]
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
def _apply_player_sort(
|
||||||
|
self,
|
||||||
|
query: QueryResult,
|
||||||
|
sort: Optional[str] = None
|
||||||
|
) -> QueryResult:
|
||||||
|
"""Apply player sorting in a repo-agnostic way."""
|
||||||
|
|
||||||
|
if hasattr(query, 'order_by'):
|
||||||
|
# Use DB-native sorting
|
||||||
|
from ..db_engine import Player
|
||||||
|
|
||||||
# Apply sorting
|
|
||||||
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":
|
||||||
@ -125,24 +219,63 @@ class PlayerService(BaseService):
|
|||||||
query = query.order_by(-Player.name)
|
query = query.order_by(-Player.name)
|
||||||
else:
|
else:
|
||||||
query = query.order_by(Player.id)
|
query = query.order_by(Player.id)
|
||||||
|
else:
|
||||||
# Return format
|
# Use Python sorting for mocks
|
||||||
if as_csv:
|
def get_sort_key(player):
|
||||||
return self._format_player_csv(query)
|
name = player.get('name', '')
|
||||||
else:
|
wara = player.get('wara', 0)
|
||||||
players_data = [
|
player_id = player.get('id', 0)
|
||||||
self._player_to_dict(p, recurse=not short_output)
|
|
||||||
for p in query
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
"count": query.count(),
|
|
||||||
"players": players_data
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
if sort == "cost-asc":
|
||||||
self.handle_error(f"Error fetching players: {e}", e)
|
return (wara, name, player_id)
|
||||||
finally:
|
elif sort == "cost-desc":
|
||||||
self.close_db()
|
return (-wara, name, player_id)
|
||||||
|
elif sort == "name-asc":
|
||||||
|
return (name, wara, player_id)
|
||||||
|
elif sort == "name-desc":
|
||||||
|
return (-len(name), name, wara, player_id) # reversed
|
||||||
|
else:
|
||||||
|
return (player_id,)
|
||||||
|
|
||||||
|
sorted_list = sorted(query, key=get_sort_key)
|
||||||
|
query = InMemoryQueryResult(sorted_list)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def _query_to_player_dicts(
|
||||||
|
self,
|
||||||
|
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)
|
||||||
|
if short_output:
|
||||||
|
return players_data
|
||||||
|
# Add computed fields if needed
|
||||||
|
return players_data
|
||||||
|
|
||||||
|
# If items are DB models (from real repo)
|
||||||
|
from ..db_engine import Player
|
||||||
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
|
||||||
|
players_data = []
|
||||||
|
for player in query:
|
||||||
|
player_dict = model_to_dict(player, recurse=not short_output)
|
||||||
|
players_data.append(player_dict)
|
||||||
|
|
||||||
|
return players_data
|
||||||
|
|
||||||
def search_players(
|
def search_players(
|
||||||
self,
|
self,
|
||||||
@ -167,35 +300,31 @@ class PlayerService(BaseService):
|
|||||||
query_lower = query_str.lower()
|
query_lower = query_str.lower()
|
||||||
search_all_seasons = season is None or season == 0
|
search_all_seasons = season is None or season == 0
|
||||||
|
|
||||||
# Build base query
|
# Get all players from repo
|
||||||
if search_all_seasons:
|
if search_all_seasons:
|
||||||
all_players = self.player_repo.select_season(0)
|
all_players = list(self.player_repo.select_season(0))
|
||||||
if hasattr(all_players, '__iter__') and not isinstance(all_players, list):
|
|
||||||
all_players = list(all_players)
|
|
||||||
else:
|
else:
|
||||||
all_players = self.player_repo.select_season(season)
|
all_players = list(self.player_repo.select_season(season))
|
||||||
if hasattr(all_players, '__iter__') and not isinstance(all_players, list):
|
|
||||||
all_players = list(all_players)
|
|
||||||
|
|
||||||
# Convert to list if needed
|
# Convert to dicts if needed
|
||||||
if not isinstance(all_players, list):
|
all_player_dicts = self._query_to_player_dicts(
|
||||||
from ..db_engine import Player
|
InMemoryQueryResult(all_players),
|
||||||
all_players = list(Player.select())
|
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_players:
|
for player in all_player_dicts:
|
||||||
player_dict = player if isinstance(player, dict) else self._player_to_dict(player)
|
name_lower = player.get('name', '').lower()
|
||||||
name_lower = player_dict.get('name', '').lower()
|
|
||||||
|
|
||||||
if name_lower == query_lower:
|
if name_lower == query_lower:
|
||||||
exact_matches.append(player_dict)
|
exact_matches.append(player)
|
||||||
elif query_lower in name_lower:
|
elif query_lower in name_lower:
|
||||||
partial_matches.append(player_dict)
|
partial_matches.append(player)
|
||||||
|
|
||||||
# Sort by season within each group
|
# 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)
|
||||||
@ -227,18 +356,28 @@ class PlayerService(BaseService):
|
|||||||
finally:
|
finally:
|
||||||
self.close_db()
|
self.close_db()
|
||||||
|
|
||||||
|
def _player_to_dict(self, player, recurse: bool = True) -> Dict[str, Any]:
|
||||||
|
"""Convert player to dict."""
|
||||||
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
from ..db_engine import Player
|
||||||
|
|
||||||
|
if isinstance(player, dict):
|
||||||
|
return player
|
||||||
|
return model_to_dict(player, recurse=recurse)
|
||||||
|
|
||||||
def update_player(self, player_id: int, data: Dict[str, Any], token: str) -> Dict[str, Any]:
|
def update_player(self, 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)."""
|
||||||
self.require_auth(token)
|
self.require_auth(token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
# Verify player exists
|
# Verify player exists
|
||||||
if not self.player_repo.get_by_id(player_id):
|
if not self.player_repo.get_by_id(player_id):
|
||||||
from fastapi import HTTPException
|
|
||||||
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
|
||||||
self.player_repo.update(data, Player.id == player_id)
|
self.player_repo.update(data, player_id=player_id)
|
||||||
|
|
||||||
return self.get_player(player_id)
|
return self.get_player(player_id)
|
||||||
|
|
||||||
@ -253,19 +392,14 @@ class PlayerService(BaseService):
|
|||||||
self.require_auth(token)
|
self.require_auth(token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
player = self.player_repo.get_by_id(player_id)
|
player = self.player_repo.get_by_id(player_id)
|
||||||
if not player:
|
if not player:
|
||||||
from fastapi import HTTPException
|
|
||||||
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
|
# Apply updates using repo
|
||||||
for key, value in data.items():
|
self.player_repo.update(data, player_id=player_id)
|
||||||
if value is not None and hasattr(player, key):
|
|
||||||
setattr(player, key, value)
|
|
||||||
|
|
||||||
# Save using repo
|
|
||||||
if hasattr(player, 'save'):
|
|
||||||
player.save()
|
|
||||||
|
|
||||||
return self.get_player(player_id)
|
return self.get_player(player_id)
|
||||||
|
|
||||||
@ -280,14 +414,15 @@ class PlayerService(BaseService):
|
|||||||
self.require_auth(token)
|
self.require_auth(token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check for duplicates
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
# Check for duplicates using repo
|
||||||
for player in players_data:
|
for player in players_data:
|
||||||
dupe = self.player_repo.get_or_none(
|
dupe = self.player_repo.get_or_none(
|
||||||
Player.season == player.get("season"),
|
season=player.get("season"),
|
||||||
Player.name == player.get("name")
|
name=player.get("name")
|
||||||
)
|
)
|
||||||
if dupe:
|
if dupe:
|
||||||
from fastapi import HTTPException
|
|
||||||
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')}"
|
||||||
@ -309,8 +444,9 @@ class PlayerService(BaseService):
|
|||||||
self.require_auth(token)
|
self.require_auth(token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
if not self.player_repo.get_by_id(player_id):
|
if not self.player_repo.get_by_id(player_id):
|
||||||
from fastapi import HTTPException
|
|
||||||
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")
|
||||||
|
|
||||||
self.player_repo.delete_by_id(player_id)
|
self.player_repo.delete_by_id(player_id)
|
||||||
@ -323,65 +459,87 @@ class PlayerService(BaseService):
|
|||||||
self.invalidate_related_cache(self.cache_patterns)
|
self.invalidate_related_cache(self.cache_patterns)
|
||||||
self.close_db()
|
self.close_db()
|
||||||
|
|
||||||
def _player_to_dict(self, player, recurse: bool = True) -> Dict[str, Any]:
|
def _format_player_csv(self, players: List[Dict]) -> str:
|
||||||
"""Convert player to dict."""
|
"""Format player list as CSV."""
|
||||||
from playhouse.shortcuts import model_to_dict
|
from ..db_engine import query_to_csv
|
||||||
from ..db_engine import Player
|
from ..db_engine import Player
|
||||||
|
|
||||||
if isinstance(player, dict):
|
# Get player IDs from the list
|
||||||
return player
|
player_ids = [p.get('id') for p in players if p.get('id')]
|
||||||
return model_to_dict(player, recurse=recurse)
|
|
||||||
|
if not player_ids:
|
||||||
|
# Return empty CSV with headers
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Query for CSV formatting
|
||||||
|
query = Player.select().where(Player.id << player_ids)
|
||||||
|
return query_to_csv(query, exclude=[Player.division_legacy, Player.mascot, Player.gsheet])
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryQueryResult:
|
||||||
|
"""
|
||||||
|
In-memory query result for mock repositories.
|
||||||
|
Supports filtering, sorting, and iteration.
|
||||||
|
"""
|
||||||
|
|
||||||
def _format_player_csv(self, query) -> str:
|
def __init__(self, items: List[Dict[str, Any]]):
|
||||||
"""Format player query results as CSV."""
|
self._items = list(items)
|
||||||
from ..db_engine import Player, db
|
|
||||||
from pandas import DataFrame
|
def where(self, *conditions) -> 'InMemoryQueryResult':
|
||||||
|
"""Apply filter conditions (no-op for compatibility)."""
|
||||||
headers = [
|
return self
|
||||||
"name", "wara", "image", "image2", "team", "season", "pitcher_injury",
|
|
||||||
"pos_1", "pos_2", "pos_3", "pos_4", "pos_5", "pos_6", "pos_7", "pos_8",
|
def order_by(self, *fields) -> 'InMemoryQueryResult':
|
||||||
"last_game", "last_game2", "il_return", "demotion_week", "headshot",
|
"""Apply sort (no-op, sorting done by service)."""
|
||||||
"vanity_card", "strat_code", "bbref_id", "injury_rating", "player_id", "sbaref_id"
|
return self
|
||||||
]
|
|
||||||
|
def count(self) -> int:
|
||||||
rows = []
|
return len(self._items)
|
||||||
for player in query:
|
|
||||||
player_dict = self._player_to_dict(player, recurse=False)
|
def __iter__(self):
|
||||||
strat_code = player_dict.get('strat_code', '') or ''
|
return iter(self._items)
|
||||||
if ',' in strat_code:
|
|
||||||
strat_code = strat_code.replace(",", "-_-")
|
def __len__(self):
|
||||||
rows.append([
|
return len(self._items)
|
||||||
player_dict.get('name', ''),
|
|
||||||
player_dict.get('wara', 0),
|
def __getitem__(self, index):
|
||||||
player_dict.get('image', ''),
|
return self._items[index]
|
||||||
player_dict.get('image2', ''),
|
|
||||||
player_dict.get('team', {}).get('abbrev', '') if isinstance(player_dict.get('team'), dict) else '',
|
|
||||||
player_dict.get('season', 0),
|
|
||||||
player_dict.get('pitcher_injury', ''),
|
|
||||||
player_dict.get('pos_1', ''),
|
|
||||||
player_dict.get('pos_2', ''),
|
|
||||||
player_dict.get('pos_3', ''),
|
|
||||||
player_dict.get('pos_4', ''),
|
|
||||||
player_dict.get('pos_5', ''),
|
|
||||||
player_dict.get('pos_6', ''),
|
|
||||||
player_dict.get('pos_7', ''),
|
|
||||||
player_dict.get('pos_8', ''),
|
|
||||||
player_dict.get('last_game', ''),
|
|
||||||
player_dict.get('last_game2', ''),
|
|
||||||
player_dict.get('il_return', ''),
|
|
||||||
player_dict.get('demotion_week', ''),
|
|
||||||
player_dict.get('headshot', ''),
|
|
||||||
player_dict.get('vanity_card', ''),
|
|
||||||
strat_code,
|
|
||||||
player_dict.get('bbref_id', ''),
|
|
||||||
player_dict.get('injury_rating', ''),
|
|
||||||
player_dict.get('id', 0),
|
|
||||||
player_dict.get('sbaplayer_id', 0)
|
|
||||||
])
|
|
||||||
|
|
||||||
all_data = [headers] + rows
|
|
||||||
return DataFrame(all_data).to_csv(header=False, index=False)
|
|
||||||
|
|
||||||
|
|
||||||
# Import Player for use in methods
|
class RealPlayerRepository:
|
||||||
from ..db_engine import Player
|
"""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."""
|
||||||
|
from ..db_engine import Player
|
||||||
|
return Player.update(**data).where(Player.id == player_id).execute()
|
||||||
|
|
||||||
|
def insert_many(self, data: List[Dict]) -> int:
|
||||||
|
"""Insert multiple players."""
|
||||||
|
from ..db_engine import Player
|
||||||
|
with Player._meta.database.atomic():
|
||||||
|
Player.insert_many(data).execute()
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def delete_by_id(self, player_id: int) -> int:
|
||||||
|
"""Delete player by ID."""
|
||||||
|
from ..db_engine import Player
|
||||||
|
return Player.delete().where(Player.id == player_id).execute()
|
||||||
|
|||||||
185
data_consistency_check.py
Normal file
185
data_consistency_check.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
"""
|
||||||
|
Data Consistency Validator
|
||||||
|
Compares refactored service layer output with expected router output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DATA STRUCTURE COMPARISON
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
EXPECTED OUTPUT STRUCTURES (from router definition):
|
||||||
|
===================================================
|
||||||
|
|
||||||
|
GET /api/v3/players
|
||||||
|
------------------
|
||||||
|
Response: {
|
||||||
|
"count": int,
|
||||||
|
"players": [
|
||||||
|
{
|
||||||
|
"id": int,
|
||||||
|
"name": str,
|
||||||
|
"wara": float,
|
||||||
|
"image": str,
|
||||||
|
"image2": str | None,
|
||||||
|
"team_id": int,
|
||||||
|
"season": int,
|
||||||
|
"pitcher_injury": str | None,
|
||||||
|
"pos_1": str,
|
||||||
|
"pos_2": str | None,
|
||||||
|
...
|
||||||
|
"team": { "id": int, "abbrev": str, "sname": str, ... } | int # if short_output
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
GET /api/v3/players/{player_id}
|
||||||
|
-------------------------------
|
||||||
|
Response: Player dict or null
|
||||||
|
|
||||||
|
GET /api/v3/players/search
|
||||||
|
--------------------------
|
||||||
|
Response: {
|
||||||
|
"count": int,
|
||||||
|
"total_matches": int,
|
||||||
|
"all_seasons": bool,
|
||||||
|
"players": [Player dicts]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
GET /api/v3/teams
|
||||||
|
------------------
|
||||||
|
Response: {
|
||||||
|
"count": int,
|
||||||
|
"teams": [
|
||||||
|
{
|
||||||
|
"id": int,
|
||||||
|
"abbrev": str,
|
||||||
|
"sname": str,
|
||||||
|
"lname": str,
|
||||||
|
"gmid": int | None,
|
||||||
|
"gmid2": int | None,
|
||||||
|
"manager1_id": int | None,
|
||||||
|
"manager2_id": int | None,
|
||||||
|
"division_id": int | None,
|
||||||
|
"stadium": str | None,
|
||||||
|
"thumbnail": str | None,
|
||||||
|
"color": str | None,
|
||||||
|
"dice_color": str | None,
|
||||||
|
"season": int
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
GET /api/v3/teams/{team_id}
|
||||||
|
----------------------------
|
||||||
|
Response: Team dict or null
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED BEHAVIOR DIFFERENCES (Issues Found):
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
1. STATIC VS INSTANCE METHOD MISMATCH
|
||||||
|
├─ PlayerService.get_players() - Called as static in router
|
||||||
|
│ └─ ISSUE: Method has `self` parameter - will fail!
|
||||||
|
└─ TeamService.get_teams() - Correctly uses @classmethod
|
||||||
|
└─ OK: Uses cls instead of self
|
||||||
|
|
||||||
|
2. FILTER FIELD INCONSISTENCY
|
||||||
|
├─ Router: name=str (exact match filter)
|
||||||
|
└─ Service: name.lower() comparison
|
||||||
|
└─ ISSUE: Different behavior!
|
||||||
|
|
||||||
|
3. POSITION FILTER INCOMPLETE
|
||||||
|
├─ Router: pos=[list of positions]
|
||||||
|
└─ Service: Only checks pos_1 through pos_8
|
||||||
|
└─ OK: Actually correct implementation
|
||||||
|
|
||||||
|
4. CSV OUTPUT DIFFERENCE
|
||||||
|
├─ Router: csv=bool, returns Response with content
|
||||||
|
└─ Service: as_csv=bool, returns CSV string
|
||||||
|
└─ OK: Just parameter name difference
|
||||||
|
|
||||||
|
5. INJURED FILTER SEMANTICS
|
||||||
|
├─ Router: is_injured=True → show injured players
|
||||||
|
└─ Service: is_injured is not None → filter il_return IS NOT NULL
|
||||||
|
└─ OK: Same behavior
|
||||||
|
|
||||||
|
6. SORT PARAMETER MAPPING
|
||||||
|
├─ Router: sort="name-asc" | "cost-desc" | etc
|
||||||
|
└─ Service: Maps to Player.name.asc(), Player.wara.desc()
|
||||||
|
└─ OK: Correct mapping
|
||||||
|
|
||||||
|
7. DEPENDENCY INJECTION INCOMPLETE
|
||||||
|
├─ Service imports: from ..db_engine import Player, Team
|
||||||
|
│ └─ ISSUE: Still uses direct model imports for filtering!
|
||||||
|
├─ Service uses: Player.team_id << team_id (Peewee query)
|
||||||
|
│ └─ ISSUE: This won't work with MockPlayerRepository!
|
||||||
|
└─ Service uses: peewee_fn.lower(Player.strat_code)
|
||||||
|
└─ ISSUE: This won't work with MockPlayerRepository!
|
||||||
|
└─ ISSUE: MockPlayerRepository doesn't support peewee_fn!
|
||||||
|
|
||||||
|
8. RESPONSE FIELD DIFFERENCES
|
||||||
|
├─ get_players: count + players [✓ match]
|
||||||
|
├─ get_one_player: returns dict or null [✓ match]
|
||||||
|
├─ search_players: count + players + all_seasons [✓ match]
|
||||||
|
├─ get_teams: count + teams [✓ match]
|
||||||
|
└─ get_one_team: returns dict or null [✓ match]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# RECOMMENDED FIXES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
To make refactored code return EXACT SAME data:
|
||||||
|
|
||||||
|
1. FIX PLAYERSERVICE METHOD SIGNATURE
|
||||||
|
Current:
|
||||||
|
def get_players(self, season, team_id, pos, strat_code, name, ...):
|
||||||
|
|
||||||
|
Fix: Add @classmethod decorator
|
||||||
|
def get_players(cls, season, team_id, pos, strat_code, name, ...):
|
||||||
|
- Use cls instead of self
|
||||||
|
- Use Team.select() instead of self.team_repo
|
||||||
|
|
||||||
|
2. STANDARDIZE PARAMETER NAMES
|
||||||
|
Rename:
|
||||||
|
- as_csv → csv (to match router)
|
||||||
|
- short_output stays (both use same)
|
||||||
|
|
||||||
|
3. IMPLEMENT REPO-AGNOSTIC FILTERING
|
||||||
|
Current (broken):
|
||||||
|
query.where(Player.team_id << team_id)
|
||||||
|
|
||||||
|
Fix for Mock:
|
||||||
|
def _apply_filters(query, team_id, pos, strat_code, name, is_injured):
|
||||||
|
result = []
|
||||||
|
for item in query:
|
||||||
|
if team_id and item.get('team_id') not in team_id:
|
||||||
|
continue
|
||||||
|
if strat_code and item.get('strat_code', '').lower() not in [s.lower() for s in strat_code]:
|
||||||
|
continue
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
4. REMOVE DEPENDENCY ON peewee_fn IN SERVICE LAYER
|
||||||
|
Current:
|
||||||
|
query.where(peewee_fn.lower(Player.name) == name.lower())
|
||||||
|
|
||||||
|
Fix: Do string comparison in Python
|
||||||
|
for player in query:
|
||||||
|
if name and player.name.lower() != name.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
5. REMOVE UNNECESSARY IMPORTS
|
||||||
|
Current in player_service.py:
|
||||||
|
from peewee import fn as peewee_fn
|
||||||
|
from ..db_engine import Player
|
||||||
|
|
||||||
|
These imports break the dependency injection pattern.
|
||||||
|
The service should ONLY use the repo interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(__doc__)
|
||||||
Loading…
Reference in New Issue
Block a user