import asyncio import pandas as pd from pydantic import BaseModel import re from typing import List, Mapping, Tuple, Union import xml.etree.ElementTree as ET from streamlit_oauth import OAuth2Component class LeagueSettings(BaseModel): league_key: str league_id: str name: str num_teams: int current_week: int season: int playoff_start_week: int num_playoff_teams: int num_playoff_consolation_teams: int @classmethod def from_xml(cls, xml_settings: ET.Element) -> "LeagueSettings": base_fields_list = [ "league_key", "league_id", "name", "num_teams", "current_week", "season", ] base_fields_dict = {f: xml_settings.findtext(f"./league/{f}") for f in base_fields_list} settings_fields_list = [ "playoff_start_week", "num_playoff_teams", "num_playoff_consolation_teams", ] settings_fields_dict = {f: xml_settings.findtext(f"./league/settings/{f}") for f in settings_fields_list} league_settings_dict = {**base_fields_dict, **settings_fields_dict} return cls( league_key=league_settings_dict["league_key"] or "", league_id=league_settings_dict["league_id"] or "", name=league_settings_dict["name"] or "", num_teams=int(league_settings_dict["num_teams"] or 0), current_week=int(league_settings_dict["current_week"] or 0), season=int(league_settings_dict["season"] or 0), playoff_start_week=int(league_settings_dict["playoff_start_week"] or 0), num_playoff_teams=int(league_settings_dict["num_playoff_teams"] or 0), num_playoff_consolation_teams=int(league_settings_dict["num_playoff_consolation_teams"] or 0), ) class YahooFantasyClient: def __init__(self, oauth: OAuth2Component, token): self.oauth: OAuth2Component = oauth self.token = token def authorize_yahoo_from_client_json(self) -> None: self.token = self.oauth.refresh_token(self.token) def yahoo_request_to_xml(self, url: str) -> Tuple[ET.Element, str]: self.authorize_yahoo_from_client_json() client = self.oauth.client.get_httpx_client() r = asyncio.run( client.get( url, headers={"Authorization": f"Bearer {self.token.get('access_token')}"}, ) ) xmlstring = r.text xmlstring = re.sub(' xmlns="[^"]+"', "", xmlstring, count=1) root = ET.fromstring(xmlstring) return root, xmlstring def find_all_leagues_for_logged_in_user(self) -> List[str]: url = "https://fantasysports.yahooapis.com/fantasy/v2/users;use_login=1/games;game_keys=nfl/leagues" root, _ = self.yahoo_request_to_xml(url) league_keys = list( filter(None, [x.text for x in root.findall("./users/user/games/game/leagues/league/league_key")]) ) return league_keys def get_guid_for_logged_in_user(self) -> str | None: url = "https://fantasysports.yahooapis.com/fantasy/v2/users;use_login=1/games;game_keys=nfl/teams" root, _ = self.yahoo_request_to_xml(url) user_guid = root.findtext("./users/user/guid") return user_guid def parse_matchup(self, matchup: ET.Element, match_index: int) -> List[Mapping[str, Union[str, float]]]: matchup_info = Matchup.from_xml(matchup, match_index) return matchup_info.to_list_team_dict() def parse_league_settings(self, league_key: str) -> LeagueSettings: url = f"https://fantasysports.yahooapis.com/fantasy/v2/league/{league_key}/settings" league_settings, _ = self.yahoo_request_to_xml(url) parsed_league_settings = LeagueSettings.from_xml(league_settings) return parsed_league_settings def parse_weeks_matchups(self, week: str, league_key: str) -> List[Mapping[str, Union[str, float]]]: url = f"https://fantasysports.yahooapis.com/fantasy/v2/leagues;league_keys={league_key}/scoreboard;week={week}" week_scoreboard, _ = self.yahoo_request_to_xml(url) week_matchups = week_scoreboard.findall("./leagues/league/scoreboard/matchups/matchup") weekly_scores = [] for match_index, matchup in enumerate(week_matchups): matchup_result = self.parse_matchup(matchup, match_index) weekly_scores.extend(matchup_result) return weekly_scores def full_schedule_dataframe(self, league_key: str) -> pd.DataFrame: league_settings = self.parse_league_settings(league_key) all_weeks = ",".join([str(w) for w in range(1, league_settings.playoff_start_week)]) df = pd.DataFrame(self.parse_weeks_matchups(week=all_weeks, league_key=league_key)) return df class MatchupTeam(BaseModel): team_id: str team_name: str team_points: float win_probability: float team_projected_points: float @classmethod def from_xml(cls, xml_matchup_team: ET.Element) -> "MatchupTeam": team_id = xml_matchup_team.findtext("./team_id") team_name = xml_matchup_team.findtext("./name") team_points = xml_matchup_team.findtext("./team_points/total") win_probability = xml_matchup_team.findtext("./win_probability") team_projected_points = xml_matchup_team.findtext("./team_projected_points/total") return cls( team_id=team_id or "", team_name=team_name or "", team_points=float(team_points or 0), win_probability=float(win_probability or 0), team_projected_points=float(team_projected_points or 0), ) class Matchup(BaseModel): matchup_status: str is_playoffs: str is_consolation: str week: float match_index: int matchup_teams: List[MatchupTeam] @classmethod def from_xml(cls, xml_matchup: ET.Element, match_index: int) -> "Matchup": matchup_status = xml_matchup.findtext("./status") is_playoffs = xml_matchup.findtext("./is_playoffs") is_consolation = xml_matchup.findtext("./is_consolation") week = xml_matchup.findtext("./week") xml_matchup_teams = xml_matchup.findall("./teams/team") matchup_teams = [MatchupTeam.from_xml(team) for team in xml_matchup_teams] return cls( matchup_status=matchup_status or "", is_playoffs=is_playoffs or "", is_consolation=is_consolation or "", week=float(week or 0), match_index=match_index, matchup_teams=matchup_teams, ) def to_list_team_dict(self) -> List[Mapping[str, Union[str, float]]]: matchup_info_dict = self.dict(exclude={"matchup_teams"}) out_list: List[Mapping[str, Union[str, float]]] = [] for team in self.matchup_teams: team_dict = team.dict() team_dict.update(matchup_info_dict) out_list.append(team_dict) return out_list