diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/api_calls/__init__.py b/api_calls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api_calls/current.py b/api_calls/current.py new file mode 100644 index 0000000..daa20aa --- /dev/null +++ b/api_calls/current.py @@ -0,0 +1,35 @@ +import logging +import pydantic + +from pydantic import field_validator +from db_calls import db_get +from exceptions import log_exception, ApiException + +logger = logging.getLogger('discord_app') + +class Current(pydantic.BaseModel): + id: int = 0 + week: int = 69 + freeze: bool = True + season: int = 69 + bet_week: str = 'sheets' + trade_deadline: int = 1 + pick_trade_start: int = 69 + pick_trade_end: int = 420 + playoffs_begin: int = 420 + + @field_validator("bet_week", mode="before") + @classmethod + def cast_to_string(cls, v): + if isinstance(v, str): + return v + return str(v) + + +async def get_current() -> Current: + data = await db_get('current') # data = {'id': 420, 'transcount': 555} + if data is None: + log_exception(ApiException('Did not receive current metadata from the API')) + # return Current() + return Current(**data) + \ No newline at end of file diff --git a/api_calls/draft_data.py b/api_calls/draft_data.py new file mode 100644 index 0000000..c0e67ff --- /dev/null +++ b/api_calls/draft_data.py @@ -0,0 +1,35 @@ +import datetime +import logging +from typing import Optional +import pydantic + +from pydantic import field_validator +from db_calls import db_get +from exceptions import log_exception, ApiException + +logger = logging.getLogger('discord_app') + +class DraftData(pydantic.BaseModel): + id: int = 0 + currentpick: int = 0 + timer: bool = False + pick_deadline: Optional[datetime.datetime] = None + result_channel: str = 'unknown' + ping_channel: str = 'unknown' + pick_minutes: int = 1 + + @field_validator("result_channel", "ping_channel", mode="before") + @classmethod + def cast_to_string(cls, v): + if isinstance(v, str): + return v + return str(v) + + +async def get_draft_data() -> DraftData: + data = await db_get('draftdata') + if data is None: + log_exception(ApiException('Did not receive current metadata from the API')) + return_val = DraftData(**data) + logger.info(f'this draft_data: {return_val}') + return return_val \ No newline at end of file diff --git a/api_calls/draft_pick.py b/api_calls/draft_pick.py new file mode 100644 index 0000000..9206411 --- /dev/null +++ b/api_calls/draft_pick.py @@ -0,0 +1,63 @@ +import logging +from typing import Optional +import pydantic + +from api_calls.player import Player +from api_calls.team import Team +from db_calls import db_get, db_patch +from exceptions import log_exception, ApiException + +logger = logging.getLogger('discord_app') + +class DraftPick(pydantic.BaseModel): + id: int = 0 + season: int = 0 + overall: int = 0 + round: int = 0 + origowner: Optional[Team] = None + owner: Optional[Team] = None + player: Optional[Player] = None + + +async def get_one_draftpick(pick_id: Optional[int] = None, season: Optional[int] = None, overall: Optional[int] = None) -> DraftPick: + if not pick_id and not season and not overall: + log_exception(KeyError('Either pick_id or season + overall must be provided to get_one_draftpick')) + elif (season is not None and overall is None) or (season is None and overall is not None): + log_exception(KeyError('Both season and overall must be provided to get_one_draftpick')) + + if pick_id is not None: + data = await db_get(f'draftpicks/{pick_id}') + if data is None: + log_exception(ApiException(f'No pick found with ID {pick_id}')) + return DraftPick(**data) + + data = await db_get('draftpicks', params=[('season', season), ('overall', overall)]) + if not data or data.get('count', 0) != 1 or len(data.get('picks', [])) != 1: + log_exception(ApiException(f'No pick found in season {season} with overall {overall}')) + + return DraftPick(**data['picks'][0]) + +async def patch_draftpick(updated_pick: DraftPick) -> DraftPick: + if updated_pick.origowner is None or updated_pick.owner is None: + log_exception(ValueError('To patch draftpicks, owner and origowner may not be None')) + + logger.info(f'Patching pick id {updated_pick.id}') + pick_data = updated_pick.model_dump(exclude={'player', 'origowner', 'owner'}) + + + pick_data['origowner_id'] = updated_pick.origowner.id + pick_data['owner_id'] = updated_pick.owner.id + if updated_pick.player: + pick_data['player_id'] = updated_pick.player.id + else: + pick_data['player_id'] = None + + new_pick = await db_patch( + 'draftpicks', + object_id=updated_pick.id, + params=[], + payload=pick_data + ) + + return DraftPick(**new_pick) + diff --git a/api_calls/player.py b/api_calls/player.py new file mode 100644 index 0000000..0dc152e --- /dev/null +++ b/api_calls/player.py @@ -0,0 +1,37 @@ +import logging +import pydantic + +from typing import Optional +from api_calls.team import Team +from db_calls import db_get +from exceptions import log_exception, ApiException + +logger = logging.getLogger('discord_app') + +class Player(pydantic.BaseModel): + id: Optional[int] = None + name: str + wara: float + team: Team + image: str + image2: Optional[str] = None + season: int + pitcher_injury: Optional[int] = None + pos_1: str + pos_2: Optional[str] = None + pos_3: Optional[str] = None + pos_4: Optional[str] = None + pos_5: Optional[str] = None + pos_6: Optional[str] = None + pos_7: Optional[str] = None + pos_8: Optional[str] = None + vanity_card: Optional[str] = None + headshot: Optional[str] = None + last_game: Optional[str] = None + last_game2: Optional[str] = None + il_return: Optional[str] = None + demotion_week: Optional[int] = None + strat_code: Optional[str] = None + bbref_id: Optional[str] = None + injury_rating: Optional[str] = None + sbaplayer_id: Optional[int] = None \ No newline at end of file diff --git a/api_calls/team.py b/api_calls/team.py new file mode 100644 index 0000000..1c1dff6 --- /dev/null +++ b/api_calls/team.py @@ -0,0 +1,25 @@ +import logging +from typing import Optional +import pydantic + +from pydantic import field_validator +from db_calls import db_get +from exceptions import log_exception, ApiException + +logger = logging.getLogger('discord_app') + +class Team(pydantic.BaseModel): + id: int = 0 + abbrev: str + sname: str + lname: str + gmid: Optional[int] = None + gmid2: Optional[int] = None + manager1_id: Optional[int] = None + manager2_id: Optional[int] = None + division_id: Optional[int] = None + stadium: Optional[str] = None + thumbnail: Optional[str] = None + color: Optional[str] = None + dice_color: Optional[str] = None + season: int \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/tests/api_calls/test_current.py b/tests/api_calls/test_current.py new file mode 100644 index 0000000..6e9e983 --- /dev/null +++ b/tests/api_calls/test_current.py @@ -0,0 +1,39 @@ +import pytest +from unittest.mock import AsyncMock, patch + +from api_calls.current import get_current, Current +from exceptions import ApiException + +@pytest.mark.asyncio +async def test_get_current_success(): + mock_data = { + "id": 420, + "week": 1, + "freeze": False, + "season": 2025, + "bet_week": 12, # should be cast to string + "trade_deadline": 10, + "pick_trade_start": 20, + "pick_trade_end": 30, + "playoffs_begin": 40 + } + + with patch("api_calls.current.db_get", new_callable=AsyncMock) as mock_db_get: + mock_db_get.return_value = mock_data + + result = await get_current() + + assert isinstance(result, Current) + assert result.id == 420 + assert result.bet_week == "12" # validated and cast to string + assert result.season == 2025 + +@pytest.mark.asyncio +async def test_get_current_returns_default_on_none(): + with patch("api_calls.current.db_get", new_callable=AsyncMock) as mock_db_get: + + mock_db_get.return_value = None + + with pytest.raises(ApiException): + await get_current() + diff --git a/tests/api_calls/test_draft_data.py b/tests/api_calls/test_draft_data.py new file mode 100644 index 0000000..1b37f49 --- /dev/null +++ b/tests/api_calls/test_draft_data.py @@ -0,0 +1,44 @@ +import pytest +from unittest.mock import AsyncMock, patch +from api_calls.draft_data import get_draft_data, DraftData +from exceptions import ApiException +import datetime + + +@pytest.mark.asyncio +async def test_get_draft_data_success(): + mock_data = { + "id": 42, + "currentpick": 3, + "timer": True, + "pick_deadline": datetime.datetime(2025, 6, 7, 12, 30).isoformat(), + "result_channel": 123456, # will be cast to string + "ping_channel": None, # will become 'None' as string + "pick_minutes": 5 + } + + with patch("api_calls.draft_data.db_get", new_callable=AsyncMock) as mock_db_get: + mock_db_get.return_value = mock_data + + result = await get_draft_data() + + assert isinstance(result, DraftData) + assert result.id == 42 + assert result.currentpick == 3 + assert result.timer is True + assert isinstance(result.pick_deadline, datetime.datetime) + assert result.result_channel == "123456" + assert result.ping_channel == "None" + assert result.pick_minutes == 5 + + +@pytest.mark.asyncio +async def test_get_draft_data_none_response_raises(): + with patch("api_calls.draft_data.db_get", new_callable=AsyncMock) as mock_db_get, \ + patch("api_calls.draft_data.log_exception") as mock_log_exception: + + mock_db_get.return_value = None + mock_log_exception.side_effect = ApiException("Mocked exception") + + with pytest.raises(ApiException, match="Mocked exception"): + await get_draft_data() diff --git a/tests/api_calls/test_draft_picks.py b/tests/api_calls/test_draft_picks.py new file mode 100644 index 0000000..cd5a99a --- /dev/null +++ b/tests/api_calls/test_draft_picks.py @@ -0,0 +1,93 @@ +import pytest +from unittest.mock import AsyncMock, patch +from api_calls.draft_pick import get_one_draftpick, patch_draftpick, DraftPick +from api_calls.team import Team +from api_calls.player import Player +from exceptions import ApiException + + +@pytest.mark.asyncio +async def test_get_one_draftpick_by_id_success(): + mock_data = { + "id": 5, + "season": 2025, + "overall": 3, + "round": 1 + } + + with patch("api_calls.draft_pick.db_get", new_callable=AsyncMock) as mock_db_get: + mock_db_get.return_value = mock_data + result = await get_one_draftpick(pick_id=5) + + assert isinstance(result, DraftPick) + assert result.id == 5 + assert result.overall == 3 + + +@pytest.mark.asyncio +async def test_get_one_draftpick_by_season_and_overall_success(): + mock_data = { + "count": 1, + "picks": [{ + "id": 10, + "season": 2025, + "overall": 1, + "round": 1 + }] + } + + with patch("api_calls.draft_pick.db_get", new_callable=AsyncMock) as mock_db_get: + mock_db_get.return_value = mock_data + result = await get_one_draftpick(season=2025, overall=1) + + assert result.id == 10 + assert result.season == 2025 + + +@pytest.mark.asyncio +async def test_get_one_draftpick_invalid_params_raise(): + with patch("api_calls.draft_pick.log_exception") as mock_log: + mock_log.side_effect = KeyError("Missing args") + with pytest.raises(KeyError, match="Missing args"): + await get_one_draftpick() + + +@pytest.mark.asyncio +async def test_patch_draftpick_success(): + updated_pick = DraftPick( + id=5, + season=2025, + overall=2, + round=1, + origowner=Team(id=1, abbrev='AAA', sname='Alpha', lname='Alpha Squad', season=2025), + owner=Team(id=2, abbrev='BBB', sname='Beta', lname='Beta Crew', season=2025), + player=Player(id=99, name="Test Player", team=Team(id=2, abbrev='BBB', sname='Beta', lname='Beta Crew', season=2025)) + ) + + mock_response = { + "id": 5, + "season": 2025, + "overall": 2, + "round": 1 + } + + with patch("api_calls.draft_pick.db_patch", new_callable=AsyncMock) as mock_patch: + mock_patch.return_value = mock_response + + result = await patch_draftpick(updated_pick) + + assert isinstance(result, DraftPick) + assert result.id == 5 + mock_patch.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_patch_draftpick_missing_fields_logs_and_returns_empty(): + pick = DraftPick(id=5, season=2025, overall=2, round=1) + + with patch("api_calls.draft_pick.log_exception") as mock_log_exception: + mock_log_exception.side_effect = ApiException("Mocked exception") + + with pytest.raises(ApiException, match="Mocked exception"): + await patch_draftpick(pick) +