''' +
diff --git a/requirements.txt b/requirements.txt
index 0cf0bbd..39f729e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
beautifulsoup4==4.12.3
-camel-converter==3.1.2
+camel-converter==4.0.0
colorama==0.4.6
espn-api==0.39.0
GitPython==3.1.43
@@ -13,7 +13,7 @@ pydantic-settings==2.5.2
PyDrive2==1.20.0
pytest==8.3.3
python-dotenv==1.0.1
-reportlab==4.2.2
+reportlab==4.2.5
requests==2.32.3
selenium==4.25.0
slack_sdk==3.33.1
diff --git a/resources/documentation/descriptions.py b/resources/documentation/descriptions.py
index 19e445b..12fed7b 100644
--- a/resources/documentation/descriptions.py
+++ b/resources/documentation/descriptions.py
@@ -1,87 +1,113 @@
__author__ = "Wren J. R. (uberfastman)"
__email__ = "uberfastman@uberfastman.dev"
-league_standings = "Overall standings for the chosen week. Dynamically adjusts information based on whether a league " \
- "uses only waivers or has a FAAB (free agent acquisition budget). Data marked with an " \
- "asterisk (\"*\") means that due to limitations of whichever fantasy platform API is being " \
- "used, the available information is incomplete in some way."
+league_standings = (
+ "Overall standings for the chosen week. Dynamically adjusts information based on whether a league uses only "
+ "waivers or has a FAAB (free agent acquisition budget). Data marked with an asterisk (\"*\") means that due "
+ "to limitations of whichever fantasy platform API is being used, the available information is incomplete in some "
+ "way."
+)
-league_median_matchup_standings = "Overall \"against the median\" league standings. Every week the median score is " \
- "calculated across all teams, and every team plays an extra matchup versus the " \
- "league median score. Teams earn an additional win/loss/tie based on how " \
- "their score matches up against the league median. Median standings are ranked by " \
- "\"Combined Record\" (most wins, then fewest losses, then most ties), and use " \
- " \"Season +/- Median\" (how many total points over/under the median teams have " \
- "scored on the season) as the tie-breaker."
+league_median_matchup_standings = (
+ "Overall \"against the median\" league standings. Every week the median score is calculated across all teams, and "
+ "every team plays an extra matchup versus the league median score. Teams earn an additional win/loss/tie based on "
+ "how their score matches up against the league median. Median standings are ranked by \"Combined Record\" (most "
+ "wins, then fewest losses, then most ties), and use \"Season +/- Median\" (how many total points over/under the "
+ "median teams have scored on the season) as the tie-breaker."
+)
-playoff_probabilities = "Predicts each team's likelihood of making the playoffs, as well as of finishing in any " \
- "given place. These predictions are created using Monte Carlo simulations to simulate the " \
- "rest of the season over and over, and then averaging out each team's performance across all " \
- "performed simulations. Currently these predictions are not aware of special playoff " \
- "eligibility for leagues with divisions or other custom playoff settings."
+playoff_probabilities = (
+ "Predicts each team's likelihood of making the playoffs, as well as of finishing in any given place. These "
+ "predictions are created using Monte Carlo simulations to simulate the rest of the season over and over, and then "
+ "averaging out each team's performance across all performed simulations. Currently these predictions are not aware "
+ "of special playoff eligibility for leagues with divisions or other custom playoff settings."
+)
-team_power_rankings = "The power rankings are calculated by taking a weekly average of each team's score, coaching " \
- "efficiency, and luck."
+team_power_rankings = (
+ "The power rankings are calculated by taking a weekly average of each team's score, coaching efficiency, and luck."
+)
-team_z_score_rankings = "Measure of standard deviations away from mean for a score. Shows teams performing above or " \
- "below their normal scores for the current week. See Standard Score. This metric shows which teams " \
- "over-performed or underperformed compared to how those teams usually do."
+team_z_score_rankings = (
+ "Measure of standard deviations away from mean for a score. Shows teams performing above or below their normal "
+ "scores for the current week. See Standard "
+ "Score. This metric shows which teams over-performed or underperformed compared to how those teams usually do."
+)
-team_score_rankings = "Teams ranked by highest score. If tie-breaks are turned on, highest bench points will be used " \
- "to break score ties."
+team_score_rankings = (
+ "Teams ranked by highest score. If tie-breaks are turned on, highest bench points will be used to break score ties."
+)
-team_coaching_efficiency_rankings = "Coaching efficiency is calculated by dividing the total points scored by each " \
- "team this week by the highest possible points they could have scored (optimal " \
- "points) this week. This metric is designed to quantify whether manager made " \
- "good sit/start decisions, regardless of how high their team scored or whether " \
- "their team won or lost.
If tie-breaks are turned " \
- "on, the team with the most starting players that exceeded their weekly average " \
- "points is awarded a higher ranking, and if that is still tied, the team whose " \
- "starting players exceeded their weekly average points by the highest cumulative " \
- "percentage points is awarded a higher ranking."
+team_coaching_efficiency_rankings = (
+ "Coaching efficiency is calculated by dividing the total points scored by each team this week by the highest "
+ "possible points they could have scored (optimal points) this week. This metric is designed to quantify whether "
+ "manager made good sit/start decisions, regardless of how high their team scored or whether their team won or "
+ "lost.
If tie-breaks are turned on, the team with the most starting players that "
+ "exceeded their weekly average points is awarded a higher ranking, and if that is still tied, the team whose "
+ "starting players exceeded their weekly average points by the highest cumulative percentage points is awarded a "
+ "higher ranking."
+)
-team_luck_rankings = "Luck is calculated by matching up each team against every other team that week to get a total " \
- "record against the whole league, then if that team won, the formula is:
" \
- " " \
- " " \
- " " \
- "luck = (losses + ties) / (number of teams excluding that " \
- "team) * 100
" \
- "and if that team lost, the formula is:
" \
- " " \
- " " \
- " " \
- "luck = 0 - (wins + ties) / (number of teams excluding " \
- "that team) * 100
" \
- " This metric is designed to show whether your team was very \"lucky\" " \
- "or \"unlucky\", since a team that would have beaten all but one team this week (second highest " \
- "score) but lost played the only other team they could have lost to, and a team that would have " \
- "lost to all but one team this week (second lowest score) but won played the only other team " \
- "that they could have beaten."
+team_luck_rankings = (
+ "Luck is calculated by matching up each team against every other team that week to get a total record against the "
+ "whole league, then if that team won, the formula is:"
+ "
"
+ " "
+ " "
+ " "
+ ""
+ "luck = (losses + ties) / (number of teams excluding that team) * 100"
+ "
"
+ "
"
+ "and if that team lost, the formula is:"
+ "
"
+ " "
+ " "
+ " "
+ ""
+ "luck = 0 - (wins + ties) / (number of teams excluding that team) * 100"
+ "
"
+ "
"
+ " "
+ "This metric is designed to show whether your team was very \"lucky\" or \"unlucky\", since a team that would have "
+ "beaten all but one team this week (second highest score) but lost played the only other team they could have lost "
+ "to, and a team that would have lost to all but one team this week (second lowest score) but won played the only "
+ "other team that they could have beaten."
+)
team_optimal_score_rankings = "Teams ranked by highest optimal score."
-bad_boy_rankings = "The Bad Boy ranking is a \"just-for-fun\" metric that pulls NFL player arrest history from the " \
- "USA Today NFL player " \
- "arrest database, and then assigns points to all crimes committed by players on each " \
- "team's starting lineup to give the team a total bad boy score. The points assigned to each " \
- "crime can be found here."
+bad_boy_rankings = (
+ "The Bad Boy ranking is a \"just-for-fun\" metric that pulls NFL player arrest history from the "
+ "USA Today NFL player arrest "
+ "database, and then assigns points to all crimes committed by players on each team's starting lineup to "
+ "give the team a total bad boy score. The points assigned to each crime can be found "
+ "here."
+)
-beef_rankings = "The Beef ranking is a \"just-for-fun\" metric with a made-up unit of measurement, the " \
- "\"TABBU\", which stands for \"Trimmed And Boneless Beef " \
- "Unit(s)\". The TABBU was derived from the amount of trimmed and boneless beef is produced by " \
- "one beef cow, based on academic research done for the beef industry found " \
- "here, " \
- "and is set as equivalent to 500 lbs. The app pulls player weight data from the Sleeper API, an " \
- "example of which can be found " \
- "here, and uses the total weight of each team's starting lineup, including the rolled-up " \
- "weights of starting defenses, to give each team a total TABBU score."
+beef_rankings = (
+ "The Beef ranking is a \"just-for-fun\" metric with a made-up unit of measurement, the \"TABBU\", which "
+ "stands for \"Trimmed And Boneless Beef Unit(s)\". The TABBU was derived from "
+ "the amount of trimmed and boneless beef is produced by one beef cow, based on academic research done for the beef "
+ "industry found "
+ "here, and is set as "
+ "equivalent to 500 lbs. The app pulls player weight data from the Sleeper API, an example of which can be found "
+ "here, and uses the total weight of each "
+ "team's starting lineup, including the rolled-up weights of starting defenses, to give each team a total TABBU "
+ "score."
+)
+
+high_roller_rankings = (
+ "The High Roller ranking is a \"just-for-fun\" metric that pulls NFL player fine history (for behavior deemed "
+ "punishable by the NFL) from Spotrac, and then "
+ "totals all the fines levied against each team's starting lineup."
+)
weekly_top_scorers = "Running list of each week's highest scoring team. Can be used for weekly highest points payouts."
weekly_low_scorers = "Running list of each week's lowest scoring team."
-weekly_highest_coaching_efficiency = "Running list of each week's team with the highest coaching efficiency. Can be " \
- "used for weekly highest coaching efficiency payouts."
+weekly_highest_coaching_efficiency = (
+ "Running list of each week's team with the highest coaching efficiency. Can be used for weekly highest coaching "
+ "efficiency payouts."
+)
diff --git a/tests/test_features.py b/tests/test_features.py
index fe72784..9482c3c 100644
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -10,7 +10,7 @@
from features.bad_boy import BadBoyFeature # noqa: E402
from features.beef import BeefFeature # noqa: E402
-from features.high_roller import HighRollerStats # noqa: E402
+from features.high_roller import HighRollerFeature # noqa: E402
from utilities.logger import get_logger # noqa: E402
@@ -20,70 +20,81 @@
if not Path(test_data_dir).exists():
os.makedirs(test_data_dir)
-player_first_name = "Marquise"
-player_last_name = "Brown"
+player_first_name = "Rashee"
+player_last_name = "Rice"
player_full_name = f"{player_first_name} {player_last_name}"
-player_team_abbr = "ARI"
+player_team_abbr = "KC"
player_position = "WR"
-season = 2024
+season = 2023
def test_bad_boy_init():
- bad_boy_stats = BadBoyFeature(
- root_dir=root_dir,
+ bad_boy_feature = BadBoyFeature(
data_dir=test_data_dir,
+ root_dir=root_dir,
+ refresh=True,
save_data=True,
- offline=False,
- refresh=True
+ offline=False
)
- bad_boy_stats.generate_crime_categories_json()
+ bad_boy_feature.generate_crime_categories_json()
logger.info(
f"\nPlayer Bad Boy crime for {player_first_name} {player_last_name}: "
- f"{bad_boy_stats.get_player_bad_boy_crime(player_first_name, player_last_name, player_team_abbr, player_position)}"
+ f"{bad_boy_feature.get_player_bad_boy_crime(player_first_name, player_last_name, player_team_abbr, player_position)}"
)
logger.info(
f"\nPlayer Bad Boy points for {player_first_name} {player_last_name}: "
- f"{bad_boy_stats.get_player_bad_boy_points(player_first_name, player_last_name, player_team_abbr, player_position)}"
+ f"{bad_boy_feature.get_player_bad_boy_points(player_first_name, player_last_name, player_team_abbr, player_position)}"
)
- assert bad_boy_stats.bad_boy_data is not None
+ assert bad_boy_feature.feature_data is not None
def test_beef_init():
- beef_stats = BeefFeature(
+ beef_feature = BeefFeature(
data_dir=test_data_dir,
+ refresh=True,
save_data=True,
- offline=False,
- refresh=True
+ offline=False
)
- beef_stats.generate_player_info_json()
+ beef_feature.generate_player_info_json()
logger.info(
f"\nPlayer weight for {player_full_name}: "
- f"{beef_stats.get_player_weight(player_first_name, player_last_name, player_team_abbr)}"
+ f"{beef_feature.get_player_weight(player_first_name, player_last_name, player_team_abbr)}"
)
logger.info(
f"\nPlayer TABBU for {player_full_name}: "
- f"{beef_stats.get_player_tabbu(player_first_name, player_last_name, player_team_abbr)}"
+ f"{beef_feature.get_player_tabbu(player_first_name, player_last_name, player_team_abbr)}"
)
- assert beef_stats.beef_data is not None
+ assert beef_feature.feature_data is not None
def test_high_roller_init():
- high_roller_stats = HighRollerStats(
- season=season,
+ high_roller_feature = HighRollerFeature(
data_dir=test_data_dir,
+ season=season,
+ refresh=True,
save_data=True,
- offline=False,
- refresh=True
+ offline=False
)
- logger.info(f"\n{high_roller_stats}")
+ logger.info(
+ f"\nPlayer worst violation for {player_full_name}: "
+ f"{high_roller_feature.get_player_worst_violation(player_first_name, player_last_name, player_team_abbr, player_position)}"
+ )
+ logger.info(
+ f"\nPlayer worst violation fine for {player_full_name}: "
+ f"{high_roller_feature.get_player_worst_violation_fine(player_first_name, player_last_name, player_team_abbr, player_position)}"
+ )
+ logger.info(
+ f"\nPlayer fines total for {player_full_name}: "
+ f"{high_roller_feature.get_player_fines_total(player_first_name, player_last_name, player_team_abbr, player_position)}"
+ )
- assert high_roller_stats.high_roller_data is not None
+ assert high_roller_feature.feature_data is not None
if __name__ == "__main__":
diff --git a/utilities/app.py b/utilities/app.py
index efa0569..392fb41 100644
--- a/utilities/app.py
+++ b/utilities/app.py
@@ -17,8 +17,6 @@
from git import Repo, TagReference, cmd
from urllib3 import connectionpool, poolmanager
-from features.bad_boy import BadBoyFeature
-from features.beef import BeefFeature
from calculate.metrics import CalculateMetrics
from dao.base import BaseLeague, BaseTeam, BasePlayer
from dao.platforms.cbs import LeagueData as CbsLeagueData
@@ -26,6 +24,9 @@
from dao.platforms.fleaflicker import LeagueData as FleaflickerLeagueData
from dao.platforms.sleeper import LeagueData as SleeperLeagueData
from dao.platforms.yahoo import LeagueData as YahooLeagueData
+from features.bad_boy import BadBoyFeature
+from features.beef import BeefFeature
+from features.high_roller import HighRollerFeature
from utilities.logger import get_logger
from utilities.settings import settings
from utilities.utils import format_platform_display
@@ -137,7 +138,7 @@ def league_data_factory(base_dir: Path, data_dir: Path, platform: str,
elif platform == "fleaflicker":
fleaflicker_league = FleaflickerLeagueData(
- None,
+ base_dir,
data_dir,
league_id,
season,
@@ -152,7 +153,7 @@ def league_data_factory(base_dir: Path, data_dir: Path, platform: str,
elif platform == "sleeper":
sleeper_league = SleeperLeagueData(
- None,
+ base_dir,
data_dir,
league_id,
season,
@@ -208,8 +209,12 @@ def add_report_player_stats(metrics: Dict[str, Any], player: BasePlayer,
player.bad_boy_crime = str()
player.bad_boy_points = int()
player.bad_boy_num_offenders = int()
- player.weight = float()
- player.tabbu = float()
+ player.beef_weight = float()
+ player.beef_tabbu = float()
+ player.high_roller_worst_violation = str()
+ player.high_roller_worst_violation_fine = float()
+ player.high_roller_fines_total = float()
+ player.high_roller_num_violators = int()
if player.selected_position not in bench_positions:
@@ -227,8 +232,23 @@ def add_report_player_stats(metrics: Dict[str, Any], player: BasePlayer,
if settings.report_settings.league_beef_rankings_bool:
beef_stats: BeefFeature = metrics.get("beef_stats")
- player.weight = beef_stats.get_player_weight(player.first_name, player.last_name, player.nfl_team_abbr)
- player.tabbu = beef_stats.get_player_tabbu(player.first_name, player.last_name, player.nfl_team_abbr)
+ player.beef_weight = beef_stats.get_player_weight(player.first_name, player.last_name, player.nfl_team_abbr)
+ player.beef_tabbu = beef_stats.get_player_tabbu(player.first_name, player.last_name, player.nfl_team_abbr)
+
+ if settings.report_settings.league_high_roller_rankings_bool:
+ high_roller_stats: HighRollerFeature = metrics.get("high_roller_stats")
+ player.high_roller_worst_violation = high_roller_stats.get_player_worst_violation(
+ player.first_name, player.last_name, player.nfl_team_abbr, player.primary_position
+ )
+ player.high_roller_worst_violation_fine = high_roller_stats.get_player_worst_violation_fine(
+ player.first_name, player.last_name, player.nfl_team_abbr, player.primary_position
+ )
+ player.high_roller_fines_total = high_roller_stats.get_player_fines_total(
+ player.first_name, player.last_name, player.nfl_team_abbr, player.primary_position
+ )
+ player.high_roller_num_violators = high_roller_stats.get_player_num_violators(
+ player.first_name, player.last_name, player.nfl_team_abbr, player.primary_position
+ )
return player
@@ -257,6 +277,7 @@ def add_report_team_stats(team: BaseTeam, league: BaseLeague, week_counter: int,
team.worst_offense = None
team.num_offenders = 0
team.worst_offense_score = 0
+ p: BasePlayer
for p in team.roster:
if p.selected_position not in bench_positions:
if p.bad_boy_points > 0:
@@ -270,8 +291,22 @@ def add_report_team_stats(team: BaseTeam, league: BaseLeague, week_counter: int,
team.worst_offense_score = p.bad_boy_points
if settings.report_settings.league_beef_rankings_bool:
- team.total_weight = sum([p.weight for p in team.roster if p.selected_position not in bench_positions])
- team.tabbu = sum([p.tabbu for p in team.roster if p.selected_position not in bench_positions])
+ team.total_weight = sum([p.beef_weight for p in team.roster if p.selected_position not in bench_positions])
+ team.tabbu = sum([p.beef_tabbu for p in team.roster if p.selected_position not in bench_positions])
+
+ if settings.report_settings.league_high_roller_rankings_bool:
+ p: BasePlayer
+ for p in team.roster:
+ if p.selected_position not in bench_positions:
+ if p.high_roller_fines_total > 0:
+ team.fines_total += p.high_roller_fines_total
+ if p.selected_position == "D/ST":
+ team.num_violators += p.high_roller_num_violators
+ else:
+ team.num_violators += 1
+ if p.high_roller_fines_total > team.worst_violation_fine:
+ team.worst_violation = p.high_roller_worst_violation
+ team.worst_violation_fine = p.high_roller_worst_violation_fine
team.positions_filled_active = [p.selected_position for p in team.roster if
p.selected_position not in bench_positions]
@@ -383,7 +418,7 @@ def git_ls_remote(url: str):
return remote_refs
-def check_for_updates(use_default: bool = False):
+def check_github_for_updates(use_default: bool = False):
if not active_network_connection():
logger.info(
"No active network connection found. Unable to check for updates for the Fantasy Football Metrics Weekly "
@@ -434,9 +469,9 @@ def check_for_updates(use_default: bool = False):
if active_branch != target_branch:
if not use_default:
switch_branch = input(
- f"{Fore.YELLOW}You are {Fore.RED}not{Fore.YELLOW} on the deployment branch "
+ f"{Fore.YELLOW}You are {Fore.RED}not {Fore.YELLOW}on the deployment branch "
f"({Fore.GREEN}\"{target_branch}\"{Fore.YELLOW}) of the Fantasy Football Metrics Weekly Report "
- f"app. Do you want to switch to the {Fore.GREEN}\"{target_branch}\"{Fore.YELLOW} branch? "
+ f"app.\nDo you want to switch to the {Fore.GREEN}\"{target_branch}\"{Fore.YELLOW} branch? "
f"({Fore.GREEN}y{Fore.YELLOW}/{Fore.RED}n{Fore.YELLOW}) -> {Style.RESET_ALL}"
)
@@ -450,7 +485,7 @@ def check_for_updates(use_default: bool = False):
else:
logger.warning("You must select either \"y\" or \"n\".")
project_repo.remote(name="origin").set_url(origin_url)
- return check_for_updates(use_default)
+ return check_github_for_updates(use_default)
else:
logger.info("Use-default is set to \"true\". Automatically switching to deployment branch \"main\".")
project_repo.git.checkout(target_branch)
@@ -505,7 +540,7 @@ def check_for_updates(use_default: bool = False):
else:
logger.warning("Please only select \"y\" or \"n\".")
time.sleep(0.25)
- check_for_updates()
+ check_github_for_updates()
else:
logger.info(
f"The Fantasy Football Metrics Weekly Report app is {Fore.GREEN}up to date{Fore.WHITE} and running "
diff --git a/utilities/settings.py b/utilities/settings.py
index 307371a..2ea978f 100644
--- a/utilities/settings.py
+++ b/utilities/settings.py
@@ -232,6 +232,7 @@ class ReportSettings(CustomSettings):
league_optimal_score_rankings_bool: bool = Field(True, title=__qualname__)
league_bad_boy_rankings_bool: bool = Field(True, title=__qualname__)
league_beef_rankings_bool: bool = Field(True, title=__qualname__)
+ league_high_roller_rankings_bool: bool = Field(True, title=__qualname__)
league_weekly_top_scorers_bool: bool = Field(True, title=__qualname__)
league_weekly_low_scorers_bool: bool = Field(True, title=__qualname__)
league_weekly_highest_ce_bool: bool = Field(True, title=__qualname__)
@@ -240,6 +241,7 @@ class ReportSettings(CustomSettings):
team_points_by_position_charts_bool: bool = Field(True, title=__qualname__)
team_bad_boy_stats_bool: bool = Field(True, title=__qualname__)
team_beef_stats_bool: bool = Field(True, title=__qualname__)
+ team_high_roller_stats_bool: bool = Field(True, title=__qualname__)
team_boom_or_bust_bool: bool = Field(True, title=__qualname__)
font: str = Field(
@@ -254,11 +256,16 @@ class ReportSettings(CustomSettings):
)
font_size: int = Field(
12,
+ ge=8,
+ le=14,
title=__qualname__,
- description="set base font size (certain report element fonts resize dynamically based on the base font size)"
+ description=(
+ "set base font size so report element fonts resize dynamically (min: 8, max: 14)"
+ )
)
image_quality: int = Field(
75,
+ le=100,
title=__qualname__,
description=(
"specify player headshot image quality in percent (default: 75%), where higher quality (up to 100%) "
@@ -327,6 +334,13 @@ class AppSettings(CustomSettings):
title=__qualname__,
description="logger output level: notset, debug, info, warning, error, critical"
)
+ check_for_updates: bool = Field(
+ True,
+ title=__qualname__,
+ description=(
+ "automatically check GitHub for app updates and prompt user to update if local installation is out of date"
+ )
+ )
# output directories can be set to store your saved data and generated reports wherever you want
data_dir_path: Path = Field(
Path("output/data"),
diff --git a/utilities/utils.py b/utilities/utils.py
index 8cc4abe..977c5ae 100644
--- a/utilities/utils.py
+++ b/utilities/utils.py
@@ -16,11 +16,22 @@ def truncate_cell_for_display(cell_text: str, halve_max_chars: bool = False, ses
if halve_max_chars and sesqui_max_chars:
logger.warning(
- f"Max characters cannot be both halved and doubled. Defaulting to configure max characters: {max_chars}"
+ f"Max characters cannot be both halved and multiplied. Defaulting to configure max characters: {max_chars}"
)
elif halve_max_chars:
max_chars //= 2
elif sesqui_max_chars:
max_chars += (max_chars // 2)
- return f"{cell_text[:max_chars].strip()}..." if len(cell_text) > max_chars else cell_text
+ if len(cell_text) > max_chars:
+ # preserve footnote character on strings that need to be truncated
+ footnote_char = None
+ if cell_text.endswith("†") or cell_text.endswith("‡"):
+ footnote_char = cell_text[-1]
+ cell_text = cell_text[:-1]
+ max_chars -= 1
+
+ return f"{cell_text[:max_chars].strip()}...{footnote_char if footnote_char else ''}"
+
+ else:
+ return cell_text