From 449732f8a6de1653db77eda75de944c5955146db Mon Sep 17 00:00:00 2001 From: dtcarls Date: Mon, 6 Nov 2023 13:47:31 -0500 Subject: [PATCH 01/12] 15 point spread for close scores. fixed bug for "finished" teams --- gamedaybot/espn/functionality.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamedaybot/espn/functionality.py b/gamedaybot/espn/functionality.py index 062e4a4..cb32555 100644 --- a/gamedaybot/espn/functionality.py +++ b/gamedaybot/espn/functionality.py @@ -269,7 +269,7 @@ def get_matchups(league, week=None): def get_close_scores(league, week=None): """ - Retrieve the projected closest scores (10.999 points or closer) for a given week in a fantasy football league. + Retrieve the projected closest scores (15 points or closer) for a given week in a fantasy football league. Parameters ---------- @@ -284,7 +284,7 @@ def get_close_scores(league, week=None): A string containing the projected closest scores for the given week, formatted as a list of team names and abbreviation. """ - # Gets current projected closest scores (10.999 points or closer) + # Gets current projected closest scores (15 points or closer) box_scores = league.box_scores(week=week) score = [] @@ -294,7 +294,7 @@ def get_close_scores(league, week=None): home_projected = get_projected_total(i.home_lineup) diffScore = away_projected - home_projected - if (-11 < diffScore <= 0 and not all_played(i.away_lineup)) or (0 <= diffScore < 11 and not all_played(i.home_lineup)): + if (abs(diffScore) <= 15 and (not all_played(i.away_lineup) or not all_played(i.home_lineup))): score += ['%4s %6.2f - %6.2f %s' % (i.home_team.team_abbrev, i.home_projected, i.away_projected, i.away_team.team_abbrev)] From 0db4a77799407712962c45a923c905ba27c0deb9 Mon Sep 17 00:00:00 2001 From: dtcarls Date: Mon, 6 Nov 2023 15:37:17 -0500 Subject: [PATCH 02/12] github actions updates. mv ci to test. update to actually work. --- .github/workflows/ci.yaml | 38 ------------------------------------- .github/workflows/test.yml | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 38 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 86aaeda..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: Run tests on PRs and commits - -on: - push: - branches: - - main - - master - pull_request: - branches: - - main - - master - workflow_dispatch: - -env: - BOT_ID: 916ccfd76a7fda25c74d09e1d5 - LEAGUE_ID: 164483 - TEST_TAG: user/test_build:test - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Build container - uses: docker/build-push-action@v2 - with: - push: false - context: . - load: true - tags: ${{ env.TEST_TAG }} - - - name: Run image against tests - run: docker run ${{ env.TEST_TAG }} python /usr/src/gamedaybot/setup.py test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0f95689 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Run tests on PRs and commits + +on: + push: + branches: + - main + - master + pull_request: + types: [opened, synchronize] + branches: + - main + - master + workflow_dispatch: + +env: + BOT_ID: 916ccfd76a7fda25c74d09e1d5 + LEAGUE_ID: 164483 + TEST_TAG: user/test_build:test + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install -r requirements-test.txt + + - name: Test with pytest + run: pytest \ No newline at end of file From adb47ac06be715de2b14fbb175982d765b89f01f Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 14:54:58 -0500 Subject: [PATCH 03/12] better github actions for creating container and testing. --- .github/workflows/publish_image.yaml | 7 ++++- .github/workflows/test_image.yml | 43 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test_image.yml diff --git a/.github/workflows/publish_image.yaml b/.github/workflows/publish_image.yaml index ae65157..b033aeb 100644 --- a/.github/workflows/publish_image.yaml +++ b/.github/workflows/publish_image.yaml @@ -43,7 +43,12 @@ jobs: tags: ${{ env.TAG }} - name: Test image - run: docker run ${{ env.TAG }} python /usr/src/gamedaybot/setup.py test + run: | + docker run ${{ env.TAG }} /bin/sh -c "\ + python -m pip install --upgrade pip && \ + pip install -r requirements.txt && \ + pip install -r requirements-test.txt && \ + pytest" - name: Push image to registry uses: docker/build-push-action@v2 diff --git a/.github/workflows/test_image.yml b/.github/workflows/test_image.yml new file mode 100644 index 0000000..62a51db --- /dev/null +++ b/.github/workflows/test_image.yml @@ -0,0 +1,43 @@ +name: Run tests on PRs and commits + +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + workflow_dispatch: + +env: + BOT_ID: 916ccfd76a7fda25c74d09e1d5 + LEAGUE_ID: 164483 + TEST_TAG: user/test_build:test + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build container + uses: docker/build-push-action@v2 + with: + push: false + context: . + load: true + tags: ${{ env.TEST_TAG }} + + - name: Run image against tests + run: | + docker run ${{ env.TEST_TAG }} /bin/sh -c "\ + python -m pip install --upgrade pip && \ + pip install -r requirements.txt && \ + pip install -r requirements-test.txt && \ + pytest" From f7881e8a18030b6fcd05cc4aa8e412eeac7951ac Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 15:15:25 -0500 Subject: [PATCH 04/12] IR check to monitor report --- gamedaybot/espn/functionality.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gamedaybot/espn/functionality.py b/gamedaybot/espn/functionality.py index cb32555..513bf92 100644 --- a/gamedaybot/espn/functionality.py +++ b/gamedaybot/espn/functionality.py @@ -224,6 +224,13 @@ def scan_roster(lineup, team): player = i.position + ' ' + i.name + ' - ' + i.injuryStatus.title().replace('_', ' ') players += [player] + if i.slot_position == 'IR' and \ + i.injuryStatus != 'INJURY_RESERVE' and i.injuryStatus != 'OUT': + + count += 1 + player = i.position + ' ' + i.name + ' - Not IR eligible' + players += [player] + list = "" report = "" From 1c2a54476fd1e0946000f0af2458096dd5a4d7dc Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 15:16:40 -0500 Subject: [PATCH 05/12] show power rankings change by week. --- gamedaybot/espn/functionality.py | 39 ++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/gamedaybot/espn/functionality.py b/gamedaybot/espn/functionality.py index 513bf92..e1ade9b 100644 --- a/gamedaybot/espn/functionality.py +++ b/gamedaybot/espn/functionality.py @@ -393,7 +393,8 @@ def get_waiver_report(league, faab=False): def get_power_rankings(league, week=None): """ - This function returns the power rankings of the teams in the league for a specific week. + This function returns the power rankings of the teams in the league for a specific week, + along with the change in power ranking number and playoff percentage from the previous week. If the week is not provided, it defaults to the current week. The power rankings are determined using a 2 step dominance algorithm, as well as a combination of points scored and margin of victory. @@ -409,20 +410,40 @@ def get_power_rankings(league, week=None): Returns ------- str - A string representing the power rankings + A string representing the power rankings with changes from the previous week """ # Check if the week is provided, if not use the current week if not week: week = league.current_week - # Get the power rankings for the provided week - power_rankings = league.power_rankings(week=week) - # Create a list of strings representing the power rankings - score = ['%6s (%.1f) - %s' % (i[0], i[1].playoff_pct, i[1].team_name) for i in power_rankings - if i] - text = ['Power Rankings (Playoff %)'] + score - return '\n'.join(text) + p_rank_up_emoji = "🟢" + p_rank_down_emoji = "🔻"#"🔴" + + # Get the power rankings for the current and previous week + current_rankings = league.power_rankings(week=week) + previous_rankings = league.power_rankings(week=week-1) if week > 1 else [None] * len(current_rankings) + + # Prepare the output string + rankings_text = [] + for current, previous in zip(current_rankings, previous_rankings): + # Handle the case for the first week or missing data + if not previous or week == 1: + rankings_text.append(f"{current[0]} ({current[1].playoff_pct}) - {current[1].team_name}") + else: + # Calculate the percent changes + rank_change_percent = ((float(current[0]) - float(previous[0])) / float(previous[0])) * 100 + + # Use emojis for displaying change + rank_change_emoji = p_rank_up_emoji if rank_change_percent > 0 else p_rank_down_emoji if rank_change_percent < 0 else "" + + # Append emoji with the absolute value of change + rank_change_text = f"{rank_change_emoji}{abs(rank_change_percent):4.1f}%" + + rankings_text.append(f"{current[0]}[{rank_change_text}] ({current[1].playoff_pct}) - {current[1].team_abbrev}") + + rankings_text = ['Power Rankings (Playoff %)'] + rankings_text + return '\n'.join(rankings_text) def get_starter_counts(league): From 13fcf6f3dd23def75a47b679c49d457a291af9f9 Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 15:17:13 -0500 Subject: [PATCH 06/12] fix edge case in starter_counts --- gamedaybot/espn/functionality.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gamedaybot/espn/functionality.py b/gamedaybot/espn/functionality.py index e1ade9b..f8e0db7 100644 --- a/gamedaybot/espn/functionality.py +++ b/gamedaybot/espn/functionality.py @@ -461,10 +461,8 @@ def get_starter_counts(league): A dictionary containing the number of players at each position within the starting lineup. """ - # Get the current week -1 to get the last week's box scores - week = league.current_week - 1 - # Get the box scores for the specified week - box_scores = league.box_scores(week=week) + # Get the box scores for last week + box_scores = league.box_scores(week=league.current_week - 1) # Initialize a dictionary to store the home team's starters and their positions h_starters = {} # Initialize a variable to keep track of the number of home team starters @@ -496,10 +494,12 @@ def get_starter_counts(league): except KeyError: a_starters[player.slot_position] = 1 - if a_starter_count > h_starter_count: - return a_starters - else: - return h_starters + # if statement for the ultra rare case of a matchup with both entire teams (or one with a bye) on the bench + if a_starter_count!=0 and h_starter_count != 0: + if a_starter_count > h_starter_count: + return a_starters + else: + return h_starters def best_flex(flexes, player_pool, num): From 3d42f2b8cd45dd547d13683c7b46c4e2b7f15387 Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 15:17:58 -0500 Subject: [PATCH 07/12] edge case (no score) in optimal scores fix --- gamedaybot/espn/functionality.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gamedaybot/espn/functionality.py b/gamedaybot/espn/functionality.py index f8e0db7..906d3d3 100644 --- a/gamedaybot/espn/functionality.py +++ b/gamedaybot/espn/functionality.py @@ -567,6 +567,9 @@ def optimal_lineup_score(lineup, starter_counts): # get all players and points score = 0 + score_pct = 0 + best_score = 0 + for player in lineup: try: position_players[player.position][player.name] = player.points @@ -609,11 +612,12 @@ def optimal_lineup_score(lineup, starter_counts): best_lineup['DP'] = result[0] position_players = result[1] - best_score = 0 for position in best_lineup: best_score += sum(best_lineup[position].values()) - score_pct = (score / best_score) * 100 + if best_score != 0: + score_pct = (score / best_score) * 100 + return (best_score, score, best_score - score, score_pct) From e2d4d501babe6b6b855924d72994fcc8abf68e1b Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 15:26:23 -0500 Subject: [PATCH 08/12] add season recap --- gamedaybot/espn/functionality.py | 114 +++++++++++++++---------------- gamedaybot/espn/season_recap.py | 112 ++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 57 deletions(-) create mode 100644 gamedaybot/espn/season_recap.py diff --git a/gamedaybot/espn/functionality.py b/gamedaybot/espn/functionality.py index 906d3d3..5687eb2 100644 --- a/gamedaybot/espn/functionality.py +++ b/gamedaybot/espn/functionality.py @@ -621,7 +621,7 @@ def optimal_lineup_score(lineup, starter_counts): return (best_score, score, best_score - score, score_pct) -def optimal_team_scores(league, week=None, full_report=False): +def optimal_team_scores(league, week=None, full_report=False, recap=False): """ This function returns the optimal team scores or managers. @@ -639,7 +639,6 @@ def optimal_team_scores(league, week=None, full_report=False): str or tuple If full_report is True, a string representing the full report of the optimal team scores. If full_report is False, a tuple containing the best and worst manager strings. - """ if not week: @@ -686,12 +685,16 @@ def optimal_team_scores(league, week=None, full_report=False): best_mgr_str = ['🤖 Best Managers 🤖'] + [f'{team_names} scored their optimal score!'] worst = best_scores.popitem() + if recap: + return worst[0].team_abbrev + worst_mgr_str = ['🤡 Worst Manager 🤡'] + ['%s left %.2f points on their bench. Only scoring %.2f%% of their optimal score.' % (worst[0].team_name, worst[1][0] - worst[1][1], worst[1][3])] + return (best_mgr_str + worst_mgr_str) -def get_achievers_trophy(league, week=None): +def get_achievers_trophy(league, week=None, recap=False): """ This function returns the overachiever and underachiever of the league based on the difference between the projected score and the actual score. @@ -710,8 +713,6 @@ def get_achievers_trophy(league, week=None): """ box_scores = league.box_scores(week=week) - over_achiever = '' - under_achiever = '' high_achiever_str = ['📈 Overachiever 📈'] low_achiever_str = ['📉 Underachiever 📉'] best_performance = -9999 @@ -723,42 +724,35 @@ def get_achievers_trophy(league, week=None): if i.home_team != 0: if home_performance > best_performance: best_performance = home_performance - over_achiever = i.home_team.team_name + over_achiever = i.home_team if home_performance < worst_performance: worst_performance = home_performance - under_achiever = i.home_team.team_name + under_achiever = i.home_team if i.away_team != 0: if away_performance > best_performance: best_performance = away_performance - over_achiever = i.away_team.team_name + over_achiever = i.away_team if away_performance < worst_performance: worst_performance = away_performance - under_achiever = i.away_team.team_name + under_achiever = i.away_team + + if recap: + return over_achiever.team_abbrev, under_achiever.team_abbrev if best_performance > 0: - high_achiever_str += ['%s was %.2f points over their projection' % (over_achiever, best_performance)] + high_achiever_str += ['%s was %.2f points over their projection' % (over_achiever.team_name, best_performance)] else: high_achiever_str += ['No team out performed their projection'] if worst_performance < 0: - low_achiever_str += ['%s was %.2f points under their projection' % (under_achiever, abs(worst_performance))] + low_achiever_str += ['%s was %.2f points under their projection' % (under_achiever.team_name, abs(worst_performance))] else: low_achiever_str += ['No team was worse than their projection'] return (high_achiever_str + low_achiever_str) -def get_lucky_trophy(league, week=None): - """ - This function takes in a league object and an optional week parameter. It retrieves the box scores for the specified league and week, and creates a dictionary with the weekly scores for each team. The teams are sorted in descending order by their scores, and the team with the highest score is determined to be the lucky team for the week. The team with the lowest score is determined to be the unlucky team for the week. The function returns a list containing the lucky and unlucky teams, along with their records for the week. - - Parameters: - league (object): A league object containing information about the league and its teams. - week (int, optional): The week for which the box scores should be retrieved. If no week is specified, the current week will be used. - - Returns: - list: A list containing the lucky and unlucky teams, along with their records for the week. - """ +def get_weekly_score_with_win_loss(league, week=None): box_scores = league.box_scores(week=week) weekly_scores = {} for i in box_scores: @@ -769,23 +763,27 @@ def get_lucky_trophy(league, week=None): else: weekly_scores[i.home_team] = [i.home_score, 'L'] weekly_scores[i.away_team] = [i.away_score, 'W'] - weekly_scores = dict(sorted(weekly_scores.items(), key=lambda item: item[1], reverse=True)) + return dict(sorted(weekly_scores.items(), key=lambda item: item[1], reverse=True)) - # losses = 0 - # for t in weekly_scores: - # print(t.team_name + ': (' + str(len(weekly_scores)-1-losses) + '-' + str(losses) +')') - # losses+=1 +def get_lucky_trophy(league, week=None, recap=False): + """ + This function takes in a league object and an optional week parameter. It retrieves the box scores for the specified league and week, and creates a dictionary with the weekly scores for each team. The teams are sorted in descending order by their scores, and the team with the lowest score and won is determined to be the lucky team for the week. The team with the highest score and lost is determined to be the unlucky team for the week. The function returns a list containing the lucky and unlucky teams, along with their records for the week. + Parameters: + league (object): A league object containing information about the league and its teams. + week (int, optional): The week for which the box scores should be retrieved. If no week is specified, the current week will be used. + Returns: + list: A list containing the lucky and unlucky teams, along with their records for the week. + """ + weekly_scores = get_weekly_score_with_win_loss(league, week=week) losses = 0 - unlucky_team_name = '' unlucky_record = '' - lucky_team_name = '' lucky_record = '' num_teams = len(weekly_scores) - 1 for t in weekly_scores: if weekly_scores[t][1] == 'L': - unlucky_team_name = t.team_name + unlucky_team = t unlucky_record = str(num_teams - losses) + '-' + str(losses) break losses += 1 @@ -794,17 +792,20 @@ def get_lucky_trophy(league, week=None): weekly_scores = dict(sorted(weekly_scores.items(), key=lambda item: item[1])) for t in weekly_scores: if weekly_scores[t][1] == 'W': - lucky_team_name = t.team_name + lucky_team = t lucky_record = str(wins) + '-' + str(num_teams - wins) break wins += 1 - lucky_str = ['🍀 Lucky 🍀'] + ['%s was %s against the league, but still got the win' % (lucky_team_name, lucky_record)] - unlucky_str = ['😡 Unlucky 😡'] + ['%s was %s against the league, but still took an L' % (unlucky_team_name, unlucky_record)] + if recap: + return lucky_team.team_abbrev, unlucky_team.team_abbrev, weekly_scores + + lucky_str = ['🍀 Lucky 🍀']+['%s was %s against the league, but still got the win' % (lucky_team.team_name, lucky_record)] + unlucky_str = ['😡 Unlucky 😡']+['%s was %s against the league, but still took an L' % (unlucky_team.team_name, unlucky_record)] return (lucky_str + unlucky_str) -def get_trophies(league, week=None): +def get_trophies(league, week=None, recap=False): """ Returns trophies for the highest score, lowest score, closest score, and biggest win. @@ -820,59 +821,58 @@ def get_trophies(league, week=None): str A string representing the trophies """ + if not week: + week = league.current_week - 1 - # Gets trophies for highest score, lowest score, closest score, and biggest win matchups = league.box_scores(week=week) low_score = 9999 - low_team_name = '' high_score = -1 - high_team_name = '' closest_score = 9999 - close_winner = '' - close_loser = '' biggest_blowout = -1 - blown_out_team_name = '' - ownerer_team_name = '' for i in matchups: if i.home_team != 0: if i.home_score > high_score: high_score = i.home_score - high_team_name = i.home_team.team_name + high_team = i.home_team if i.home_score < low_score: low_score = i.home_score - low_team_name = i.home_team.team_name + low_team = i.home_team if i.away_team != 0: if i.away_score > high_score: high_score = i.away_score - high_team_name = i.away_team.team_name + high_team = i.away_team if i.away_score < low_score: low_score = i.away_score - low_team_name = i.away_team.team_name + low_team = i.away_team if i.away_team != 0 and i.home_team != 0: if i.away_score - i.home_score != 0 and \ abs(i.away_score - i.home_score) < closest_score: closest_score = abs(i.away_score - i.home_score) if i.away_score - i.home_score < 0: - close_winner = i.home_team.team_name - close_loser = i.away_team.team_name + close_winner = i.home_team + close_loser = i.away_team else: - close_winner = i.away_team.team_name - close_loser = i.home_team.team_name + close_winner = i.away_team + close_loser = i.home_team if abs(i.away_score - i.home_score) > biggest_blowout: biggest_blowout = abs(i.away_score - i.home_score) if i.away_score - i.home_score < 0: - ownerer_team_name = i.home_team.team_name - blown_out_team_name = i.away_team.team_name + ownerer = i.home_team + blown_out = i.away_team else: - ownerer_team_name = i.away_team.team_name - blown_out_team_name = i.home_team.team_name + ownerer = i.away_team + blown_out = i.home_team + + if (recap): + return high_team.team_abbrev, low_team.team_abbrev, blown_out.team_abbrev, close_winner.team_abbrev - high_score_str = ['👑 High score 👑'] + ['%s with %.2f points' % (high_team_name, high_score)] - low_score_str = ['💩 Low score 💩'] + ['%s with %.2f points' % (low_team_name, low_score)] - close_score_str = ['😅 Close win 😅'] + ['%s barely beat %s by %.2f points' % (close_winner, close_loser, closest_score)] - blowout_str = ['😱 Blow out 😱'] + ['%s blew out %s by %.2f points' % (ownerer_team_name, blown_out_team_name, biggest_blowout)] + high_score_str = ['👑 High score 👑']+['%s with %.2f points' % (high_team.team_name, high_score)] + low_score_str = ['💩 Low score 💩']+['%s with %.2f points' % (low_team.team_name, low_score)] + close_score_str = ['😅 Close win 😅']+['%s barely beat %s by %.2f points' % + (close_winner.team_name, close_loser.team_name, closest_score)] + blowout_str = ['😱 Blow out 😱']+['%s blew out %s by %.2f points' % (ownerer.team_name, blown_out.team_name, biggest_blowout)] text = ['Trophies of the week:'] + high_score_str + low_score_str + blowout_str + close_score_str + \ get_lucky_trophy(league, week) + get_achievers_trophy(league, week) + optimal_team_scores(league, week) diff --git a/gamedaybot/espn/season_recap.py b/gamedaybot/espn/season_recap.py new file mode 100644 index 0000000..3648d09 --- /dev/null +++ b/gamedaybot/espn/season_recap.py @@ -0,0 +1,112 @@ +import os +# import pandas as pd +# import dataframe_image as dfi +if os.environ.get("AWS_EXECUTION_ENV") is not None: + import espn.functionality as espn +else: + # For local use + import sys + sys.path.insert(1, os.path.abspath('.')) + import gamedaybot.espn.functionality as espn + + +def trophy_recap(league): + """ + This function takes in a league object and returns a string representing the trophies earned by each team in the league. + + Parameters + ---------- + league : object + A league object from the ESPN Fantasy API. + + Returns + ------- + str + A string that contains the team names and the number of trophies earned for each team + """ + + ICONS = ['👑', '💩', '😱', '😅', '🍀', '😡', '📈', '📉', '🤡'] + legend = ['*LEGEND*', '👑: Most Points', '💩: Least Points', '😱: Blown out', '😅: Close wins', '🍀: Lucky', + '😡: Unlucky', '📈: Most over projection', '📉: Most under projection', '🤡: Most points left on bench'] + team_trophies = {} + team_names = [] + + for team in league.teams: + # Initialize trophy count for each team + team_trophies[team.team_abbrev] = [0 for i in range(len(ICONS))] + team_names.append(team.team_abbrev) + + for week in range(1, league.current_week): + # Get high score, low score, blown out, and close win trophies + high_score_team, low_score_team, blown_out_team, close_win_team = espn.get_trophies(league=league, week=week, recap=True) + team_trophies[high_score_team][0] += 1 + team_trophies[low_score_team][1] += 1 + team_trophies[blown_out_team][2] += 1 + team_trophies[close_win_team][3] += 1 + + # Get lucky and unlucky trophies + lucky_team, unlucky_team, scores = espn.get_lucky_trophy(league=league, week=week, recap=True) + team_trophies[lucky_team][4] += 1 + team_trophies[unlucky_team][5] += 1 + + # Get overachiever and underachiever trophies + overachiever_team, underachiever_team = espn.get_achievers_trophy(league=league, week=week, recap=True) + team_trophies[overachiever_team][6] += 1 + team_trophies[underachiever_team][7] += 1 + + # Get most points left on bench trophy + best_manager_team = espn.optimal_team_scores(league=league, week=week, recap=True) + team_trophies[best_manager_team][8] += 1 + + result = 'Season Recap!\n' + result += "Team".ljust(7, ' ') + for icon in ICONS: + result += icon + ' ' + result += '\n' + for team_name, trophies in team_trophies.items(): + result += f"{team_name.ljust(5, ' ')}: {trophies}\n" + result += '\n'.join(legend) + + # Pretty picture + ### Libraries make lambda size too big + # df = pd.DataFrame.from_dict(team_trophies, orient='index', columns=ICONS) + # df_styled = df.style.background_gradient(cmap='Greens') + # dfi.export(df_styled, '/tmp/season_recap.png') + return (result) + + +def win_matrix(league): + """ + This function takes in a league and returns a string of the standings if every team played every other team every week. + The standings are sorted by winning percentage, and the string includes the team abbreviation, wins, and losses. + + Parameters + ---------- + league : object + A league object from the ESPN Fantasy API. + + Returns + ------- + str + A string of the standings in the format of "position. team abbreviation (wins-losses)" + """ + + team_record = {team.team_abbrev: [0, 0] for team in league.teams} + + for week in range(1, league.current_week): + scores = espn.get_weekly_score_with_win_loss(league=league, week=week) + losses = 0 + for team in scores: + team_record[team.team_abbrev][0] += len(scores) - 1 - losses + team_record[team.team_abbrev][1] += losses + losses += 1 + + team_record = dict(sorted(team_record.items(), key=lambda item: item[1][0] / item[1][1], reverse=True)) + + standings_txt = ["Standings if everyone played every team every week"] + pos = 1 + for team in team_record: + standings_txt += [f"{pos:2}. {team:4} ({team_record[team][0]}-{team_record[team][1]})"] + pos += 1 + + return '\n'.join(standings_txt) From 54b19999cce50506cd2066d2b78061e210d066c0 Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 16:20:13 -0500 Subject: [PATCH 09/12] move utils --- gamedaybot/espn/env_vars.py | 2 +- gamedaybot/{utils.py => utils/util.py} | 0 tests/dry_run_all_functions.py | 58 ++++++++++++++++++++++++++ tests/test_utils.py | 2 +- 4 files changed, 60 insertions(+), 2 deletions(-) rename gamedaybot/{utils.py => utils/util.py} (100%) create mode 100644 tests/dry_run_all_functions.py diff --git a/gamedaybot/espn/env_vars.py b/gamedaybot/espn/env_vars.py index 1376712..6bb7dae 100644 --- a/gamedaybot/espn/env_vars.py +++ b/gamedaybot/espn/env_vars.py @@ -1,6 +1,6 @@ import os import gamedaybot.espn.functionality as espn -import gamedaybot.utils as utils +import gamedaybot.utils.util as utils def get_env_vars(): diff --git a/gamedaybot/utils.py b/gamedaybot/utils/util.py similarity index 100% rename from gamedaybot/utils.py rename to gamedaybot/utils/util.py diff --git a/tests/dry_run_all_functions.py b/tests/dry_run_all_functions.py new file mode 100644 index 0000000..b06ff81 --- /dev/null +++ b/tests/dry_run_all_functions.py @@ -0,0 +1,58 @@ +import sys +import os +sys.path.insert(1, os.path.abspath('.')) + +from espn_api.football import League +import gamedaybot.espn.season_recap as recap +import gamedaybot.espn.functionality as espn +from gamedaybot.chat.discord import Discord +from gamedaybot.chat.slack import Slack +from gamedaybot.chat.groupme import GroupMe + +# LEAGUE_ID = os.environ["LEAGUE_ID"] +# LEAGUE_YEAR = os.environ["LEAGUE_YEAR"] + +## Manually populate +LEAGUE_ID = 164483 +LEAGUE_YEAR = 2023 + +league = League(league_id=LEAGUE_ID, year=LEAGUE_YEAR) +print(espn.get_scoreboard_short(league)) +print(espn.get_projected_scoreboard(league)) +print(espn.get_standings(league)) +print(espn.get_close_scores(league)) +print(espn.get_monitor(league)) +print(espn.get_matchups(league)) +print(espn.get_power_rankings(league)) +print(espn.get_trophies(league)) +print(espn.optimal_team_scores(league, full_report=True)) + +print(recap.win_matrix(league)) +print(recap.trophy_recap(league)) + +try: + swid = os.environ["SWID"] +except KeyError: + swid = '{1}' +try: + espn_s2 = os.environ["ESPN_S2"] +except KeyError: + espn_s2 = '1' + +if swid != '{1}' and espn_s2 != '1': + league = League(league_id=LEAGUE_ID, year=LEAGUE_YEAR, espn_s2=espn_s2, swid=swid) + print(espn.get_waiver_report(league)) + print(espn.get_waiver_report(league, True)) + +# bot = GroupMe(os.environ['BOT_ID']) +# bot.send_message(recap.trophy_recap(league), file_path='/tmp/season_recap.png') +# bot.send_message("hi", file_path='/tmp/season_recap.png') +# bot.send_message("Testing") + +# discord_bot = Discord(os.environ['DISCORD_WEBHOOK_URL']) +# discord_bot.send_message(recap.trophy_recap(league), file_path='/tmp/season_recap.png') +# discord_bot.send_message("Testing") + +# slack_bot = Slack(os.environ['SLACK_WEBHOOK_URL']) +# slack_bot.send_message(recap.trophy_recap(league), file_path='/tmp/season_recap.png') +# slack_bot.send_message("Testing") diff --git a/tests/test_utils.py b/tests/test_utils.py index a254be2..fbda05f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ import sys import os sys.path.insert(1, os.path.abspath('.')) -from gamedaybot import utils as util +import gamedaybot.utils.util as util class TestStringToBool: From 72bbc97a2be795ba70617a8227a42e217a72129c Mon Sep 17 00:00:00 2001 From: dtcarls Date: Tue, 19 Dec 2023 16:26:14 -0500 Subject: [PATCH 10/12] add win matrix and trophy recap --- gamedaybot/espn/espn_bot.py | 212 ++++++++++++++++++++++++++++-------- 1 file changed, 168 insertions(+), 44 deletions(-) diff --git a/gamedaybot/espn/espn_bot.py b/gamedaybot/espn/espn_bot.py index 0dcaaca..43f1be3 100644 --- a/gamedaybot/espn/espn_bot.py +++ b/gamedaybot/espn/espn_bot.py @@ -1,58 +1,166 @@ -import sys import os -sys.path.insert(1, os.path.abspath('.')) -import json -from gamedaybot.espn.env_vars import get_env_vars -import gamedaybot.espn.functionality as espn -import gamedaybot.utils as utils -from gamedaybot.chat.groupme import GroupMe -from gamedaybot.chat.slack import Slack -from gamedaybot.chat.discord import Discord +if os.environ.get("AWS_EXECUTION_ENV") is not None: + # For use in lambda function + import utils.util as util + from chat.groupme import GroupMe + from chat.slack import Slack + from chat.discord import Discord +else: + # For local use + import sys + sys.path.insert(1, os.path.abspath('.')) + import gamedaybot.utils.util as util + from gamedaybot.chat.groupme import GroupMe + from gamedaybot.chat.slack import Slack + from gamedaybot.chat.discord import Discord + from gamedaybot.espn.env_vars import get_env_vars + import gamedaybot.espn.functionality as espn + import gamedaybot.espn.season_recap as recap + + from espn_api.football import League +import json +import logging + +logger = logging.getLogger(__name__) +# logger.setLevel(logging.INFO) +logger.setLevel(logging.DEBUG) + def espn_bot(function): + """ + This function is used to send messages to a messaging platform (e.g. Slack, Discord, or GroupMe) with information + about a fantasy football league. + + Parameters + ---------- + function: str + A string that specifies which type of information to send (e.g. "get_matchups", "get_power_rankings"). + + Returns + ------- + None + + Notes + ----- + The function uses the following information from the data dictionary: + + str_limit: the character limit for messages on slack. + bot_id: the id of the GroupMe bot. + If not provided, defaults to 1. + slack_webhook_url: the webhook url for the slack bot. + If not provided, defaults to 1. + discord_webhook_url: the webhook url for the discord bot. + If not provided, defaults to 1. + league_id: the id of the fantasy football league. + year: the year of the league. + If not provided, defaults to current year. + swid: the swid of the league. + If not provided, defaults to '{1}'. + espn_s2: the espn s2 of the league. + If not provided, defaults to '1'. + top_half_scoring: a boolean that indicates whether to include only the top half of the league in the standings. + If not provided, defaults to False. + random_phrase: a boolean that indicates whether to include a random phrase in the message. + If not provided, defaults to False. + + The function creates GroupMe, Slack, and Discord objects, and a League object using the provided information. + It then uses the specified function to generate a message and sends it through the appropriate messaging platform. + + Possible function values: + + get_matchups: sends the current week's matchups and the projected scores for the remaining games. + get_monitor: sends a message with a summary of the current week's scores. + get_scoreboard_short: sends a short version of the current week's scores. + get_projected_scoreboard: sends the projected scores for the remaining games. + get_close_scores: sends a message with the scores of games that have a difference of less than 7 points. + get_power_rankings: sends a message with the power rankings for the league. + get_trophies: sends a message with the trophies for the league. + get_standings: sends a message with the standings for the league. + get_final: sends the final scores and trophies for the previous week. + get_waiver_report: sends a message with the waiver report for the league. + init: sends a message to confirm that the bot has been set up. + """ + data = get_env_vars() - bot = GroupMe(data['bot_id']) - slack_bot = Slack(data['slack_webhook_url']) - discord_bot = Discord(data['discord_webhook_url']) - swid = data['swid'] - espn_s2 = data['espn_s2'] + str_limit = data['str_limit'] # slack char limit + + try: + bot_id = data['bot_id'] + except KeyError: + bot_id = 1 + + try: + slack_webhook_url = data['slack_webhook_url'] + except KeyError: + slack_webhook_url = 1 + + try: + discord_webhook_url = data['discord_webhook_url'] + except KeyError: + discord_webhook_url = 1 + + if (len(str(bot_id)) <= 1 and + len(str(slack_webhook_url)) <= 1 and + len(str(discord_webhook_url)) <= 1): + # Ensure that there's info for at least one messaging platform, + # use length of str in case of blank but non null env variable + raise Exception("No messaging platform info provided. Be sure one of BOT_ID, SLACK_WEBHOOK_URL, or DISCORD_WEBHOOK_URL env variables are set") + league_id = data['league_id'] - year = data['year'] - random_phrase = data['random_phrase'] - test = data['test'] - top_half_scoring = data['top_half_scoring'] - waiver_report = data['waiver_report'] + + try: + year = int(data['year']) + except KeyError: + year = 2023 + + try: + swid = data['swid'] + except KeyError: + swid = '{1}' + + if swid.find("{", 0) == -1: + swid = "{" + swid + if swid.find("}", -1) == -1: + swid = swid + "}" + + try: + espn_s2 = data['espn_s2'] + except KeyError: + espn_s2 = '1' + + try: + top_half_scoring = util.str_to_bool(data['top_half_scoring']) + except KeyError: + top_half_scoring = False + + try: + random_phrase = util.str_to_bool(data['random_phrase']) + except KeyError: + random_phrase = False + + groupme_bot = GroupMe(bot_id) + slack_bot = Slack(slack_webhook_url) + discord_bot = Discord(discord_webhook_url) if swid == '{1}' or espn_s2 == '1': league = League(league_id=league_id, year=year) else: league = League(league_id=league_id, year=year, espn_s2=espn_s2, swid=swid) - if league.scoringPeriodId > len(league.settings.matchup_periods) and not test: - print("Not in active season") - return + try: + broadcast_message = data['broadcast_message'] + except KeyError: + broadcast_message = None - faab = league.settings.faab - - if test: - print(espn.get_scoreboard_short(league)) - print(espn.get_projected_scoreboard(league)) - print(espn.get_standings(league)) - print(espn.get_standings(league, True)) - print(espn.get_close_scores(league)) - print(espn.get_monitor(league)) - print(espn.get_matchups(league)) - print(espn.get_power_rankings(league)) - print(espn.get_trophies(league)) - print(espn.optimal_team_scores(league, full_report=True)) - if waiver_report and swid != '{1}' and espn_s2 != '1': - print(espn.get_waiver_report(league, faab)) - # bot.send_message("Testing") - # slack_bot.send_message("Testing") - # discord_bot.send_message("Testing") + # always let init and broadcast run + if function not in ["init", "broadcast"] and league.scoringPeriodId > len(league.settings.matchup_periods): + logger.info("Not in active season") + return text = '' + logger.info("Function: " + function) + if function == "get_matchups": text = espn.get_matchups(league, random_phrase) text = text + "\n\n" + espn.get_projected_scoreboard(league) @@ -71,6 +179,13 @@ def espn_bot(function): text = espn.get_trophies(league) elif function == "get_standings": text = espn.get_standings(league, top_half_scoring) + elif function == "win_matrix": + text = recap.win_matrix(league) + elif function == "trophy_recap": + text = recap.trophy_recap(league) + # groupme_bot.send_message(text, file_path='/tmp/season_recap.png') + # slack_bot.send_message(text, file_path='/tmp/season_recap.png') + # discord_bot.send_message(text, file_path='/tmp/season_recap.png') elif function == "get_final": # on Tuesday we need to get the scores of last week week = league.current_week - 1 @@ -79,6 +194,12 @@ def espn_bot(function): elif function == "get_waiver_report" and swid != '{1}' and espn_s2 != '1': faab = league.settings.faab text = espn.get_waiver_report(league, faab) + elif function == "broadcast": + try: + text = broadcast_message + except KeyError: + # do nothing here, empty broadcast message + pass elif function == "init": try: text = data["init_msg"] @@ -86,17 +207,20 @@ def espn_bot(function): # do nothing here, empty init message pass else: - text = "Something happened. HALP" + text = "Something bad happened. HALP" - if text != '' and not test: - messages=utils.str_limit_check(text, data['str_limit']) + logger.debug(data) + if text != '': + logger.debug(text) + messages = util.str_limit_check(text, str_limit) for message in messages: - bot.send_message(message) + groupme_bot.send_message(message) slack_bot.send_message(message) discord_bot.send_message(message) if __name__ == '__main__': from gamedaybot.espn.scheduler import scheduler + espn_bot("init") scheduler() \ No newline at end of file From 9f5b2c25d507c3c54cfbedeadfe26b030635de94 Mon Sep 17 00:00:00 2001 From: dtcarls Date: Wed, 20 Dec 2023 11:19:23 -0500 Subject: [PATCH 11/12] normalize power rankings. fix percent change bug. --- gamedaybot/espn/functionality.py | 51 +++++++++++++++++++------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/gamedaybot/espn/functionality.py b/gamedaybot/espn/functionality.py index 5687eb2..dc574eb 100644 --- a/gamedaybot/espn/functionality.py +++ b/gamedaybot/espn/functionality.py @@ -413,36 +413,47 @@ def get_power_rankings(league, week=None): A string representing the power rankings with changes from the previous week """ - # Check if the week is provided, if not use the current week + # Check if the week is provided, if not use the previous week if not week: - week = league.current_week + week = league.current_week - 1 p_rank_up_emoji = "🟢" - p_rank_down_emoji = "🔻"#"🔴" + p_rank_down_emoji = "🔻" + p_rank_same_emoji = "🟰" - # Get the power rankings for the current and previous week + # Get the power rankings for the previous 2 weeks current_rankings = league.power_rankings(week=week) - previous_rankings = league.power_rankings(week=week-1) if week > 1 else [None] * len(current_rankings) + previous_rankings = league.power_rankings(week=week-1) if week > 1 else [] + + # Normalize the scores + def normalize_rankings(rankings): + if not rankings: + return [] + max_score = max(float(score) for score, _ in rankings) + return [(f"{99.99 * float(score) / max_score:.2f}", team) for score, team in rankings] - # Prepare the output string - rankings_text = [] - for current, previous in zip(current_rankings, previous_rankings): - # Handle the case for the first week or missing data - if not previous or week == 1: - rankings_text.append(f"{current[0]} ({current[1].playoff_pct}) - {current[1].team_name}") - else: - # Calculate the percent changes - rank_change_percent = ((float(current[0]) - float(previous[0])) / float(previous[0])) * 100 - # Use emojis for displaying change - rank_change_emoji = p_rank_up_emoji if rank_change_percent > 0 else p_rank_down_emoji if rank_change_percent < 0 else "" + normalized_current_rankings = normalize_rankings(current_rankings) + normalized_previous_rankings = normalize_rankings(previous_rankings) + + # Convert normalized previous rankings to a dictionary for easy lookup + previous_rankings_dict = {team.team_abbrev: score for score, team in normalized_previous_rankings} + + # Prepare the output string + rankings_text = ['Power Rankings (Playoff %)'] + for normalized_current_score, current_team in normalized_current_rankings: + team_abbrev = current_team.team_abbrev + rank_change_text = '' - # Append emoji with the absolute value of change - rank_change_text = f"{rank_change_emoji}{abs(rank_change_percent):4.1f}%" + # Check if the team was present in the normalized previous rankings + if team_abbrev in previous_rankings_dict: + previous_score = previous_rankings_dict[team_abbrev] + rank_change_percent = ((float(normalized_current_score) - float(previous_score)) / float(previous_score)) * 100 + rank_change_emoji = p_rank_up_emoji if rank_change_percent > 0 else p_rank_down_emoji if rank_change_percent < 0 else p_rank_same_emoji + rank_change_text = f"[{rank_change_emoji}{abs(rank_change_percent):4.1f}%]" - rankings_text.append(f"{current[0]}[{rank_change_text}] ({current[1].playoff_pct}) - {current[1].team_abbrev}") + rankings_text.append(f"{normalized_current_score}{rank_change_text} ({current_team.playoff_pct:4.1f}) - {team_abbrev}") - rankings_text = ['Power Rankings (Playoff %)'] + rankings_text return '\n'.join(rankings_text) From 65d5137437347f740029c0c19906dd8679339c93 Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 28 Dec 2023 20:21:52 -0500 Subject: [PATCH 12/12] pin espn_api to 0.33.0 short term fix for https://github.com/cwendt94/espn-api/issues/518 until PR is merged --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 09d4aa0..bbb5d74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ flake8==3.3.0 apscheduler>=3.3.0,<4.0.0 requests>=2.0.0,<3.0.0 -espn_api>=0.31.0 -datetime \ No newline at end of file +espn_api==0.33.0 +datetime