- Moved peewee/fastapi imports inside methods to enable testing without DB - Added InMemoryQueryResult for mock-compatible filtering/sorting - Updated interfaces with @runtime_checkable for isinstance() checks - Fixed get_or_none() to accept keyword arguments - _player_to_dict() now handles both dicts and Peewee models Result: All 14 tests pass without database connection. Service can now be fully tested with MockPlayerRepository.
109 lines
2.4 KiB
Python
109 lines
2.4 KiB
Python
"""
|
|
Abstract Base Classes (Protocols) for Dependency Injection
|
|
Defines interfaces that can be mocked for testing.
|
|
"""
|
|
|
|
from typing import List, Dict, Any, Optional, Protocol, runtime_checkable
|
|
|
|
|
|
class PlayerData(Dict):
|
|
"""Player data structure matching Peewee model."""
|
|
pass
|
|
|
|
|
|
class TeamData(Dict):
|
|
"""Team data structure matching Peewee model."""
|
|
pass
|
|
|
|
|
|
@runtime_checkable
|
|
class QueryResult(Protocol):
|
|
"""Protocol for query-like objects."""
|
|
|
|
def where(self, *conditions) -> 'QueryResult':
|
|
...
|
|
|
|
def order_by(self, *fields) -> 'QueryResult':
|
|
...
|
|
|
|
def count(self) -> int:
|
|
...
|
|
|
|
def __iter__(self):
|
|
...
|
|
|
|
def __len__(self) -> int:
|
|
...
|
|
|
|
|
|
@runtime_checkable
|
|
class AbstractPlayerRepository(Protocol):
|
|
"""Abstract interface for player data access."""
|
|
|
|
def select_season(self, season: int) -> QueryResult:
|
|
...
|
|
|
|
def get_by_id(self, player_id: int) -> Optional[PlayerData]:
|
|
...
|
|
|
|
def get_or_none(self, *conditions, **field_conditions) -> Optional[PlayerData]:
|
|
...
|
|
|
|
def update(self, data: Dict, *conditions, **field_conditions) -> int:
|
|
...
|
|
|
|
def insert_many(self, data: List[Dict]) -> int:
|
|
...
|
|
|
|
def delete_by_id(self, player_id: int) -> int:
|
|
...
|
|
|
|
|
|
@runtime_checkable
|
|
class AbstractTeamRepository(Protocol):
|
|
"""Abstract interface for team data access."""
|
|
|
|
def select_season(self, season: int) -> QueryResult:
|
|
...
|
|
|
|
def get_by_id(self, team_id: int) -> Optional[TeamData]:
|
|
...
|
|
|
|
def get_or_none(self, *conditions, **field_conditions) -> Optional[TeamData]:
|
|
...
|
|
|
|
def update(self, data: Dict, *conditions, **field_conditions) -> int:
|
|
...
|
|
|
|
def insert_many(self, data: List[Dict]) -> int:
|
|
...
|
|
|
|
def delete_by_id(self, team_id: int) -> int:
|
|
...
|
|
|
|
|
|
@runtime_checkable
|
|
class AbstractCacheService(Protocol):
|
|
"""Abstract interface for cache operations."""
|
|
|
|
def get(self, key: str) -> Optional[str]:
|
|
...
|
|
|
|
def set(self, key: str, value: str, ttl: int = 300) -> bool:
|
|
...
|
|
|
|
def setex(self, key: str, ttl: int, value: str) -> bool:
|
|
...
|
|
|
|
def keys(self, pattern: str) -> List[str]:
|
|
...
|
|
|
|
def delete(self, *keys: str) -> int:
|
|
...
|
|
|
|
def invalidate_pattern(self, pattern: str) -> int:
|
|
...
|
|
|
|
def exists(self, key: str) -> bool:
|
|
...
|