diff --git a/.gitignore b/.gitignore index b260ec8b..498a9786 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ test_output/ *private*.json !private.template.json *token*.json +venv/ diff --git a/MANIFEST.in b/MANIFEST.in index f3457805..8e37bdba 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,9 @@ include LICENSE include requirements.txt include requirements-dev.txt +# Include version bounds file +include VERSION_PYTHON.py + # Include Yahoo Fantasy Sports REST API authentication resources include .env.template diff --git a/VERSION.py b/VERSION.py index 848600a5..d03dd480 100644 --- a/VERSION.py +++ b/VERSION.py @@ -1,2 +1,2 @@ -# DO NOT EDIT - VERSIONING CONTROLLED BY GIT TAGS -__version__ = "v16.0.3" +# DO NOT EDIT - VERSIONING CONTROLLED BY GIT TAGS +__version__ = "v16.0.3" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..47f2b283 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", + "ruamel.yaml" +] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index d99f79d1..90c58854 100644 --- a/setup.py +++ b/setup.py @@ -6,9 +6,28 @@ import setuptools from ruamel.yaml import YAML -from VERSION_PYTHON import __version_minimum_python__, __version_maximum_python__ +# --- Load supported Python version bounds without importing the module (import failed in build isolation) --- +# We avoid `import VERSION_PYTHON` because during `pip install .` (PEP 517 isolated build) the project root +# may not yet be reliably on sys.path when setuptools initially evaluates setup.py for build requirements. +def _read_version_bounds(version_file_path: Path): + minimum = "3.10" # sensible defaults if file missing + maximum = "3.12" + if version_file_path.exists(): + try: + for line in version_file_path.read_text(encoding="utf-8").splitlines(): + if line.startswith("__version_minimum_python__"): + minimum = line.split("=")[-1].strip().strip('"').strip("'") + elif line.startswith("__version_maximum_python__"): + maximum = line.split("=")[-1].strip().strip('"').strip("'") + except Exception: + # Fall back to defaults if parsing somehow fails + pass + return minimum, maximum project_root_dir = Path(__file__).parent +_min_py, _max_py = _read_version_bounds(project_root_dir / "VERSION_PYTHON.py") +__version_minimum_python__, __version_maximum_python__ = _min_py, _max_py + version_file = project_root_dir / "VERSION.py" diff --git a/yfpy/models.py b/yfpy/models.py index 5bacf1e2..b9aca8fa 100644 --- a/yfpy/models.py +++ b/yfpy/models.py @@ -528,6 +528,9 @@ def __init__(self, extracted_data): team_projected_points (TeamProjectedPoints): A YFPY TeamProjectedPoints instance. projected_points (float): The total projected points for the team. team_standings (TeamStandings): A YFPY TeamStandings instance. + team_stats (TeamStats): A YFPY TeamStats instance containing the team's statistics. + team_remaining_games (TeamRemainingGames): A YFPY TeamRemainingGames instance containing information about + remaining games for the team. wins (int): The number of wins by the team. losses (int): The number of losses by the team. ties (int): The number of ties by the team. @@ -587,10 +590,11 @@ def __init__(self, extracted_data): self.team_paid: int = self._extracted_data.get("team_paid", 0) self.team_points: TeamPoints = self._extracted_data.get("team_points", TeamPoints({})) self.points: float = self._get_nested_value(self.team_points, "total", 0.0, float) - self.team_projected_points: TeamProjectedPoints = self._extracted_data.get("team_projected_points", - TeamProjectedPoints({})) + self.team_projected_points: TeamProjectedPoints = self._extracted_data.get("team_projected_points", TeamProjectedPoints({})) self.projected_points: float = self._get_nested_value(self.team_projected_points, "total", 0.0, float) self.team_standings: TeamStandings = self._extracted_data.get("team_standings", TeamStandings({})) + self.team_stats: TeamStats = self._extracted_data.get("team_stats", TeamStats({})) + self.team_remaining_games: TeamRemainingGames = self._extracted_data.get("team_remaining_games", TeamRemainingGames({})) self.wins: int = self._get_nested_value(self.team_standings, ["outcome_totals", "wins"], 0, int) self.losses: int = self._get_nested_value(self.team_standings, ["outcome_totals", "losses"], 0, int) self.ties: int = self._get_nested_value(self.team_standings, ["outcome_totals", "ties"], 0, int) @@ -887,6 +891,55 @@ def __init__(self, extracted_data): self.total: float = self._get_nested_value(self._extracted_data, "total", 0.0, float) self.week: Optional[int] = self._extracted_data.get("week", None) +# noinspection PyUnresolvedReferences +class TeamRemainingGames(YahooFantasyObject): + """Model class for "team_remaining_games" data key. + """ + + def __init__(self, extracted_data): + """Instantiate the TeamRemainingGames child class of YahooFantasyObject. + + Args: + extracted_data (dict): Parsed and cleaned JSON data retrieved from the Yahoo Fantasy Sports REST API. + + Attributes: + coverage_type (str): The timeframe for the remaining games data ("week", "date", "season", etc.). + week (int): The week number. + total (dict): Dictionary containing remaining games statistics: + remaining_games (int): Number of remaining games. + live_games (int): Number of currently live games. + completed_games (int): Number of completed games. + """ + YahooFantasyObject.__init__(self, extracted_data) + self.coverage_type: str = self._extracted_data.get("coverage_type", "") + self.week: Optional[int] = self._extracted_data.get("week", None) + self.total: dict = self._extracted_data.get("total", {}) + self.remaining_games: int = self._get_nested_value(self.total, "remaining_games", 0, int) + self.live_games: int = self._get_nested_value(self.total, "live_games", 0, int) + self.completed_games: int = self._get_nested_value(self.total, "completed_games", 0, int) + + +# noinspection PyUnresolvedReferences +class TeamStats(YahooFantasyObject): + """Model class for "team_stats" data key. + """ + + def __init__(self, extracted_data): + """Instantiate the TeamStats child class of YahooFantasyObject. + + Args: + extracted_data (dict): Parsed and cleaned JSON data retrieved from the Yahoo Fantasy Sports REST API. + + Attributes: + coverage_type (str): The timeframe for the selected team stats ("week", "date", "season", etc.). + week (int): The week number. + stats (list[Stat]): List of Stat objects containing the team's statistics. + """ + YahooFantasyObject.__init__(self, extracted_data) + self.coverage_type: str = self._extracted_data.get("coverage_type", "") + self.week: Optional[int] = self._extracted_data.get("week", None) + self.stats: List[Stat] = self._extracted_data.get("stats", []) + # noinspection PyUnresolvedReferences class TeamStandings(YahooFantasyObject): diff --git a/yfpy/query.py b/yfpy/query.py index 15a4b533..1d79ec5f 100644 --- a/yfpy/query.py +++ b/yfpy/query.py @@ -46,6 +46,7 @@ TeamPoints, TeamProjectedPoints, TeamStandings, + TeamStats, Transaction, User, YahooFantasyObject @@ -522,16 +523,16 @@ def query(self, url: str, data_key_list: Union[List[str], List[List[str]]], data if isinstance(data_key_list[i], list): reformatted = reformat_json_list(raw_response_data) raw_response_data = [ - {data_key_list[i][0]: reformatted[data_key_list[i][0]]}, - {data_key_list[i][1]: reformatted[data_key_list[i][1]]} + {key: reformatted[key]} for key in data_key_list[i] + if key in reformatted ] else: raw_response_data = reformat_json_list(raw_response_data)[data_key_list[i]] else: if isinstance(data_key_list[i], list): raw_response_data = [ - {data_key_list[i][0]: raw_response_data[data_key_list[i][0]]}, - {data_key_list[i][1]: raw_response_data[data_key_list[i][1]]} + {key: raw_response_data[key]} for key in data_key_list[i] + if key in raw_response_data ] else: raw_response_data = raw_response_data.get(data_key_list[i]) @@ -2090,7 +2091,7 @@ def get_team_stats(self, team_id: Union[str, int]) -> TeamPoints: def get_team_stats_by_week( self, team_id: Union[str, int], chosen_week: Union[int, str] = "current" - ) -> Dict[str, Union[TeamPoints, TeamProjectedPoints]]: + ) -> Dict[str, Union[TeamPoints, TeamStats, TeamProjectedPoints]]: """Retrieve stats of specific team by team_id and by week for chosen league. Args: @@ -2113,19 +2114,186 @@ def get_team_stats_by_week( "coverage_type": "week", "total": "78.85", "week": "1" + }), + "team_stats": TeamStats({ + "coverage_type": "week", + "week": "1", + "stats": [ + { + "stat": { + "stat_id": "4", + "value": "249" + } + }, + ... + ] }) } Returns: - dict[str, TeamPoints | TeamProjectedPoints]: Dictionary containing keys "team_points" and - "team_projected_points" with respective values YFPY TeamPoints and YFPY TeamProjectedPoints instances. + dict[str, TeamPoints | TeamProjectedPoints | TeamStats]: Dictionary containing keys "team_points", + "team_projected_points", and "team_stats" with respective values YFPY TeamPoints, YFPY TeamProjectedPoints, + and YFPY TeamStats instances. """ team_key = f"{self.get_league_key()}.t.{team_id}" return self.query( f"https://fantasysports.yahooapis.com/fantasy/v2/team/{team_key}/stats;type=week;week={chosen_week}", - ["team", ["team_points", "team_projected_points"]] + ["team", ["team_points", "team_stats", "team_projected_points"]] ) + + def get_teams_stats_by_week(self, team_ids: List[Union[str, int]], chosen_week: Union[int, str] = "current") -> List[Team]: + """Get stats for multiple teams for a specific week. + + Args: + team_ids (List[Union[str, int]]): List of team IDs to get stats for + chosen_week (Union[int, str], optional): Week number to get stats for. Defaults to "current". + + Examples: + >>> from yfpy.query import YahooFantasySportsQuery + >>> query = YahooFantasySportsQuery(league_id="#####", game_code="nba") + >>> query.get_teams_stats_by_week([1, 2], 22) + [ + { + "clinched_playoffs": 1, + "draft_position": 4, + "has_draft_grade": 0, + "league_scoring_type": "head", + "managers": { + "manager": { + "email": "manager1@example.com", + "felo_score": 801, + "felo_tier": "platinum", + "guid": "ABCD1234EFGH5678IJKL9012MN", + "image_url": "https://s.yimg.com/ag/images/sample_profile_64sq.jpg", + "is_commissioner": 0, + "manager_id": 1, + "nickname": "Manager1" + } + }, + "name": "Team Alpha", + "number_of_moves": 81, + "number_of_trades": 0, + "roster_adds": { + "coverage_type": "week", + "coverage_value": 23, + "value": 0 + }, + "team_id": 1, + "team_key": "454.l.######.t.1", + "team_logos": { + "team_logo": { + "size": "large", + "url": "https://yahoofantasysports-res.cloudinary.com/image/upload/t_s192sq/fantasy-logos/0adf0d289fa54e02c4e3a6d8e99533ff29f1f93b22b7392cfd4fac4951f4c627.jpg" + } + }, + "team_points": { + "coverage_type": "week", + "total": 5.0, + "week": 22 + }, + "team_stats": { + "coverage_type": "week", + "week": 22, + "stats": [ + { + "stat": { + "stat_id": 9004003, + "value": 0.0 + } + }, + { + "stat": { + "stat_id": 5, + "value": 0.445 + } + }, + { + "stat": { + "stat_id": 9007006, + "value": 0.0 + } + }, + { + "stat": { + "stat_id": 8, + "value": 0.822 + } + }, + { + "stat": { + "stat_id": 10, + "value": 86.0 + } + }, + { + "stat": { + "stat_id": 12, + "value": 772.0 + } + }, + { + "stat": { + "stat_id": 15, + "value": 268.0 + } + }, + { + "stat": { + "stat_id": 16, + "value": 178.0 + } + }, + { + "stat": { + "stat_id": 17, + "value": 56.0 + } + }, + { + "stat": { + "stat_id": 18, + "value": 34.0 + } + }, + { + "stat": { + "stat_id": 19, + "value": 80.0 + } + } + ] + }, + "url": "https://basketball.fantasysports.yahoo.com/nba/######/1", + "team_remaining_games": { + "coverage_type": "week", + "week": 22, + "total": { + "remaining_games": 0, + "live_games": 0, + "completed_games": 58 + } + } + }, + ... + ] + + Returns: + List[Team]: List of YFPY Team instances containing comprehensive team data for the specified week. Each Team + instance includes team_points (TeamPoints), team_stats (TeamStats), team_remaining_games + (TeamRemainingGames), managers (Manager), roster_adds (RosterAdds), team_logos (TeamLogo), and other + team-related attributes populated with week-specific statistical data. + + """ + # Convert team IDs to team keys using game_code from initialization and league ID (52687) + team_keys = [f"{self.get_league_key()}.t.{team_id}" for team_id in team_ids] + team_keys_str = ",".join(team_keys) + return self.query( + f"https://fantasysports.yahooapis.com/fantasy/v2/teams;team_keys={team_keys_str}/stats;type=week;week={chosen_week}", + ["teams"] + ) + + def get_team_standings(self, team_id: Union[str, int]) -> TeamStandings: """Retrieve standings of specific team by team_id for chosen league. @@ -2688,6 +2856,35 @@ def get_team_matchups(self, team_id: Union[str, int]) -> List[Matchup]: f"https://fantasysports.yahooapis.com/fantasy/v2/team/{team_key}/matchups", ["team", "matchups"] ) + + def get_team_matchup_by_week(self, team_id: Union[str, int], chosen_week: Union[int, str]) -> Matchup: + """Retrieve matchup of specific team by team_id for a specific week in the chosen league. + + Args: + team_id (str | int): Selected team ID for which to retrieve data (can be integers 1 through n where n is the + number of teams in the league). + chosen_week (Union[int, str]): Week number to get matchup for. + + Examples: + >>> from pathlib import Path + >>> from yfpy.query import YahooFantasySportsQuery + >>> query = YahooFantasySportsQuery(league_id="######", game_code="nfl") + >>> # Get matchup for a specific week + >>> query.get_team_matchup_by_week(1, 5) + Matchup({ + (see get_league_matchups_by_week docstring for matchup data example) + }) + + Returns: + Matchup: YFPY Matchup instance for the specified week. + + """ + team_key = f"{self.get_league_key()}.t.{team_id}" + return self.query( + f"https://fantasysports.yahooapis.com/fantasy/v2/team/{team_key}/matchups;weeks={chosen_week}", + ["team", "matchups", "0", "matchup"], + Matchup + ) def get_player_stats_for_season(self, player_key: str, limit_to_league_stats: bool = True) -> Player: """Retrieve stats of specific player by player_key for the entire season for chosen league. @@ -2771,6 +2968,32 @@ def get_player_stats_for_season(self, player_key: str, limit_to_league_stats: bo ["players", "0", "player"], Player ) + + def get_players_stats_for_season(self, player_keys: list[str], limit_to_league_stats: bool = True) -> list[Player]: + """Retrieve stats of multiple players by player_keys for the entire season. + + Args: + player_keys (list[str]): List of player keys (e.g., ["454.p.6417", "454.p.6032"]). + limit_to_league_stats (bool): Limit stats to the league context (default: True). When False, retrieves general game stats. + + Examples: + >>> from yfpy.query import YahooFantasySportsQuery + >>> query = YahooFantasySportsQuery(league_id="######", game_code="nfl") + >>> player_keys = ["454.p.6417", "454.p.6032"] + >>> query.get_players_stats_for_season(player_keys) + [Player(...), Player(...)] + + Returns: + list[Player]: List of YFPY Player instances. + """ + player_keys_str = ",".join(player_keys) + + # The query method should support extracting a list of Player instances + return self.query( + f"https://fantasysports.yahooapis.com/fantasy/v2/players;" + f"player_keys={player_keys_str}/stats", + ["players"], + ) def get_player_stats_by_week(self, player_key: str, chosen_week: Union[int, str] = "current", limit_to_league_stats: bool = True) -> Player: @@ -3199,4 +3422,4 @@ def get_player_draft_analysis(self, player_key: str) -> Player: f"player_keys={player_key}/draft_analysis", ["league", "players", "0", "player"], Player - ) + ) \ No newline at end of file diff --git a/yfpy/utils.py b/yfpy/utils.py index 50f7af34..ba197deb 100644 --- a/yfpy/utils.py +++ b/yfpy/utils.py @@ -20,7 +20,7 @@ logger = get_logger(__name__) -yahoo_fantasy_sports_game_codes = ["nfl", "nhl", "mlb", "nba"] +yahoo_fantasy_sports_game_codes = ["nfl", "nhl", "mlb", "nba","454"] def retrieve_game_code_from_user() -> str: