From f45da6b2daf5f12ced9c9d7b44bca505493918d4 Mon Sep 17 00:00:00 2001 From: Justin Hsieh <41461549+justin-hsieh@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:50:06 -0700 Subject: [PATCH] Update app --- app.py | 8 +- .../espn_api_submodule/espn_api/_version.py | 2 +- .../espn_api/base_league.py | 65 ++- .../espn_api_submodule/espn_api/base_pick.py | 17 + .../espn_api/football/constant.py | 512 +++++++++++++----- .../espn_api/football/helper.py | 211 ++++++++ .../espn_api/football/league.py | 177 ++++-- .../espn_api/football/player.py | 17 +- .../espn_api/football/settings.py | 20 + .../espn_api/football/team.py | 72 +-- .../espn_api/requests/constant.py | 3 +- fantasy_app/scores.py | 8 +- 12 files changed, 856 insertions(+), 256 deletions(-) create mode 100644 fantasy_app/espn_api_submodule/espn_api/base_pick.py create mode 100644 fantasy_app/espn_api_submodule/espn_api/football/helper.py create mode 100644 fantasy_app/espn_api_submodule/espn_api/football/settings.py diff --git a/app.py b/app.py index d3676e6..7d96ee3 100644 --- a/app.py +++ b/app.py @@ -34,7 +34,7 @@ def calculate_most_points_get(): points = get_most_position_points(contest_list[contest]['position'], contest_list[contest]['stat'], OWNERS, - '2023', + '2024', current_week() - 1) result_dict['contest_results'] = order_positions_by_points(points) result_dict['contest'] = contest @@ -49,7 +49,7 @@ def calculate_most_points_post(): year = point_request['year'] points = get_most_position_points( - contest_list[contest]['position'], contest_list[contest]['stat'], OWNERS, year, week) + contest_list[contest]['position'], contest_list[contest]['stat'], OWNERS, year, int(week)) if contest_list[contest].get('single'): points = get_highest_points(points) @@ -60,6 +60,6 @@ def calculate_most_points_post(): # Add data to firestore db = firestore.client() - doc_ref = db.collection(year).document(output_week) - doc_ref.set(result_dict) + doc_ref = db.collection('yearly_data').document(year) + doc_ref.collection("contests").document(output_week).set(result_dict) return '{"status":"200", "data": "OK"}' \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/_version.py b/fantasy_app/espn_api_submodule/espn_api/_version.py index c42375c..457618b 100644 --- a/fantasy_app/espn_api_submodule/espn_api/_version.py +++ b/fantasy_app/espn_api_submodule/espn_api/_version.py @@ -1 +1 @@ -__version__ = '0.26.2' +__version__ = '0.38.0' diff --git a/fantasy_app/espn_api_submodule/espn_api/base_league.py b/fantasy_app/espn_api_submodule/espn_api/base_league.py index f93e7db..886c1c2 100644 --- a/fantasy_app/espn_api_submodule/espn_api/base_league.py +++ b/fantasy_app/espn_api_submodule/espn_api/base_league.py @@ -2,13 +2,12 @@ from typing import List, Tuple from .base_settings import BaseSettings +from .base_pick import BasePick from .utils.logger import Logger from .requests.espn_requests import EspnFantasyRequests - class BaseLeague(ABC): '''Creates a League instance for Public/Private ESPN league''' - def __init__(self, league_id: int, year: int, sport: str, espn_s2=None, swid=None, debug=False): self.logger = Logger(name=f'{sport} league', debug=debug) self.league_id = league_id @@ -24,51 +23,67 @@ def __init__(self, league_id: int, year: int, sport: str, espn_s2=None, swid=Non 'espn_s2': espn_s2, 'SWID': swid } - self.espn_request = EspnFantasyRequests( - sport=sport, year=year, league_id=league_id, cookies=cookies, logger=self.logger) + self.espn_request = EspnFantasyRequests(sport=sport, year=year, league_id=league_id, cookies=cookies, logger=self.logger) def __repr__(self): return 'League(%s, %s)' % (self.league_id, self.year, ) - def _fetch_league(self, SettingsClass=BaseSettings): + def _fetch_league(self, SettingsClass = BaseSettings): data = self.espn_request.get_league() self.currentMatchupPeriod = data['status']['currentMatchupPeriod'] self.scoringPeriodId = data['scoringPeriodId'] self.firstScoringPeriod = data['status']['firstScoringPeriod'] self.finalScoringPeriod = data['status']['finalScoringPeriod'] + self.previousSeasons = [ + year for year in data["status"]["previousSeasons"] if year < self.year + ] if self.year < 2018: self.current_week = data['scoringPeriodId'] else: - self.current_week = self.scoringPeriodId if self.scoringPeriodId <= data[ - 'status']['finalScoringPeriod'] else data['status']['finalScoringPeriod'] + self.current_week = self.scoringPeriodId if self.scoringPeriodId <= data['status']['finalScoringPeriod'] else data['status']['finalScoringPeriod'] self.settings = SettingsClass(data['settings']) self.members = data.get('members', []) return data - def _fetch_teams(self, data, TeamClass, pro_schedule=None): + def _fetch_draft(self): + '''Creates list of Pick objects from the leagues draft''' + data = self.espn_request.get_league_draft() + + # League has not drafted yet + if not data.get('draftDetail', {}).get('drafted'): + return + + picks = data.get('draftDetail', {}).get('picks', []) + for pick in picks: + team = self.get_team_data(pick.get('teamId')) + playerId = pick.get('playerId') + playerName = '' + if playerId in self.player_map: + playerName = self.player_map[playerId] + round_num = pick.get('roundId') + round_pick = pick.get('roundPickNumber') + bid_amount = pick.get('bidAmount') + keeper_status = pick.get('keeper') + nominatingTeam = self.get_team_data(pick.get('nominatingTeamId')) + self.draft.append(BasePick(team, playerId, playerName, round_num, round_pick, bid_amount, keeper_status, nominatingTeam)) + + def _fetch_teams(self, data, TeamClass, pro_schedule = None): '''Fetch teams in league''' self.teams = [] teams = data['teams'] - members = data['members'] schedule = data['schedule'] seasonId = data['seasonId'] + members = data.get('members', []) team_roster = {} for team in data['teams']: team_roster[team['id']] = team.get('roster', {}) for team in teams: - for member in members: - # For league that is not full the team will not have a owner field - if 'owners' not in team or not team['owners']: - member = None - break - elif member['id'] == team['owners'][0]: - break roster = team_roster[team['id']] - self.teams.append(TeamClass(team, roster=roster, member=member, - schedule=schedule, year=seasonId, pro_schedule=pro_schedule)) + owners = [member for member in members if member.get('id') in team.get('owners', [])] + self.teams.append(TeamClass(team, roster=roster, schedule=schedule, year=seasonId, owners=owners, pro_schedule=pro_schedule)) # sort by team ID self.teams = sorted(self.teams, key=lambda x: x.team_id, reverse=False) @@ -93,10 +108,9 @@ def _get_pro_schedule(self, scoringPeriodId: int = None): pro_game = team.get('proGamesByScoringPeriod', {}) if team['id'] != 0 and (str(scoringPeriodId) in pro_game.keys() and pro_game[str(scoringPeriodId)]): game_data = pro_game[str(scoringPeriodId)][0] - pro_team_schedule[team['id']] = (game_data['homeProTeamId'], game_data['date']) if team['id'] == game_data['awayProTeamId'] else ( - game_data['awayProTeamId'], game_data['date']) + pro_team_schedule[team['id']] = (game_data['homeProTeamId'], game_data['date']) if team['id'] == game_data['awayProTeamId'] else (game_data['awayProTeamId'], game_data['date']) return pro_team_schedule - + def _get_all_pro_schedule(self): data = self.espn_request.get_pro_schedule() @@ -109,6 +123,11 @@ def _get_all_pro_schedule(self): return pro_team_schedule def standings(self) -> List: - standings = sorted( - self.teams, key=lambda x: x.final_standing if x.final_standing != 0 else x.standing, reverse=False) + standings = sorted(self.teams, key=lambda x: x.final_standing if x.final_standing != 0 else x.standing, reverse=False) return standings + + def get_team_data(self, team_id: int) -> List: + for team in self.teams: + if team_id == team.team_id: + return team + return None \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/base_pick.py b/fantasy_app/espn_api_submodule/espn_api/base_pick.py new file mode 100644 index 0000000..5c553a7 --- /dev/null +++ b/fantasy_app/espn_api_submodule/espn_api/base_pick.py @@ -0,0 +1,17 @@ +class BasePick(object): + ''' Pick represents a pick in draft ''' + def __init__(self, team, playerId, playerName, round_num, round_pick, bid_amount, keeper_status, nominatingTeam): + self.team = team + self.playerId = playerId + self.playerName = playerName + self.round_num = round_num + self.round_pick = round_pick + self.bid_amount = bid_amount + self.keeper_status = keeper_status + self.nominatingTeam = nominatingTeam + + def __repr__(self): + return 'Pick(R:%s P:%s, %s, %s)' % (self.round_num, self.round_pick, self.playerName, self.team) + + def auction_repr(self): + return ', '.join(map(str, [self.team, self.playerId, self.playerName, self.bid_amount, self.keeper_status])) \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/football/constant.py b/fantasy_app/espn_api_submodule/espn_api/football/constant.py index 85a034e..93c36ef 100644 --- a/fantasy_app/espn_api_submodule/espn_api/football/constant.py +++ b/fantasy_app/espn_api_submodule/espn_api/football/constant.py @@ -57,7 +57,7 @@ 10: 'TEN', 11: 'IND', 12: 'KC', - 13: 'OAK', + 13: 'LV', 14: 'LAR', 15: 'MIA', 16: 'MIN', @@ -93,177 +93,415 @@ PLAYER_STATS_MAP = { # Passing Stats - 0: "passingAttempts", # PA - 1: "passingCompletions", # PC - 2: "passingIncompletions", # INC - 3: "passingYards", # PY - 4: "passingTouchdowns", # PTD + 0: 'passingAttempts', # PA + 1: 'passingCompletions', # PC + 2: 'passingIncompletions', # INC + 3: 'passingYards', # PY + 4: 'passingTouchdowns', # PTD # 5-14 appear for passing players # 5-7: 6 is half of 5 (integer divide by 2), 7 is half of 6 (integer divide by 2) # 8-10: 9 is half of 8 (integer divide by 2), 10 is half of 9 (integer divide by 2) # 11-12: 12 is half of 11 (integer divide by 2) # 13-14: 14 is half of 13 (integer divide by 2) - 15: "passing40PlusYardTD", # PTD40 - 16: "passing50PlusYardTD", # PTD50 - 17: "passing300To399YardGame", # P300 - 18: "passing400PlusYardGame", # P400 - 19: "passing2PtConversions", # 2PC - 20: "passingInterceptions", # INT - 21: "passingCompletionPercentage", - 22: "passingYards", # PY - TODO: figure out what the difference is between 22 and 3 + 15: 'passing40PlusYardTD', # PTD40 + 16: 'passing50PlusYardTD', # PTD50 + 17: 'passing300To399YardGame', # P300 + 18: 'passing400PlusYardGame', # P400 + 19: 'passing2PtConversions', # 2PC + 20: 'passingInterceptions', # INT + 21: 'passingCompletionPercentage', + 22: 'passingYards', # PY - TODO: figure out what the difference is between 22 and 3 # Rushing Stats - 23: "rushingAttempts", # RA - 24: "rushingYards", # RY - 25: "rushingTouchdowns", # RTD - 26: "rushing2PtConversions", # 2PR + 23: 'rushingAttempts', # RA + 24: 'rushingYards', # RY + 25: 'rushingTouchdowns', # RTD + 26: 'rushing2PtConversions', # 2PR # 27-34 appear for rushing players # 27-29: 28 is half of 27 (integer divide by 2), 29 is half of 28 (integer divide by 2) # 30-32: 31 is half of 30 (integer divide by 2), 32 is half of 31 (integer divide by 2) # 33-34: 34 is half of 33 (integer divide by 2) - 35: "rushing40PlusYardTD", # RTD40 - 36: "rushing50PlusYardTD", # RTD50 - 37: "rushing100To199YardGame", # RY100 - 38: "rushing200PlusYardGame", # RY200 - 39: "rushingYardsPerAttempt", - 40: "rushingYards", # RY - TODO: figure out what the difference is between 40 and 24 + 35: 'rushing40PlusYardTD', # RTD40 + 36: 'rushing50PlusYardTD', # RTD50 + 37: 'rushing100To199YardGame', # RY100 + 38: 'rushing200PlusYardGame', # RY200 + 39: 'rushingYardsPerAttempt', + 40: 'rushingYards', # RY - TODO: figure out what the difference is between 40 and 24 # Receiving Stats - 41: "receivingReceptions", # REC - 42: "receivingYards", # REY - 43: "receivingTouchdowns", # RETD - 44: "receiving2PtConversions", # 2PRE - 45: "receiving40PlusYardTD", # RETD40 - 46: "receiving50PlusYardTD", # RETD50 + 41: 'receivingReceptions', # REC + 42: 'receivingYards', # REY + 43: 'receivingTouchdowns', # RETD + 44: 'receiving2PtConversions', # 2PRE + 45: 'receiving40PlusYardTD', # RETD40 + 46: 'receiving50PlusYardTD', # RETD50 # 47-52 appear for receiving players # 47-49: 48 is half of 47 (integer divide by 2), 49 is half of 48 (integer divide by 2) # 50-52: 51 is half of 50 (integer divide by 2), 52 is half of 51 (integer divide by 2) - 53: "receivingReceptions", # REC - TODO: figure out what the difference is between 53 and 41 + 53: 'receivingReceptions', # REC - TODO: figure out what the difference is between 53 and 41 # 54-55 appear for receiving players # 54-55: 55 is half of 54 (integer divide by 2) - 56: "receiving100To199YardGame", # REY100 - 57: "receiving200PlusYardGame", # REY200 - 58: "receivingTargets", # RET - 59: "receivingYardsAfterCatch", - 60: "receivingYardsPerReception", - 61: "receivingYards", # REY - TODO: figure out what the difference is between 61 and 42 - 62: "2PtConversions", - 63: "fumbleRecoveredForTD", # FTD - 64: "passingTimesSacked", # SK + 56: 'receiving100To199YardGame', # REY100 + 57: 'receiving200PlusYardGame', # REY200 + 58: 'receivingTargets', # RET + 59: 'receivingYardsAfterCatch', + 60: 'receivingYardsPerReception', + 61: 'receivingYards', # REY - TODO: figure out what the difference is between 61 and 42 + 62: '2PtConversions', + 63: 'fumbleRecoveredForTD', # FTD + 64: 'passingTimesSacked', # SK - 68: "fumbles", # FUM + 68: 'fumbles', # FUM - 72: "lostFumbles", # FUML - 73: "turnovers", + 72: 'lostFumbles', # FUML + 73: 'turnovers', # Kicking Stats - 74: "madeFieldGoalsFrom50Plus", # FG50 (does not map directly to FG50 as FG50 does not include 60+) - 75: "attemptedFieldGoalsFrom50Plus", # FGA50 (does not map directly to FGA50 as FG50 does not include 60+) - 76: "missedFieldGoalsFrom50Plus", # FGM50 (does not map directly to FGM50 as FG50 does not include 60+) - 77: "madeFieldGoalsFrom40To49", # FG40 - 78: "attemptedFieldGoalsFrom40To49", # FGA40 - 79: "missedFieldGoalsFrom40To49", # FGM40 - 80: "madeFieldGoalsFromUnder40", # FG0 - 81: "attemptedFieldGoalsFromUnder40", # FGA0 - 82: "missedFieldGoalsFromUnder40", # FGM0 - 83: "madeFieldGoals", # FG - 84: "attemptedFieldGoals", # FGA - 85: "missedFieldGoals", # FGM - 86: "madeExtraPoints", # PAT - 87: "attemptedExtraPoints", # PATA - 88: "missedExtraPoints", # PATM + 74: 'madeFieldGoalsFrom50Plus', # FG50 (does not map directly to FG50 as FG50 does not include 60+) + 75: 'attemptedFieldGoalsFrom50Plus', # FGA50 (does not map directly to FGA50 as FG50 does not include 60+) + 76: 'missedFieldGoalsFrom50Plus', # FGM50 (does not map directly to FGM50 as FG50 does not include 60+) + 77: 'madeFieldGoalsFrom40To49', # FG40 + 78: 'attemptedFieldGoalsFrom40To49', # FGA40 + 79: 'missedFieldGoalsFrom40To49', # FGM40 + 80: 'madeFieldGoalsFromUnder40', # FG0 + 81: 'attemptedFieldGoalsFromUnder40', # FGA0 + 82: 'missedFieldGoalsFromUnder40', # FGM0 + 83: 'madeFieldGoals', # FG + 84: 'attemptedFieldGoals', # FGA + 85: 'missedFieldGoals', # FGM + 86: 'madeExtraPoints', # PAT + 87: 'attemptedExtraPoints', # PATA + 88: 'missedExtraPoints', # PATM # Defensive Stats - 89: "defensive0PointsAllowed", # PA0 - 90: "defensive1To6PointsAllowed", # PA1 - 91: "defensive7To13PointsAllowed", # PA7 - 92: "defensive14To17PointsAllowed", # PA14 - 93: "defensiveBlockedKickForTouchdowns", # BLKKRTD - 94: "defensiveTouchdowns", # Does not include defensive blocked kick for touchdowns (BLKKRTD) - 95: "defensiveInterceptions", # INT - 96: "defensiveFumbles", # FR - 97: "defensiveBlockedKicks", # BLKK - 98: "defensiveSafeties", # SF - 99: "defensiveSacks", # SK + 89: 'defensive0PointsAllowed', # PA0 + 90: 'defensive1To6PointsAllowed', # PA1 + 91: 'defensive7To13PointsAllowed', # PA7 + 92: 'defensive14To17PointsAllowed', # PA14 + 93: 'defensiveBlockedKickForTouchdowns', # BLKKRTD + 94: 'defensiveTouchdowns', # Does not include defensive blocked kick for touchdowns (BLKKRTD) + 95: 'defensiveInterceptions', # INT + 96: 'defensiveFumbles', # FR + 97: 'defensiveBlockedKicks', # BLKK + 98: 'defensiveSafeties', # SF + 99: 'defensiveSacks', # SK # 100: This appears to be defensiveSacks * 2 - 101: "kickoffReturnTouchdowns", # KRTD - 102: "puntReturnTouchdowns", # PRTD - 103: "interceptionReturnTouchdowns", # INTTD - 104: "fumbleReturnTouchdowns", # FRTD - 105: "defensivePlusSpecialTeamsTouchdowns", # Includes defensive blocked kick for touchdowns (BLKKRTD) and kickoff/punt return touchdowns - 106: "defensiveForcedFumbles", # FF - 107: "defensiveAssistedTackles", # TKA - 108: "defensiveSoloTackles", # TKS - 109: "defensiveTotalTackles", # TK + 101: 'kickoffReturnTouchdowns', # KRTD + 102: 'puntReturnTouchdowns', # PRTD + 103: 'interceptionReturnTouchdowns', # INTTD + 104: 'fumbleReturnTouchdowns', # FRTD + 105: 'defensivePlusSpecialTeamsTouchdowns', # Includes defensive blocked kick for touchdowns (BLKKRTD) and kickoff/punt return touchdowns + 106: 'defensiveForcedFumbles', # FF + 107: 'defensiveAssistedTackles', # TKA + 108: 'defensiveSoloTackles', # TKS + 109: 'defensiveTotalTackles', # TK - 113: "defensivePassesDefensed", # PD - 114: "kickoffReturnYards", # KR - 115: "puntReturnYards", # PR + 113: 'defensivePassesDefensed', # PD + 114: 'kickoffReturnYards', # KR + 115: 'puntReturnYards', # PR - 118: "puntsReturned", # PTR + 118: 'puntsReturned', # PTR - 120: "defensivePointsAllowed", # PA - 121: "defensive18To21PointsAllowed", # PA18 - 122: "defensive22To27PointsAllowed", # PA22 - 123: "defensive28To34PointsAllowed", # PA28 - 124: "defensive35To45PointsAllowed", # PA35 - 125: "defensive45PlusPointsAllowed", # PA46 + 120: 'defensivePointsAllowed', # PA + 121: 'defensive18To21PointsAllowed', # PA18 + 122: 'defensive22To27PointsAllowed', # PA22 + 123: 'defensive28To34PointsAllowed', # PA28 + 124: 'defensive35To45PointsAllowed', # PA35 + 125: 'defensive45PlusPointsAllowed', # PA46 - 127: "defensiveYardsAllowed", # YA - 128: "defensiveLessThan100YardsAllowed", #YA100 - 129: "defensive100To199YardsAllowed", # YA199 - 130: "defensive200To299YardsAllowed", # YA299 - 131: "defensive300To349YardsAllowed", # YA349 - 132: "defensive350To399YardsAllowed", # YA399 - 133: "defensive400To449YardsAllowed", # YA449 - 134: "defensive450To499YardsAllowed", # YA499 - 135: "defensive500To549YardsAllowed", # YA549 - 136: "defensive550PlusYardsAllowed", # YA550 + 127: 'defensiveYardsAllowed', # YA + 128: 'defensiveLessThan100YardsAllowed', #YA100 + 129: 'defensive100To199YardsAllowed', # YA199 + 130: 'defensive200To299YardsAllowed', # YA299 + 131: 'defensive300To349YardsAllowed', # YA349 + 132: 'defensive350To399YardsAllowed', # YA399 + 133: 'defensive400To449YardsAllowed', # YA449 + 134: 'defensive450To499YardsAllowed', # YA499 + 135: 'defensive500To549YardsAllowed', # YA549 + 136: 'defensive550PlusYardsAllowed', # YA550 # Punter Stats - 138: "netPunts", # PT - 139: "puntYards", # PTY - 140: "puntsInsideThe10", # PT10 - 141: "puntsInsideThe20", # PT20 - 142: "blockedPunts", # PTB - 145: "puntTouchbacks", # PTTB - 146: "puntFairCatches", #PTFC - 147: "puntAverage", - 148: "puntAverage44.0+", # PTA44 - 149: "puntAverage42.0-43.9", #PTA42 - 150: "puntAverage40.0-41.9", #PTA40 - 151: "puntAverage38.0-39.9", #PTA38 - 152: "puntAverage36.0-37.9", #PTA36 - 153: "puntAverage34.0-35.9", #PTA34 - 154: "puntAverage33.9AndUnder", #PTA33 + 138: 'netPunts', # PT + 139: 'puntYards', # PTY + 140: 'puntsInsideThe10', # PT10 + 141: 'puntsInsideThe20', # PT20 + 142: 'blockedPunts', # PTB + 145: 'puntTouchbacks', # PTTB + 146: 'puntFairCatches', #PTFC + 147: 'puntAverage', + 148: 'puntAverage44.0+', # PTA44 + 149: 'puntAverage42.0-43.9', #PTA42 + 150: 'puntAverage40.0-41.9', #PTA40 + 151: 'puntAverage38.0-39.9', #PTA38 + 152: 'puntAverage36.0-37.9', #PTA36 + 153: 'puntAverage34.0-35.9', #PTA34 + 154: 'puntAverage33.9AndUnder', #PTA33 # Head Coach Stats - 155: "teamWin", # TW - 156: "teamLoss", # TL - 157: "teamTie", # TIE - 158: "pointsScored", # PTS + 155: 'teamWin', # TW + 156: 'teamLoss', # TL + 157: 'teamTie', # TIE + 158: 'pointsScored', # PTS - 160: "pointsMargin", - 161: "25+pointWinMargin", # WM25 - 162: "20-24pointWinMargin", # WM20 - 163: "15-19pointWinMargin", # WM15 - 164: "10-14pointWinMargin", # WM10 - 165: "5-9pointWinMargin", # WM5 - 166: "1-4pointWinMargin", # WM1 - 167: "1-4pointLossMargin", # LM1 - 168: "5-9pointLossMargin", # LM5 - 169: "10-14pointLossMargin", # LM10 - 170: "15-19pointLossMargin", # LM15 - 171: "20-24pointLossMargin", # LM20 - 172: "25+pointLossMargin", # LM25 - 174: "winPercentage", # Value goes from 0-1 + 160: 'pointsMargin', + 161: '25+pointWinMargin', # WM25 + 162: '20-24pointWinMargin', # WM20 + 163: '15-19pointWinMargin', # WM15 + 164: '10-14pointWinMargin', # WM10 + 165: '5-9pointWinMargin', # WM5 + 166: '1-4pointWinMargin', # WM1 + 167: '1-4pointLossMargin', # LM1 + 168: '5-9pointLossMargin', # LM5 + 169: '10-14pointLossMargin', # LM10 + 170: '15-19pointLossMargin', # LM15 + 171: '20-24pointLossMargin', # LM20 + 172: '25+pointLossMargin', # LM25 + 174: 'winPercentage', # Value goes from 0-1 - 187: "defensivePointsAllowed", # TODO: figure out what the difference is between 187 and 120 + 187: 'defensivePointsAllowed', # TODO: figure out what the difference is between 187 and 120 - 201: "madeFieldGoalsFrom60Plus", # FG60 - 202: "attemptedFieldGoalsFrom60Plus", # FGA60 - 203: "missedFieldGoalsFrom60Plus", # FGM60 + 201: 'madeFieldGoalsFrom60Plus', # FG60 + 202: 'attemptedFieldGoalsFrom60Plus', # FGA60 + 203: 'missedFieldGoalsFrom60Plus', # FGM60 - 205: "defensive2PtReturns", # 2PTRET - 206: "defensive2PtReturns", # 2PTRET - TODO: figure out what the difference is between 206 and 205 + 205: 'defensive2PtReturns', # 2PTRET + 206: 'defensive2PtReturns', # 2PTRET - TODO: figure out what the difference is between 206 and 205 } + +SETTINGS_SCORING_FORMAT_MAP = { + 0: { 'abbr': 'PA', 'label': 'Each Pass Attempted' }, + 1: { 'abbr': 'PC', 'label': 'Each Pass Completed' }, + 2: { 'abbr': 'INC', 'label': 'Each Incomplete Pass' }, + 3: { 'abbr': 'PY', 'label': 'Passing Yards' }, + 4: { 'abbr': 'PTD', 'label': 'TD Pass' }, + 5: { 'abbr': 'PY5', 'label': 'Every 5 passing yards' }, + 6: { 'abbr': 'PY10', 'label': 'Every 10 passing yards' }, + 7: { 'abbr': 'PY20', 'label': 'Every 20 passing yards' }, + 8: { 'abbr': 'PY25', 'label': 'Every 25 passing yards' }, + 9: { 'abbr': 'PY50', 'label': 'Every 50 passing yards' }, + 10: { 'abbr': 'PY100', 'label': 'Every 100 passing yards' }, + 11: { 'abbr': 'PC5', 'label': 'Every 5 pass completions' }, + 12: { 'abbr': 'PC10', 'label': 'Every 10 pass completions' }, + 13: { 'abbr': 'IP5', 'label': 'Every 5 pass incompletions' }, + 14: { 'abbr': 'IP10', 'label': 'Every 10 pass incompletions' }, + 15: { 'abbr': 'PTD40', 'label': '40+ yard TD pass bonus' }, + 16: { 'abbr': 'PTD50', 'label': '50+ yard TD pass bonus' }, + 17: { 'abbr': 'P300', 'label': '300-399 yard passing game' }, + 18: { 'abbr': 'P400', 'label': '400+ yard passing game' }, + 19: { 'abbr': '2PC', 'label': '2pt Passing Conversion' }, + 20: { 'abbr': 'INTT', 'label': 'Interceptions Thrown' }, + 21: { 'abbr': 'CPCT', 'label': 'Passing Completion Pct' }, + 22: { 'abbr': 'PYPG', 'label': 'Passing Yards Per Game' }, + 23: { 'abbr': 'RA', 'label': 'Rushing Attempts' }, + 24: { 'abbr': 'RY', 'label': 'Rushing Yards' }, + 25: { 'abbr': 'RTD', 'label': 'TD Rush' }, + 26: { 'abbr': '2PR', 'label': '2pt Rushing Conversion' }, + 27: { 'abbr': 'RY5', 'label': 'Every 5 rushing yards' }, + 28: { 'abbr': 'RY10', 'label': 'Every 10 rushing yards' }, + 29: { 'abbr': 'RY20', 'label': 'Every 20 rushing yards' }, + 30: { 'abbr': 'RY25', 'label': 'Every 25 rushing yards' }, + 31: { 'abbr': 'RY50', 'label': 'Every 50 rushing yards' }, + 32: { 'abbr': 'R100', 'label': 'Every 100 rushing yards' }, + 33: { 'abbr': 'RA5', 'label': 'Every 5 rush attempts' }, + 34: { 'abbr': 'RA10', 'label': 'Every 10 rush attempts' }, + 35: { 'abbr': 'RTD40', 'label': '40+ yard TD rush bonus' }, + 36: { 'abbr': 'RTD50', 'label': '50+ yard TD rush bonus' }, + 37: { 'abbr': 'RY100', 'label': '100-199 yard rushing game' }, + 38: { 'abbr': 'RY200', 'label': '200+ yard rushing game' }, + 39: { 'abbr': 'RYPA', 'label': 'Rushing Yards Per Attempt' }, + 40: { 'abbr': 'RYPG', 'label': 'Rushing Yards Per Game' }, + 41: { 'abbr': 'RECS', 'label': 'Receptions' }, + 42: { 'abbr': 'REY', 'label': 'Receiving Yards' }, + 43: { 'abbr': 'RETD', 'label': 'TD Reception' }, + 44: { 'abbr': '2PRE', 'label': '2pt Receiving Conversion' }, + 45: { 'abbr': 'RETD40', 'label': '40+ yard TD rec bonus' }, + 46: { 'abbr': 'RETD50', 'label': '50+ yard TD rec bonus' }, + 47: { 'abbr': 'REY5', 'label': 'Every 5 receiving yards' }, + 48: { 'abbr': 'REY10', 'label': 'Every 10 receiving yards' }, + 49: { 'abbr': 'REY20', 'label': 'Every 20 receiving yards' }, + 50: { 'abbr': 'REY25', 'label': 'Every 25 receiving yards' }, + 51: { 'abbr': 'REY50', 'label': 'Every 50 receiving yards' }, + 52: { 'abbr': 'RE100', 'label': 'Every 100 receiving yards' }, + 53: { 'abbr': 'REC', 'label': 'Each reception' }, + 54: { 'abbr': 'REC5', 'label': 'Every 5 receptions'}, + 55: { 'abbr': 'REC10', 'label': 'Every 10 receptions' }, + 56: { 'abbr': 'REY100', 'label': '100-199 yard receiving game' }, + 57: { 'abbr': 'REY200', 'label': '200+ yard receiving game' }, + 58: { 'abbr': 'RET', 'label': 'Receiving Target' }, + 59: { 'abbr': 'YAC', 'label': 'Receiving Yards After Catch' }, + 60: { 'abbr': 'YPC', 'label': 'Receiving Yards Per Catch' }, + 61: { 'abbr': 'REYPG', 'label': 'Receiving Yards Per Game' }, + 62: { 'abbr': 'PTL', 'label': 'Total 2pt Conversions' }, + 63: { 'abbr': 'FTD', 'label': 'Fumble Recovered for TD' }, + 64: { 'abbr': 'SKD', 'label': 'Sacked' }, + 65: { 'abbr': 'PFUM', 'label': 'Passing Fumbles' }, + 66: { 'abbr': 'RFUM', 'label': 'Rushing Fumbles' }, + 67: { 'abbr': 'REFUM', 'label': 'Receiving Fumbles' }, + 68: { 'abbr': 'FUM', 'label': 'Total Fumbles' }, + 69: { 'abbr': 'PFUML', 'label': 'Passing Fumbles Lost' }, + 70: { 'abbr': 'RFUML', 'label': 'Rushing Fumbles Lost' }, + 71: { 'abbr': 'REFUML', 'label': 'Receiving Fumbles Lost' }, + 72: { 'abbr': 'FUML', 'label': 'Total Fumbles Lost' }, + 73: { 'abbr': 'TT', 'label': 'Total Turnovers' }, + 74: { 'abbr': 'FG50P', 'label': 'FG Made (50+ yards)' }, + 75: { 'abbr': 'FGA50P', 'label': 'FG Attempted (50+ yards)' }, + 76: { 'abbr': 'FGM50P', 'label': 'FG Missed (50+ yards)' }, + 77: { 'abbr': 'FG40', 'label': 'FG Made (40-49 yards)' }, + 78: { 'abbr': 'FGA40', 'label': 'FG Attempted (40-49 yards)' }, + 79: { 'abbr': 'FGM40', 'label': 'FG Missed (40-49 yards)' }, + 80: { 'abbr': 'FG0', 'label': 'FG Made (0-39 yards)' }, + 81: { 'abbr': 'FGA0', 'label': 'FG Attempted (0-39 yards)' }, + 82: { 'abbr': 'FGM0', 'label': 'FG Missed (0-39 yards)' }, + 83: { 'abbr': 'FG', 'label': 'Total FG Made' }, + 84: { 'abbr': 'FGA', 'label': 'Total FG Attempted' }, + 85: { 'abbr': 'FGM', 'label': 'Total FG Missed' }, + 86: { 'abbr': 'PAT', 'label': 'Each PAT Made' }, + 87: { 'abbr': 'PATA', 'label': 'Each PAT Attempted' }, + 88: { 'abbr': 'PATM', 'label': 'Each PAT Missed' }, + 89: { 'abbr': 'PA0', 'label': '0 points allowed' }, + 90: { 'abbr': 'PA1', 'label': '1-6 points allowed' }, + 91: { 'abbr': 'PA7', 'label': '7-13 points allowed' }, + 92: { 'abbr': 'PA14', 'label': '14-17 points allowed' }, + 93: { 'abbr': 'BLKKRTD', 'label': 'Blocked Punt or FG return for TD' }, + 94: { 'abbr': 'DEFRETTD', 'label': 'Fumble or INT Return for TD' }, + 95: { 'abbr': 'INT', 'label': 'Each Interception' }, + 96: { 'abbr': 'FR', 'label': 'Each Fumble Recovered' }, + 97: { 'abbr': 'BLKK', 'label': 'Blocked Punt, PAT or FG' }, + 98: { 'abbr': 'SF', 'label': 'Each Safety' }, + 99: { 'abbr': 'SK', 'label': 'Each Sack' }, + 100: { 'abbr': 'HALFSK', 'label': '1/2 Sack' }, + 101: { 'abbr': 'KRTD', 'label': 'Kickoff Return TD' }, + 102: { 'abbr': 'PRTD', 'label': 'Punt Return TD' }, + 103: { 'abbr': 'INTTD', 'label': 'Interception Return TD' }, + 104: { 'abbr': 'FRTD', 'label': 'Fumble Return TD' }, + 105: { 'abbr': 'TRTD', 'label': 'Total Return TD' }, + 106: { 'abbr': 'FF', 'label': 'Each Fumble Forced' }, + 107: { 'abbr': 'TKA', 'label': 'Assisted Tackles' }, + 108: { 'abbr': 'TKS', 'label': 'Solo Tackles' }, + 109: { 'abbr': 'TK', 'label': 'Total Tackles' }, + 110: { 'abbr': 'TK3', 'label': 'Every 3 Total Tackles' }, + 111: { 'abbr': 'TK5', 'label': 'Every 5 Total Tackles' }, + 112: { 'abbr': 'STF', 'label': 'Stuffs' }, + 113: { 'abbr': 'PD', 'label': 'Passes Defensed' }, + 114: { 'abbr': 'KR', 'label': 'Kickoff Return Yards' }, + 115: { 'abbr': 'PR', 'label': 'Punt Return Yards' }, + 116: { 'abbr': 'KR10', 'label': 'Every 10 kickoff return yards' }, + 117: { 'abbr': 'KR25', 'label': 'Every 25 kickoff return yards' }, + 118: { 'abbr': 'PR10', 'label': 'Every 10 punt return yards' }, + 119: { 'abbr': 'PR25', 'label': 'Every 25 punt return yards' }, + 120: { 'abbr': 'PTSA', 'label': 'Points Allowed' }, + 121: { 'abbr': 'PA18', 'label': '18-21 points allowed' }, + 122: { 'abbr': 'PA22', 'label': '22-27 points allowed' }, + 123: { 'abbr': 'PA28', 'label': '28-34 points allowed' }, + 124: { 'abbr': 'PA35', 'label': '35-45 points allowed' }, + 125: { 'abbr': 'PA46', 'label': '46+ points allowed' }, + 126: { 'abbr': 'PAPG', 'label': 'Points Allowed Per Game' }, + 127: { 'abbr': 'YA', 'label': 'Yards Allowed' }, + 128: { 'abbr': 'YA100', 'label': 'Less than 100 total yards allowed' }, + 129: { 'abbr': 'YA199', 'label': '100-199 total yards allowed' }, + 130: { 'abbr': 'YA299', 'label': '200-299 total yards allowed' }, + 131: { 'abbr': 'YA349', 'label': '300-349 total yards allowed' }, + 132: { 'abbr': 'YA399', 'label': '350-399 total yards allowed' }, + 133: { 'abbr': 'YA449', 'label': '400-449 total yards allowed' }, + 134: { 'abbr': 'YA499', 'label': '450-499 total yards allowed' }, + 135: { 'abbr': 'YA549', 'label': '500-549 total yards allowed' }, + 136: { 'abbr': 'YA550', 'label': '550+ total yards allowed' }, + 137: { 'abbr': 'YAPG', 'label': 'Yards Allowed Per Game' }, + 138: { 'abbr': 'PT', 'label': 'Net Punts' }, + 139: { 'abbr': 'PTY', 'label': 'Punt Yards' }, + 140: { 'abbr': 'PT10', 'label': 'Punts Inside the 10' }, + 141: { 'abbr': 'PT20', 'label': 'Punts Inside the 20' }, + 142: { 'abbr': 'PTB', 'label': 'Blocked Punts' }, + 143: { 'abbr': 'PTR', 'label': 'Punts Returned' }, + 144: { 'abbr': 'PTRY', 'label': 'Punt Return Yards' }, + 145: { 'abbr': 'PTTB', 'label': 'Touchbacks' }, + 146: { 'abbr': 'PTFC', 'label': 'Fair Catches' }, + 147: { 'abbr': 'PTAVG', 'label': 'Punt Average' }, + 148: { 'abbr': 'PTA44', 'label': 'Punt Average 44.0+' }, + 149: { 'abbr': 'PTA42', 'label': 'Punt Average 42.0-43.9' }, + 150: { 'abbr': 'PTA40', 'label': 'Punt Average 40.0-41.9' }, + 151: { 'abbr': 'PTA38', 'label': 'Punt Average 38.0-39.9' }, + 152: { 'abbr': 'PTA36', 'label': 'Punt Average 36.0-37.9' }, + 153: { 'abbr': 'PTA34', 'label': 'Punt Average 34.0-35.9' }, + 154: { 'abbr': 'PTA33', 'label': 'Punt Average 33.9 or less' }, + 155: { 'abbr': 'TW', 'label': 'Team Win' }, + 156: { 'abbr': 'TL', 'label': 'Team Loss' }, + 157: { 'abbr': 'TIE', 'label': 'Team Tie' }, + 158: { 'abbr': 'PTS', 'label': 'Points Scored' }, + 159: { 'abbr': 'PPG', 'label': 'Points Scored Per Game' }, + 160: { 'abbr': 'MGN', 'label': 'Margin of Victory' }, + 161: { 'abbr': 'WM25', 'label': '25+ point Win Margin' }, + 162: { 'abbr': 'WM20', 'label': '20-24 point Win Margin' }, + 163: { 'abbr': 'WM15', 'label': '15-19 point Win Margin' }, + 164: { 'abbr': 'WM10', 'label': '10-14 point Win Margin' }, + 165: { 'abbr': 'WM5', 'label': '5-9 point Win Margin' }, + 166: { 'abbr': 'WM1', 'label': '1-4 point Win Margin' }, + 167: { 'abbr': 'LM1', 'label': '1-4 point Loss Margin' }, + 168: { 'abbr': 'LM5', 'label': '5-9 point Loss Margin' }, + 169: { 'abbr': 'LM10', 'label': '10-14 point Loss Margin' }, + 170: { 'abbr': 'LM15', 'label': '15-19 point Loss Margin' }, + 171: { 'abbr': 'LM20', 'label': '20-24 point Loss Margin' }, + 172: { 'abbr': 'LM25', 'label': '25+ point Loss Margin' }, + 173: { 'abbr': 'MGNPG', 'label': 'Margin of Victory Per Game' }, + 174: { 'abbr': 'WINPCT', 'label': 'Winning Pct' }, + 175: { 'abbr': 'PTD0', 'label': '0-9 yd TD pass bonus' }, + 176: { 'abbr': 'PTD10', 'label': '10-19 yd TD pass bonus' }, + 177: { 'abbr': 'PTD20', 'label': '20-29 yd TD pass bonus' }, + 178: { 'abbr': 'PTD30', 'label': '30-39 yd TD pass bonus' }, + 179: { 'abbr': 'RTD0', 'label': '0-9 yd TD rush bonus' }, + 180: { 'abbr': 'RTD10', 'label': '10-19 yd TD rush bonus' }, + 181: { 'abbr': 'RTD20', 'label': '20-29 yd TD rush bonus' }, + 182: { 'abbr': 'RTD30', 'label': '30-39 yd TD rush bonus' }, + 183: { 'abbr': 'RETD0', 'label': '0-9 yd TD rec bonus' }, + 184: { 'abbr': 'RETD10', 'label': '10-19 yd TD rec bonus' }, + 185: { 'abbr': 'RETD20', 'label': '20-29 yd TD rec bonus' }, + 186: { 'abbr': 'RETD30', 'label': '30-39 yd TD rec bonus' }, + 187: { 'abbr': 'DPTSA', 'label': 'D/ST Points Allowed' }, + 188: { 'abbr': 'DPA0', 'label': 'D/ST 0 points allowed' }, + 189: { 'abbr': 'DPA1', 'label': 'D/ST 1-6 points allowed' }, + 190: { 'abbr': 'DPA7', 'label': 'D/ST 7-13 points allowed' }, + 191: { 'abbr': 'DPA14', 'label': 'D/ST 14-17 points allowed' }, + 192: { 'abbr': 'DPA18', 'label': 'D/ST 18-21 points allowed' }, + 193: { 'abbr': 'DPA22', 'label': 'D/ST 22-27 points allowed' }, + 194: { 'abbr': 'DPA28', 'label': 'D/ST 28-34 points allowed' }, + 195: { 'abbr': 'DPA35', 'label': 'D/ST 35-45 points allowed' }, + 196: { 'abbr': 'DPA46', 'label': 'D/ST 46+ points allowed' }, + 197: { 'abbr': 'DPAPG', 'label': 'D/ST Points Allowed Per Game' }, + 198: { 'abbr': 'FG50', 'label': 'FG Made (50-59 yards)' }, + 199: { 'abbr': 'FGA50', 'label': 'FG Attempted (50-59 yards)' }, + 200: { 'abbr': 'FGM50', 'label': 'FG Missed (50-59 yards)' }, + 201: { 'abbr': 'FG60', 'label': 'FG Made (60+ yards)' }, + 202: { 'abbr': 'FGA60', 'label': 'FG Attempted (60+ yards)' }, + 203: { 'abbr': 'FGM60', 'label': 'FG Missed (60+ yards)' }, + 204: { 'abbr': 'O2PRET', 'label': 'Offensive 2pt Return' }, + 205: { 'abbr': 'D2PRET', 'label': 'Defensive 2pt Return' }, + 206: { 'abbr': '2PRET', 'label': '2pt Return' }, + 207: { 'abbr': 'O1PSF', 'label': 'Offensive 1pt Safety' }, + 208: { 'abbr': 'D1PSF', 'label': 'Defensive 1pt Safety' }, + 209: { 'abbr': '1PSF', 'label': '1pt Safety' }, + 210: { 'abbr': 'GP', 'label': 'Games Played' }, + 211: { 'abbr': 'PFD', 'label': 'Passing First Down' }, + 212: { 'abbr': 'RFD', 'label': 'Rushing First Down' }, + 213: { 'abbr': 'REFD', 'label': 'Receiving First Down' }, + 214: { 'abbr': 'FGY', 'label': 'FG Made Yards' }, + 215: { 'abbr': 'FGMY', 'label': 'FG Missed Yards' }, + 216: { 'abbr': 'FGAY', 'label': 'FG Attempt Yards' }, + 217: { 'abbr': 'FGY5', 'label': 'Every 5 FG Made yards' }, + 218: { 'abbr': 'FGY10', 'label': 'Every 10 FG Made yards' }, + 219: { 'abbr': 'FGY20', 'label': 'Every 20 FG Made yards' }, + 220: { 'abbr': 'FGY25', 'label': 'Every 25 FG Made yards' }, + 221: { 'abbr': 'FGY50', 'label': 'Every 50 FG Made yards' }, + 222: { 'abbr': 'FGY100', 'label': 'Every 100 FG Made yards' }, + 223: { 'abbr': 'FGMY5', 'label': 'Every 5 FG Missed yards' }, + 224: { 'abbr': 'FGMY10', 'label': 'Every 10 FG Missed yards' }, + 225: { 'abbr': 'FGMY20', 'label': 'Every 20 FG Missed yards' }, + 226: { 'abbr': 'FGMY25', 'label': 'Every 25 FG Missed yards' }, + 227: { 'abbr': 'FGMY50', 'label': 'Every 50 FG Missed yards' }, + 228: { 'abbr': 'FGMY100', 'label': 'Every 100 FG Missed yards' }, + 229: { 'abbr': 'FGAY5', 'label': 'Every 5 FG Attempt yards' }, + 230: { 'abbr': 'FGAY10', 'label': 'Every 10 FG Attempt yards' }, + 231: { 'abbr': 'FGAY20', 'label': 'Every 20 FG Attempt yards' }, + 232: { 'abbr': 'FGAY25', 'label': 'Every 25 FG Attempt yards' }, + 233: { 'abbr': 'FGAY50', 'label': 'Every 50 FG Attempt yards' }, + 234: { 'abbr': 'FGAY100', 'label': 'Every 100 FG Attempt yards' } +} \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/football/helper.py b/fantasy_app/espn_api_submodule/espn_api/football/helper.py new file mode 100644 index 0000000..16ccd28 --- /dev/null +++ b/fantasy_app/espn_api_submodule/espn_api/football/helper.py @@ -0,0 +1,211 @@ +import random +from typing import Callable, Dict, List, Tuple + + +def build_division_record_dict(team_data_list: List[Dict]) -> Dict: + """Create a DataFrame with each team's divisional record.""" + # Create a dictionary with each team's divisional record + div_outcomes = { + team_data["team_id"]: {"wins": 0, "divisional_games": 0} + for team_data in team_data_list + } + + # Loop through each team's schedule and outcomes and build the dictionary + for team_data in team_data_list: + team = team_data["team"] + for opp, outcome in zip(team_data["schedule"], team_data["outcomes"]): + if team_data["division_id"] == opp.division_id: + if outcome == "W": + div_outcomes[team_data["team_id"]]["wins"] += 1 + if outcome == "T": + div_outcomes[team_data["team_id"]]["wins"] += 0.5 + + div_outcomes[team_data["team_id"]]["divisional_games"] += 1 + + # Calculate the divisional record + div_record = { + team_data["team_id"]: ( + div_outcomes[team_data["team_id"]]["wins"] + / max(div_outcomes[team_data["team_id"]]["divisional_games"], 1) + ) + for team_data in team_data_list + } + + return div_record + + +def build_h2h_dict(team_data_list: List[Dict]) -> Dict: + """Create a dictionary with each team's divisional record.""" + # Create a dictionary with each team's head to head record + h2h_outcomes = { + team_data["team_id"]: { + opp["team_id"]: {"h2h_wins": 0, "h2h_games": 0} + for opp in team_data_list + if opp["team_id"] != team_data["team_id"] + } + for team_data in team_data_list + } + + # Loop through each team's schedule and outcomes and build the dictionary + for team_data in team_data_list: + team = team_data["team"] + for opp, outcome in zip(team_data["schedule"], team_data["outcomes"]): + # Ignore teams that are not part of this tiebreaker + if opp.team_id not in h2h_outcomes[team.team_id].keys(): + continue + + # Add the outcome to the dictionary + if outcome == "W": + h2h_outcomes[team.team_id][opp.team_id]["h2h_wins"] += 1 + if outcome == "T": + h2h_outcomes[team.team_id][opp.team_id]["h2h_wins"] += 0.5 + + h2h_outcomes[team.team_id][opp.team_id]["h2h_games"] += 1 + + # # Calculate the head to head record + # for team_data in team_data_list: + # for opp_data in team_data_list: + # h2h_outcomes[team_data["team_id"]][opp_data["team_id"]]["h2h_record"] = ( + # h2h_outcomes[team_data["team_id"]][opp_data["team_id"]]["h2h_wins"] + # / max( + # h2h_outcomes[team_data["team_id"]][opp_data["team_id"]][ + # "h2h_games" + # ], + # 1, + # ) + # ) + + return h2h_outcomes + + +def sort_by_win_pct(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return sorted(team_data_list, key=lambda x: x["win_pct"], reverse=True) + + +def sort_by_points_for(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return sorted(team_data_list, key=lambda x: x["points_for"], reverse=True) + + +def sort_by_division_record(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 3rd level tiebreaker""" + division_records = build_division_record_dict(team_data_list) + for team_data in team_data_list: + team_data["division_record"] = division_records[team_data["team_id"]] + return sorted(team_data_list, key=lambda x: x["division_record"], reverse=True) + + +def sort_by_points_against(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 4th level tiebreaker""" + return sorted(team_data_list, key=lambda x: x["points_against"], reverse=True) + + +def sort_by_coin_flip(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 5th level tiebreaker""" + for team_data in team_data_list: + team_data["coin_flip"] = random.random() + return sorted(team_data_list, key=lambda x: x["coin_flip"], reverse=True) + + +def sort_by_head_to_head( + team_data_list: List[Dict], +) -> List[Dict]: + """Take a list of team standings data and sort it using the H2H_RECORD tiebreaker""" + # Create a dictionary with each team's head to head record + h2h_dict = build_h2h_dict(team_data_list) + + # If there is only one team, return the dataframe as-is + if len(team_data_list) < 2: + return team_data_list + + # If there are only two teams, sort descending by H2H wins + elif len(h2h_dict) == 2: + # Filter the H2H DataFrame to only include the teams in question + h2h_dict = build_h2h_dict(team_data_list) + + # Sum the H2H wins against all tied opponents + for team_data in team_data_list: + team_data["h2h_wins"] = sum( + h2h_dict[team_data["team_id"]][opp_id]["h2h_wins"] + for opp_id in h2h_dict[team_data["team_id"]].keys() + ) + return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) + + # If there are more than two teams... + else: + # Filter the H2H DataFrame to only include the teams in question + h2h_dict = build_h2h_dict(team_data_list) + + # Check if the teams have all played each other an equal number of times + matchup_counts = [ + h2h_dict[team_id][opp_id]["h2h_games"] + for team_id in h2h_dict.keys() + for opp_id in h2h_dict[team_id].keys() + ] + if len(set(matchup_counts)) == 1: + # All teams have played each other an equal number of times + # Sort the teams by total H2H wins against each other + for team_data in team_data_list: + team_data["h2h_wins"] = sum( + h2h_dict[team_data["team_id"]][opp_id]["h2h_wins"] + for opp_id in h2h_dict[team_data["team_id"]].keys() + ) + return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) + else: + # All teams have not played each other an equal number of times + # This tiebreaker is invalid + for team_data in team_data_list: + team_data["h2h_wins"] = 0 + return team_data_list + + +def sort_team_data_list( + team_data_list: List[Dict], + tiebreaker_hierarchy: List[Tuple[Callable, str]], +) -> List[Dict]: + """This recursive function sorts a list of team standings data by the tiebreaker hierarchy. + It iterates through each tiebreaker, sorting any remaning ties by the next tiebreaker. + + Args: + team_data_list (List[Dict]): List of team data dictionaries + tiebreaker_hierarchy (List[Tuple[Callable, str]]): List of tiebreaker functions and columns to sort by + + Returns: + List[Dict]: Sorted list of team data dictionaries + """ + # If there are no more tiebreakers, return the standings list as-is + if not tiebreaker_hierarchy: + return team_data_list + + # If there is only one team to sort, return the standings list as-is + if len(team_data_list) == 1: + return team_data_list + + # Get the tiebreaker function and column name to group by + tiebreaker_function = tiebreaker_hierarchy[0][0] + tiebreaker_col = tiebreaker_hierarchy[0][1] + + # Apply the tiebreaker function to the standings list + team_data_list = tiebreaker_function(team_data_list) + + # Loop through each remaining unique tiebreaker value to see if ties remain + sorted_team_data_list = [] + for val in sorted( + set([team_data[tiebreaker_col] for team_data in team_data_list]), + reverse=True, + ): + # Filter the standings list to only include the teams with the current tiebreaker value + team_data_subset = [ + team_data + for team_data in team_data_list + if team_data[tiebreaker_col] == val + ] + + # Append the sorted subset to the final sorted standings list + sorted_team_data_list = sorted_team_data_list + sort_team_data_list( + team_data_subset, + tiebreaker_hierarchy[1:], + ) + + return sorted_team_data_list \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/football/league.py b/fantasy_app/espn_api_submodule/espn_api/football/league.py index aa5f11e..9cda112 100644 --- a/fantasy_app/espn_api_submodule/espn_api/football/league.py +++ b/fantasy_app/espn_api_submodule/espn_api/football/league.py @@ -1,24 +1,33 @@ -import datetime -import time import json -from typing import List, Tuple, Union +import random +from typing import Callable, Dict, List, Tuple, Union from ..base_league import BaseLeague from .team import Team from .matchup import Matchup -from .pick import Pick from .box_score import BoxScore from .box_player import BoxPlayer from .player import Player from .activity import Activity +from .settings import Settings from .utils import power_points, two_step_dominance from .constant import POSITION_MAP, ACTIVITY_MAP +from .helper import ( + sort_by_coin_flip, + sort_by_division_record, + sort_by_head_to_head, + sort_by_points_against, + sort_by_points_for, + sort_by_win_pct, + sort_team_data_list, +) + class League(BaseLeague): '''Creates a League instance for Public/Private ESPN league''' def __init__(self, league_id: int, year: int, espn_s2=None, swid=None, fetch_league=True, debug=False): super().__init__(league_id=league_id, year=year, sport='nfl', espn_s2=espn_s2, swid=swid, debug=debug) - + if fetch_league: self.fetch_league() @@ -26,12 +35,12 @@ def fetch_league(self): self._fetch_league() def _fetch_league(self): - data = super()._fetch_league() + data = super()._fetch_league(SettingsClass=Settings) self.nfl_week = data['status']['latestScoringPeriod'] self._fetch_players() self._fetch_teams(data) - self._fetch_draft() + super()._fetch_draft() def _fetch_teams(self, data): '''Fetch teams in league''' @@ -51,28 +60,6 @@ def _fetch_teams(self, data): mov = team.scores[week] - opponent.scores[week] team.mov.append(mov) - def _fetch_draft(self): - '''Creates list of Pick objects from the leagues draft''' - data = self.espn_request.get_league_draft() - - # League has not drafted yet - if not data['draftDetail']['drafted']: - return - - picks = data['draftDetail']['picks'] - for pick in picks: - team = self.get_team_data(pick['teamId']) - playerId = pick['playerId'] - playerName = '' - if playerId in self.player_map: - playerName = self.player_map[playerId] - round_num = pick['roundId'] - round_pick = pick['roundPickNumber'] - bid_amount = pick['bidAmount'] - keeper_status = pick['keeper'] - nominatingTeam = self.get_team_data(pick['nominatingTeamId']) - self.draft.append(Pick(team, playerId, playerName, round_num, round_pick, bid_amount, keeper_status, nominatingTeam)) - def _get_positional_ratings(self, week: int): params = { 'view': 'mPositionalRatings', @@ -95,6 +82,14 @@ def refresh(self): self.nfl_week = data['status']['latestScoringPeriod'] self._fetch_teams(data) + + def refresh_draft(self, refresh_players=False, refresh__teams=False): + super()._fetch_draft() + if refresh_players: + self._fetch_players() + if refresh__teams: + self._fetch_teams(data) + def load_roster_week(self, week: int) -> None: '''Sets Teams Roster for a Certain Week''' params = { @@ -115,6 +110,106 @@ def standings(self) -> List[Team]: standings = sorted(self.teams, key=lambda x: x.final_standing if x.final_standing != 0 else x.standing, reverse=False) return standings + def standings_weekly(self, week: int) -> List[Team]: + """This is the main function to get the standings for a given week. + + It controls the tiebreaker hierarchy and calls the recursive League()._sort_team_data_list function. + First, the division winners must be determined. Then, the rest of the teams are sorted. + + The standard tiebreaker hierarchy is: + 1. Head-to-head record among the tied teams + 2. Total points scored for the season + 3. Division record (if all tied teams are in the same division) + 4. Total points scored against for the season + 5. Coin flip + + Args: + week (int): Week to get the standings for + + Returns: + List[Dict]: Sorted standings list + """ + # Return empty standings if no matchup periods have completed yet + if self.currentMatchupPeriod <= 1: + return self.standings() + + # Get standings data for each team up to the given week + list_of_team_data = [] + for team in self.teams: + team_data = { + "team": team, + "team_id": team.team_id, + "division_id": team.division_id, + "wins": sum([1 for outcome in team.outcomes[:week] if outcome == "W"]), + "ties": sum([1 for outcome in team.outcomes[:week] if outcome == "T"]), + "losses": sum( + [1 for outcome in team.outcomes[:week] if outcome == "L"] + ), + "points_for": sum(team.scores[:week]), + "points_against": sum( + [team.schedule[w].scores[w] for w in range(week)] + ), + "schedule": team.schedule[:week], + "outcomes": team.outcomes[:week], + } + team_data["win_pct"] = (team_data["wins"] + team_data["ties"] / 2) / sum( + [1 for outcome in team.outcomes[:week] if outcome in ["W", "T", "L"]] + ) + list_of_team_data.append(team_data) + + # Identify the proper tiebreaker hierarchy + if self.settings.playoff_seed_tie_rule == "TOTAL_POINTS_SCORED": + tiebreaker_hierarchy = [ + (sort_by_win_pct, "win_pct"), + (sort_by_points_for, "points_for"), + (sort_by_head_to_head, "h2h_wins"), + (sort_by_division_record, "division_record"), + (sort_by_points_against, "points_against"), + (sort_by_coin_flip, "coin_flip"), + ] + elif self.settings.playoff_seed_tie_rule == "H2H_RECORD": + tiebreaker_hierarchy = [ + (sort_by_win_pct, "win_pct"), + (sort_by_head_to_head, "h2h_wins"), + (sort_by_points_for, "points_for"), + (sort_by_division_record, "division_record"), + (sort_by_points_against, "points_against"), + (sort_by_coin_flip, "coin_flip"), + ] + else: + raise ValueError( + "Unkown tiebreaker_method: Must be either 'TOTAL_POINTS_SCORED' or 'H2H_RECORD'" + ) + + # First assign the division winners + division_winners = [] + for division_id in list(self.settings.division_map.keys()): + division_teams = [ + team_data + for team_data in list_of_team_data + if team_data["division_id"] == division_id + ] + division_winner = sort_team_data_list(division_teams, tiebreaker_hierarchy)[ + 0 + ] + division_winners.append(division_winner) + list_of_team_data.remove(division_winner) + + # Sort the division winners + sorted_division_winners = sort_team_data_list( + division_winners, tiebreaker_hierarchy + ) + + # Then sort the rest of the teams + sorted_rest_of_field = sort_team_data_list( + list_of_team_data, tiebreaker_hierarchy + ) + + # Combine all teams + sorted_team_data = sorted_division_winners + sorted_rest_of_field + + return [team_data["team"] for team_data in sorted_team_data] + def top_scorer(self) -> Team: most_pf = sorted(self.teams, key=lambda x: x.points_for, reverse=True) return most_pf[0] @@ -143,13 +238,8 @@ def least_scored_week(self) -> Tuple[Team, int]: least_tup = sorted(least_scored_tup, key=lambda tup: int(tup[1]), reverse=False) return least_tup[0] - def get_team_data(self, team_id: int) -> Team: - for team in self.teams: - if team_id == team.team_id: - return team - return None - def recent_activity(self, size: int = 25, msg_type: str = None) -> List[Activity]: + def recent_activity(self, size: int = 25, msg_type: str = None, offset: int = 0) -> List[Activity]: '''Returns a list of recent league activities (Add, Drop, Trade)''' if self.year < 2019: raise Exception('Cant use recent activity before 2019') @@ -161,7 +251,7 @@ def recent_activity(self, size: int = 25, msg_type: str = None) -> List[Activity 'view': 'kona_league_communication' } - filters = {"topics":{"filterType":{"value":["ACTIVITY_TRANSACTIONS"]},"limit":size,"limitPerMessageSet":{"value":25},"offset":0,"sortMessageDate":{"sortPriority":1,"sortAsc":False},"sortFor":{"sortPriority":2,"sortAsc":False},"filterIncludeMessageTypeIds":{"value":msg_types}}} + filters = {"topics":{"filterType":{"value":["ACTIVITY_TRANSACTIONS"]},"limit":size,"limitPerMessageSet":{"value":25},"offset":offset,"sortMessageDate":{"sortPriority":1,"sortAsc":False},"sortFor":{"sortPriority":2,"sortAsc":False},"filterIncludeMessageTypeIds":{"value":msg_types}}} headers = {'x-fantasy-filter': json.dumps(filters)} data = self.espn_request.league_get(extend='/communication/', params=params, headers=headers) data = data['topics'] @@ -184,9 +274,9 @@ def scoreboard(self, week: int = None) -> List[Matchup]: for team in self.teams: for matchup in matchups: - if matchup.home_team == team.team_id: + if matchup._home_team_id == team.team_id: matchup.home_team = team - elif matchup.away_team == team.team_id: + elif matchup._away_team_id == team.team_id: matchup.away_team = team return matchups @@ -296,3 +386,14 @@ def player_info(self, name: str = None, playerId: Union[int, list] = None) -> Un if len(data['players']) > 1: return [Player(player, self.year) for player in data['players']] + def message_board(self, msg_types: List[str] = None): + ''' Returns a list of league messages''' + data = self.espn_request.get_league_message_board(msg_types) + + msg_topics = list(data.get('topicsByType', {}).keys()) + messages = [] + for topic in msg_topics: + msgs = data['topicsByType'][topic] + for msg in msgs: + messages.append(msg) + return messages \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/football/player.py b/fantasy_app/espn_api_submodule/espn_api/football/player.py index ac856fd..3871689 100644 --- a/fantasy_app/espn_api_submodule/espn_api/football/player.py +++ b/fantasy_app/espn_api_submodule/espn_api/football/player.py @@ -12,6 +12,7 @@ def __init__(self, data, year): self.proTeam = PRO_TEAM_MAP[json_parsing(data, 'proTeamId')] self.injuryStatus = json_parsing(data, 'injuryStatus') self.onTeamId = json_parsing(data, 'onTeamId') + self.lineupSlot = POSITION_MAP.get(data.get('lineupSlotId'), '') self.stats = {} # Get players main position @@ -27,6 +28,7 @@ def __init__(self, data, year): self.percent_owned = round(player.get('ownership', {}).get('percentOwned', -1), 2) self.percent_started = round(player.get('ownership', {}).get('percentStarted', -1), 2) + self.active_status = 'bye' player_stats = player.get('stats', []) for stats in player_stats: if stats.get('seasonId') != year: @@ -34,16 +36,25 @@ def __init__(self, data, year): stats_breakdown = stats.get('stats') or stats.get('appliedStats', {}) breakdown = {PLAYER_STATS_MAP.get(int(k), k):v for (k,v) in stats_breakdown.items()} points = round(stats.get('appliedTotal', 0), 2) + avg_points = round(stats.get('appliedAverage', 0), 2) scoring_period = stats.get('scoringPeriodId') stat_source = stats.get('statSourceId') - (points_type, breakdown_type) = ('points', 'breakdown') if stat_source == 0 else ('projected_points', 'projected_breakdown') + (points_type, breakdown_type, avg_type) = ('points', 'breakdown', 'avg_points') if stat_source == 0 else ('projected_points', 'projected_breakdown', 'projected_avg_points') if self.stats.get(scoring_period): self.stats[scoring_period][points_type] = points self.stats[scoring_period][breakdown_type] = breakdown + self.stats[scoring_period][avg_type] = avg_points else: - self.stats[scoring_period] = {points_type: points, breakdown_type: breakdown} + self.stats[scoring_period] = {points_type: points, breakdown_type: breakdown, avg_type: avg_points} + if not stat_source: + if not self.stats[scoring_period][breakdown_type]: + self.active_status = 'inactive' + else: + self.active_status = 'active' self.total_points = self.stats.get(0, {}).get('points', 0) self.projected_total_points = self.stats.get(0, {}).get('projected_points', 0) + self.avg_points = self.stats.get(0, {}).get('avg_points', 0) + self.projected_avg_points = self.stats.get(0, {}).get('projected_avg_points', 0) def __repr__(self): - return f'Player({self.name})' + return f'Player({self.name})' \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/football/settings.py b/fantasy_app/espn_api_submodule/espn_api/football/settings.py new file mode 100644 index 0000000..ef087fa --- /dev/null +++ b/fantasy_app/espn_api_submodule/espn_api/football/settings.py @@ -0,0 +1,20 @@ +from ..base_settings import BaseSettings +from .constant import SETTINGS_SCORING_FORMAT_MAP, POSITION_MAP + +class Settings(BaseSettings): + def __init__(self, data): + super().__init__(data) + self.scoring_format = [] + scoring_items = data['scoringSettings'].get('scoringItems', []) + lineup_slot_counts = data['rosterSettings'].get('lineupSlotCounts', {}) + position_labels = list(POSITION_MAP.values())[:len(lineup_slot_counts)] + self.position_slot_counts = dict(zip(position_labels,list(lineup_slot_counts.values()))) + + for scoring_item in scoring_items: + stat_id = scoring_item['statId'] + points_override = scoring_item.get('pointsOverrides', {}).get('16') + + scoring_type = SETTINGS_SCORING_FORMAT_MAP.get(stat_id, { 'abbr': 'Unknown', 'label': 'Unknown' }) + scoring_type['id'] = stat_id + scoring_type['points'] = points_override or scoring_item.get('points', 0) + self.scoring_format.append(scoring_type) \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/football/team.py b/fantasy_app/espn_api_submodule/espn_api/football/team.py index 4cf01ea..eb64137 100644 --- a/fantasy_app/espn_api_submodule/espn_api/football/team.py +++ b/fantasy_app/espn_api_submodule/espn_api/football/team.py @@ -1,42 +1,31 @@ from .player import Player - class Team(object): '''Teams are part of the league''' - - def __init__(self, data, roster, member, schedule, year, **kwargs): + def __init__(self, data, roster, schedule, year, **kwargs): self.team_id = data['id'] self.team_abbrev = data['abbrev'] self.team_name = data.get('name', 'Unknown') if self.team_name == 'Unknown': - self.team_name = "%s %s" % ( - data.get('location', 'Unknown'), data.get('nickname', 'Unknown')) + self.team_name = "%s %s" % (data.get('location', 'Unknown'), data.get('nickname', 'Unknown')) self.division_id = data['divisionId'] - self.division_name = '' # set by caller + self.division_name = '' # set by caller self.wins = data['record']['overall']['wins'] self.losses = data['record']['overall']['losses'] self.ties = data['record']['overall']['ties'] self.points_for = data['record']['overall']['pointsFor'] - self.points_against = round( - data['record']['overall']['pointsAgainst'], 2) - self.acquisitions = data.get( - 'transactionCounter', {}).get('acquisitions', 0) - self.acquisition_budget_spent = data.get( - 'transactionCounter', {}).get('acquisitionBudgetSpent', 0) + self.points_against = round(data['record']['overall']['pointsAgainst'], 2) + self.acquisitions = data.get('transactionCounter', {}).get('acquisitions', 0) + self.acquisition_budget_spent = data.get('transactionCounter', {}).get('acquisitionBudgetSpent', 0) self.drops = data.get('transactionCounter', {}).get('drops', 0) self.trades = data.get('transactionCounter', {}).get('trades', 0) - self.playoff_pct = data.get( - 'currentSimulationResults', {}).get('playoffPct', 0) * 100 + self.playoff_pct = data.get('currentSimulationResults', {}).get('playoffPct', 0) * 100 self.draft_projected_rank = data.get('draftDayProjectedRank', 0) - self.owner = 'None' - if member: - self.owner = "%s %s" % (member['firstName'], - member['lastName']) self.streak_length = data['record']['overall']['streakLength'] self.streak_type = data['record']['overall']['streakType'] self.standing = data['playoffSeed'] self.final_standing = data['rankCalculatedFinal'] - if 'logo' in data: + if 'logo' in data: self.logo_url = data['logo'] else: self.logo_url = '' @@ -46,12 +35,12 @@ def __init__(self, data, roster, member, schedule, year, **kwargs): self.outcomes = [] self.mov = [] self._fetch_schedule(schedule) - self.owners = data.get('owners', []) self._fetch_roster(roster, year) + self.owners = kwargs.get('owners', []) def __repr__(self): return 'Team(%s)' % (self.team_name, ) - + def _fetch_roster(self, data, year): '''Fetch teams roster''' self.roster.clear() @@ -64,32 +53,27 @@ def _fetch_schedule(self, data): '''Fetch schedule and scores for team''' for matchup in data: - if 'away' in matchup.keys(): - if matchup['away']['teamId'] == self.team_id: - score = matchup['away']['totalPoints'] - opponentId = matchup['home']['teamId'] - self.outcomes.append( - self._get_winner(matchup['winner'], True)) - self.scores.append(score) - self.schedule.append(opponentId) - elif matchup['home']['teamId'] == self.team_id: - score = matchup['home']['totalPoints'] - opponentId = matchup['away']['teamId'] - self.outcomes.append( - self._get_winner(matchup['winner'], False)) - self.scores.append(score) - self.schedule.append(opponentId) - elif matchup['home']['teamId'] == self.team_id: - score = matchup['home']['totalPoints'] - opponentId = matchup['home']['teamId'] - self.outcomes.append( - self._get_winner(matchup['winner'], False)) - self.scores.append(score) - self.schedule.append(opponentId) + home_team = matchup.get('home', {}) + away_team = matchup.get('away', {}) + home_id = home_team.get('teamId', -1) + away_id = away_team.get('teamId', -1) + + if self.team_id in (home_id, away_id): + # find if current team is home or away + (current_team, opponent_id, away) = (home_team, away_id, False) if home_id == self.team_id else (away_team, home_id, True) + # if bye week set opponent id to self + if opponent_id == -1: opponent_id = self.team_id + score = current_team.get('totalPoints') + self.outcomes.append(self._get_winner(matchup['winner'], away)) + self.scores.append(score) + self.schedule.append(opponent_id) + def _get_winner(self, winner: str, is_away: bool) -> str: if winner == 'UNDECIDED': return 'U' + elif winner == 'TIE': + return 'T' elif (is_away and winner == 'AWAY') or (not is_away and winner == 'HOME'): return 'W' else: @@ -99,4 +83,4 @@ def get_player_name(self, playerId: int) -> str: for player in self.roster: if player.playerId == playerId: return player.name - return '' + return '' \ No newline at end of file diff --git a/fantasy_app/espn_api_submodule/espn_api/requests/constant.py b/fantasy_app/espn_api_submodule/espn_api/requests/constant.py index d7900dc..853a94f 100644 --- a/fantasy_app/espn_api_submodule/espn_api/requests/constant.py +++ b/fantasy_app/espn_api_submodule/espn_api/requests/constant.py @@ -1,5 +1,4 @@ -FANTASY_BASE_ENDPOINT = 'https://fantasy.espn.com/apis/v3/games/' - +FANTASY_BASE_ENDPOINT = 'https://lm-api-reads.fantasy.espn.com/apis/v3/games/' FANTASY_SPORTS = { 'nfl' : 'ffl', 'nba' : 'fba', diff --git a/fantasy_app/scores.py b/fantasy_app/scores.py index 7ff40af..a678360 100644 --- a/fantasy_app/scores.py +++ b/fantasy_app/scores.py @@ -76,7 +76,7 @@ def new_dict(position, current_dict, lineup, stat, week): def get_most_position_points(position, stat, owners, year, currentweek=0): league = league_instance(int(year)) matchups = league.box_scores(currentweek) - + matchups_list = [] for matchup in matchups: @@ -84,11 +84,11 @@ def get_most_position_points(position, stat, owners, year, currentweek=0): player_dict1 = {} away = matchup.away_lineup home = matchup.home_lineup - + player_dict['team_name'] = matchup.away_team.team_name player_dict1['team_name'] = matchup.home_team.team_name - player_dict['team_owner'] = owners[matchup.away_team.owners[0]] - player_dict1['team_owner'] = owners[matchup.home_team.owners[0]] + player_dict['team_owner'] = owners[matchup.away_team.owners[0]['id']] + player_dict1['team_owner'] = owners[matchup.home_team.owners[0]['id']] away_dict = new_dict( position, player_dict, away, stat, currentweek)