import asyncio import pandas as pd from pydantic import BaseModel import re from typing import List, Mapping, Tuple, Union, Optional 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 roster_positions: list[dict] @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} roster_fields_list = [ "position", "position_type", "count", "is_starting_position", ] roster_positions = [ {f: x.findtext(f"./{f}") for f in roster_fields_list} for x in xml_settings.findall("./league/settings/roster_positions/*") ] 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), roster_positions=roster_positions, ) 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, season: Optional[str]) -> List[str]: if season: season_str = f";seasons={season}" else: season_str = "" url = ( f"https://fantasysports.yahooapis.com/fantasy/v2/users;use_login=1/games{season_str};game_codes=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 get_draft(self, league_key: str): url = f"https://fantasysports.yahooapis.com/fantasy/v2/league/{league_key}/draftresults" draft_results_xml, _ = self.yahoo_request_to_xml(url) parsed_draft = [ { "pick": x.findtext("./pick"), "round": x.findtext("./round"), "team_key": x.findtext("./team_key"), "player_key": x.findtext("./player_key"), } for x in draft_results_xml.findall("./league/draft_results/*") ] df = pd.DataFrame(parsed_draft) for col in ["round", "pick"]: if col in df: df[col] = df[col].astype(int) return df 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 def get_all_logged_in_user_league_settings(self, season: str) -> list[LeagueSettings]: all_league_keys = self.find_all_leagues_for_logged_in_user(season) return [self.parse_league_settings(lk) for lk in all_league_keys] 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