import csv import math from decimal import ROUND_HALF_EVEN, Decimal from exceptions import logger import pandas as pd import pybaseball as pb import random import requests import time from db_calls import db_get from db_calls_card_creation import * from bs4 import BeautifulSoup # Card Creation Constants NEW_PLAYER_COST = 99999 # Sentinel value indicating a new player not yet priced RARITY_BASE_COSTS = { 1: 810, # Diamond 2: 270, # Gold 3: 90, # Silver 4: 30, # Bronze 5: 10, # Common 99: 2400, # Special/Legend } # Rarity Cost Adjustments # Maps (old_rarity, new_rarity) -> (cost_adjustment, minimum_cost) # When a player's rarity changes, adjust their cost by the specified amount # and enforce minimum cost if specified (None = no minimum) RARITY_COST_ADJUSTMENTS = { # From Diamond (1) (1, 2): (-540, 100), (1, 3): (-720, 50), (1, 4): (-780, 15), (1, 5): (-800, 5), (1, 99): (1600, None), # From Gold (2) (2, 1): (540, None), (2, 3): (-180, 50), (2, 4): (-240, 15), (2, 5): (-260, 5), (2, 99): (2140, None), # From Silver (3) (3, 1): (720, None), (3, 2): (180, None), (3, 4): (-60, 15), (3, 5): (-80, 5), (3, 99): (2320, None), # From Bronze (4) (4, 1): (780, None), (4, 2): (240, None), (4, 3): (60, None), (4, 5): (-20, 5), (4, 99): (2380, None), # From Common (5) (5, 1): (800, None), (5, 2): (260, None), (5, 3): (80, None), (5, 4): (20, None), (5, 99): (2400, None), # From Special/Legend (99) (99, 1): (-1600, 800), (99, 2): (-2140, 100), (99, 3): (-2320, 50), (99, 4): (-2380, 15), (99, 5): (-2400, 5), } # Default OPS Values (fallbacks when actual averages unavailable) # These are used to calculate player costs when we don't have enough data # to calculate rarity-specific averages from the cardset # Batter default OPS by rarity DEFAULT_BATTER_OPS = { 1: 1.066, # Diamond 2: 0.938, # Gold 3: 0.844, # Silver 4: 0.752, # Bronze 5: 0.612, # Common } # Starting Pitcher default OPS-against by rarity DEFAULT_STARTER_OPS = { 99: 0.388, # Special/Legend 1: 0.445, # Diamond 2: 0.504, # Gold 3: 0.568, # Silver 4: 0.634, # Bronze 5: 0.737, # Common } # Relief Pitcher default OPS-against by rarity DEFAULT_RELIEVER_OPS = { 99: 0.282, # Special/Legend 1: 0.375, # Diamond 2: 0.442, # Gold 3: 0.516, # Silver 4: 0.591, # Bronze 5: 0.702, # Common } # Franchise normalization: Convert city+team names to city-agnostic team names # This enables cross-era player matching (e.g., 'Oakland Athletics' -> 'Athletics') FRANCHISE_NORMALIZE = { "Arizona Diamondbacks": "Diamondbacks", "Atlanta Braves": "Braves", "Baltimore Orioles": "Orioles", "Boston Red Sox": "Red Sox", "Chicago Cubs": "Cubs", "Chicago White Sox": "White Sox", "Cincinnati Reds": "Reds", "Cleveland Guardians": "Guardians", "Colorado Rockies": "Rockies", "Detroit Tigers": "Tigers", "Houston Astros": "Astros", "Kansas City Royals": "Royals", "Los Angeles Angels": "Angels", "Los Angeles Dodgers": "Dodgers", "Miami Marlins": "Marlins", "Milwaukee Brewers": "Brewers", "Minnesota Twins": "Twins", "New York Mets": "Mets", "New York Yankees": "Yankees", "Oakland Athletics": "Athletics", "Philadelphia Phillies": "Phillies", "Pittsburgh Pirates": "Pirates", "San Diego Padres": "Padres", "San Francisco Giants": "Giants", "Seattle Mariners": "Mariners", "St Louis Cardinals": "Cardinals", "St. Louis Cardinals": "Cardinals", "Tampa Bay Rays": "Rays", "Texas Rangers": "Rangers", "Toronto Blue Jays": "Blue Jays", "Washington Nationals": "Nationals", } def normalize_franchise(franchise: str) -> str: """Convert city+team name to team-only (e.g., 'Oakland Athletics' -> 'Athletics')""" return FRANCHISE_NORMALIZE.get(franchise, franchise) D20_CHANCES = { "2": {"chances": 1, "inc": 0.05}, "3": {"chances": 2, "inc": 0.1}, "4": {"chances": 3, "inc": 0.15}, "5": {"chances": 4, "inc": 0.2}, "6": {"chances": 5, "inc": 0.25}, "7": {"chances": 6, "inc": 0.3}, "8": {"chances": 5, "inc": 0.25}, "9": {"chances": 4, "inc": 0.2}, "10": {"chances": 3, "inc": 0.15}, "11": {"chances": 2, "inc": 0.1}, "12": {"chances": 1, "inc": 0.05}, } BLANK_RESULTS = { "vL": { "1": { "2": {"result": None, "splits": None, "2d6": None}, "3": {"result": None, "splits": None, "2d6": None}, "4": {"result": None, "splits": None, "2d6": None}, "5": {"result": None, "splits": None, "2d6": None}, "6": {"result": None, "splits": None, "2d6": None}, "7": {"result": None, "splits": None, "2d6": None}, "8": {"result": None, "splits": None, "2d6": None}, "9": {"result": None, "splits": None, "2d6": None}, "10": {"result": None, "splits": None, "2d6": None}, "11": {"result": None, "splits": None, "2d6": None}, "12": {"result": None, "splits": None, "2d6": None}, "splits": 0, }, "2": { "2": {"result": None, "splits": None, "2d6": None}, "3": {"result": None, "splits": None, "2d6": None}, "4": {"result": None, "splits": None, "2d6": None}, "5": {"result": None, "splits": None, "2d6": None}, "6": {"result": None, "splits": None, "2d6": None}, "7": {"result": None, "splits": None, "2d6": None}, "8": {"result": None, "splits": None, "2d6": None}, "9": {"result": None, "splits": None, "2d6": None}, "10": {"result": None, "splits": None, "2d6": None}, "11": {"result": None, "splits": None, "2d6": None}, "12": {"result": None, "splits": None, "2d6": None}, "splits": 0, }, "3": { "2": {"result": None, "splits": None, "2d6": None}, "3": {"result": None, "splits": None, "2d6": None}, "4": {"result": None, "splits": None, "2d6": None}, "5": {"result": None, "splits": None, "2d6": None}, "6": {"result": None, "splits": None, "2d6": None}, "7": {"result": None, "splits": None, "2d6": None}, "8": {"result": None, "splits": None, "2d6": None}, "9": {"result": None, "splits": None, "2d6": None}, "10": {"result": None, "splits": None, "2d6": None}, "11": {"result": None, "splits": None, "2d6": None}, "12": {"result": None, "splits": None, "2d6": None}, "splits": 0, }, }, "vR": { "1": { "2": {"result": None, "splits": None, "2d6": None}, "3": {"result": None, "splits": None, "2d6": None}, "4": {"result": None, "splits": None, "2d6": None}, "5": {"result": None, "splits": None, "2d6": None}, "6": {"result": None, "splits": None, "2d6": None}, "7": {"result": None, "splits": None, "2d6": None}, "8": {"result": None, "splits": None, "2d6": None}, "9": {"result": None, "splits": None, "2d6": None}, "10": {"result": None, "splits": None, "2d6": None}, "11": {"result": None, "splits": None, "2d6": None}, "12": {"result": None, "splits": None, "2d6": None}, "splits": 0, }, "2": { "2": {"result": None, "splits": None, "2d6": None}, "3": {"result": None, "splits": None, "2d6": None}, "4": {"result": None, "splits": None, "2d6": None}, "5": {"result": None, "splits": None, "2d6": None}, "6": {"result": None, "splits": None, "2d6": None}, "7": {"result": None, "splits": None, "2d6": None}, "8": {"result": None, "splits": None, "2d6": None}, "9": {"result": None, "splits": None, "2d6": None}, "10": {"result": None, "splits": None, "2d6": None}, "11": {"result": None, "splits": None, "2d6": None}, "12": {"result": None, "splits": None, "2d6": None}, "splits": 0, }, "3": { "2": {"result": None, "splits": None, "2d6": None}, "3": {"result": None, "splits": None, "2d6": None}, "4": {"result": None, "splits": None, "2d6": None}, "5": {"result": None, "splits": None, "2d6": None}, "6": {"result": None, "splits": None, "2d6": None}, "7": {"result": None, "splits": None, "2d6": None}, "8": {"result": None, "splits": None, "2d6": None}, "9": {"result": None, "splits": None, "2d6": None}, "10": {"result": None, "splits": None, "2d6": None}, "11": {"result": None, "splits": None, "2d6": None}, "12": {"result": None, "splits": None, "2d6": None}, "splits": 0, }, }, } TESTING = False YES = ["y", "yes", "yeet", "please", "yeah"] CLUB_LIST = { "ANA": "Anaheim Angels", "ARI": "Arizona Diamondbacks", "ATH": "Athletics", "ATL": "Atlanta Braves", "BAL": "Baltimore Orioles", "BOS": "Boston Red Sox", "CHC": "Chicago Cubs", "CHW": "Chicago White Sox", "CIN": "Cincinnati Reds", "CLE": "Cleveland Guardians", "COL": "Colorado Rockies", "DET": "Detroit Tigers", "HOU": "Houston Astros", "KCR": "Kansas City Royals", "LAA": "Los Angeles Angels", "LAD": "Los Angeles Dodgers", "FLA": "Florida Marlins", "MIA": "Miami Marlins", "MIL": "Milwaukee Brewers", "MIN": "Minnesota Twins", "MON": "Montreal Expos", "NYM": "New York Mets", "NYY": "New York Yankees", "OAK": "Oakland Athletics", "PHI": "Philadelphia Phillies", "PIT": "Pittsburgh Pirates", "SDP": "San Diego Padres", "SEA": "Seattle Mariners", "SFG": "San Francisco Giants", "STL": "St Louis Cardinals", "TBD": "Tampa Bay Devil Rays", "TBR": "Tampa Bay Rays", "TEX": "Texas Rangers", "TOR": "Toronto Blue Jays", "WSN": "Washington Nationals", "TOT": "None", "2 Tms": "None", "2TM": "None", "3 Tms": "None", "3TM": "None", "4 Tms": "None", "4TM": "None", } FRANCHISE_LIST = { "ANA": "Angels", "ARI": "Diamondbacks", "ATH": "Athletics", "ATL": "Braves", "BAL": "Orioles", "BOS": "Red Sox", "CHC": "Cubs", "CHW": "White Sox", "CIN": "Reds", "CLE": "Guardians", "COL": "Rockies", "DET": "Tigers", "FLA": "Marlins", "HOU": "Astros", "KCR": "Royals", "LAA": "Angels", "LAD": "Dodgers", "MIA": "Marlins", "MIL": "Brewers", "MIN": "Twins", "MON": "Nationals", # Expos -> Nationals franchise "NYM": "Mets", "NYY": "Yankees", "OAK": "Athletics", "PHI": "Phillies", "PIT": "Pirates", "SDP": "Padres", "SEA": "Mariners", "SFG": "Giants", "STL": "Cardinals", "TBD": "Rays", "TBR": "Rays", "TEX": "Rangers", "TOR": "Blue Jays", "WSN": "Nationals", "TOT": "None", "2 Tms": "None", "2TM": "None", "3 Tms": "None", "3TM": "None", "4 Tms": "None", "4TM": "None", } # PLAYER_DB_BACKUP = { # 684007: {'key_mlbam': 684007, 'key_retro': 'imans001', 'key_fangraphs': 33829, 'key_bbref': 'imanash01'}, # # } def get_args(args): logger.info(f"Process arguments: {args}") final_args = {} for x in args: if "=" not in x: raise TypeError(f"Invalid = argument: {x}") key, value = x.split("=") logger.info(f"key: {key} / value: {value}") if key in final_args: raise ValueError(f"Duplicate argument: {key}") final_args[key] = value return final_args def should_update_player_description( cardset_name: str, player_cost: int, current_description: str, new_description: str ) -> bool: """ Determine if a player's description should be updated. Business logic for description updates: - Promo cardsets: Only update NEW players (cost == NEW_PLAYER_COST) - Regular cardsets: Update if description differs and not a PotM card Args: cardset_name: Name of the cardset (e.g., "2024 Promos", "2025 Season") player_cost: Current cost of the player (NEW_PLAYER_COST indicates new player) current_description: Player's current description new_description: Proposed new description Returns: True if description should be updated, False otherwise Examples: >>> should_update_player_description("2024 Promos", 99999, "", "May") True # New promo card, set description >>> should_update_player_description("2024 Promos", 100, "April", "May") False # Existing promo card, keep "April" >>> should_update_player_description("2025 Season", 100, "2024", "2025") True # Regular cardset, update outdated description >>> should_update_player_description("2025 Season", 100, "April PotM", "2025") False # PotM card, never update """ is_promo_cardset = "promo" in cardset_name.lower() if is_promo_cardset: # For promo cardsets: only update NEW players return player_cost == NEW_PLAYER_COST else: # For regular cardsets: update if different and not PotM is_potm = "potm" in current_description.lower() is_different = current_description != new_description return is_different and not is_potm def calculate_rarity_cost_adjustment( old_rarity: int, new_rarity: int, old_cost: int ) -> int: """ Calculate new cost when a player's rarity changes. Uses the RARITY_COST_ADJUSTMENTS lookup table to determine the cost adjustment and minimum cost when a player moves between rarity tiers. Args: old_rarity: Current rarity tier (1-5, 99) new_rarity: New rarity tier (1-5, 99) old_cost: Current player cost Returns: New cost after adjustment (with minimum enforced if applicable) Examples: >>> calculate_rarity_cost_adjustment(1, 2, 1000) 460 # Diamond to Gold: 1000 - 540 = 460, min 100 → 460 >>> calculate_rarity_cost_adjustment(1, 5, 100) 5 # Diamond to Common: 100 - 800 = -700, min 5 → 5 >>> calculate_rarity_cost_adjustment(5, 1, 50) 850 # Common to Diamond: 50 + 800 = 850, no min → 850 >>> calculate_rarity_cost_adjustment(3, 3, 100) 100 # No change: same rarity returns same cost """ # No change if rarity stays the same if old_rarity == new_rarity: return old_cost # Look up the adjustment and minimum cost adjustment_data = RARITY_COST_ADJUSTMENTS.get((old_rarity, new_rarity)) if adjustment_data is None: # No defined adjustment for this transition - return old cost logger.warning( f"creation_helpers.calculate_rarity_cost_adjustment - No cost adjustment defined for " f"rarity change {old_rarity} → {new_rarity}. Keeping cost at {old_cost}." ) return old_cost cost_adjustment, min_cost = adjustment_data # Calculate new cost new_cost = old_cost + cost_adjustment # Apply minimum cost if specified if min_cost is not None: new_cost = max(new_cost, min_cost) return new_cost async def pd_players_df(cardset_id: int): p_query = await db_get( "players", params=[("inc_dex", False), ("cardset_id", cardset_id), ("short_output", True)], ) if p_query["count"] == 0: return pd.DataFrame( { "player_id": [], "p_name": [], "cost": [], "image": [], "image2": [], "mlbclub": [], "franchise": [], "cardset": [], "set_num": [], "rarity": [], "pos_1": [], "pos_2": [], "pos_3": [], "pos_4": [], "pos_5": [], "pos_6": [], "pos_7": [], "pos_8": [], "headshot": [], "vanity_card": [], "strat_code": [], "bbref_id": [], "fangr_id": [], "description": [], "quantity": [], "mlbplayer": [], } ) return pd.DataFrame(p_query["players"]) async def pd_positions_df(cardset_id: int): pos_query = await db_get( "cardpositions", params=[ ("cardset_id", cardset_id), ("short_output", True), ("sort", "innings-desc"), ], ) if pos_query["count"] == 0: raise ValueError("No position ratings returned from Paper Dynasty API") all_pos = pd.DataFrame(pos_query["positions"]).rename( columns={"player": "player_id"} ) return all_pos def get_pitching_peripherals(season: int): url = f"https://www.baseball-reference.com/leagues/majors/{season}-standard-pitching.shtml" soup = BeautifulSoup(requests.get(url).text, "html.parser") time.sleep(3) table = soup.find("table", {"id": "players_standard_pitching"}) headers = [] data = [] indeces = [] for row in table.find_all("tr"): row_data = [] col_names = [] for cell in row.find_all("td"): try: player_id = cell["data-append-csv"] row_data.append(player_id) if len(headers) == 0: col_names.append("key_bbref") except Exception: pass row_data.append(cell.text) if len(headers) == 0: col_names.append(cell["data-stat"]) if len(row_data) > 0: data.append(row_data) indeces.append(row_data[0]) if len(headers) == 0: headers.extend(col_names) pit_frame = pd.DataFrame(data, index=indeces, columns=headers).query( "key_bbref == key_bbref" ) return pit_frame.drop_duplicates(subset=["key_bbref"], keep="first") def mround(x, prec=2, base=0.05): num, to = Decimal(str(x)), Decimal(str(base)) return float(round(num / to) * to) # return float(round(Decimal(str(x)) / Decimal(str(base)) * Decimal(str(base)), prec)) # return round(base * round(float(x) / base), prec) def chances_from_row(row_num): if row_num == "2" or row_num == "12": return 1 if row_num == "3" or row_num == "11": return 2 if row_num == "4" or row_num == "10": return 3 if row_num == "5" or row_num == "9": return 4 if row_num == "6" or row_num == "8": return 5 if row_num == "7": return 6 raise ValueError(f"No chance count found for row_num {row_num}") def legal_splits(tot_chances): legal_2d6 = [] for x in D20_CHANCES: num_incs = mround(tot_chances) / D20_CHANCES[x]["inc"] if num_incs - int(num_incs) == 0 and int(20 - num_incs) > 0: legal_2d6.append( { "2d6": int(x), "incs": int(num_incs), "bad_chances": mround( D20_CHANCES[x]["chances"] * (int(20 - num_incs) / 20) ), "bad_incs": int(20 - num_incs), } ) random.shuffle(legal_2d6) # if TESTING: print(f'tot_chances: {myround(tot_chances)}') # if TESTING: print(f'legal_2d6: {legal_2d6}') return legal_2d6 def result_string(tba_data, row_num, split_min=None, split_max=None): bold1 = f'{"" if tba_data["bold"] else ""}' bold2 = f'{"" if tba_data["bold"] else ""}' row_string = f'{" " if int(row_num) < 10 else ""}{row_num}' if TESTING: print( f'adding {tba_data["string"]} to row {row_num} / ' f"split_min: {split_min} / split_max: {split_max}" ) # No splits; standard result if not split_min: return f'{bold1}{row_string}-{tba_data["string"]}{bold2}' # With splits split_nums = f'{split_min if split_min != 20 else ""}{"-" if split_min != 20 else ""}{split_max}' data_string = ( tba_data["sm-string"] if "sm-string" in tba_data.keys() else tba_data["string"] ) spaces = 18 - len(data_string) - len(split_nums) if "WALK" in data_string: spaces -= 3 elif "SI**" in data_string: spaces += 1 elif "DO*" in data_string: spaces -= 1 elif "DO*" in data_string: spaces -= 2 elif "so" in data_string: spaces += 3 elif "gb" in data_string: spaces -= 3 if TESTING: print( f'len(tba_data["string"]): {len(data_string)} / len(split_nums): {len(split_nums)} ' f"spaces: {spaces}" ) if split_min == 1 or split_min is None: row_output = f"{row_string}-" else: row_output = " " if TESTING: print(f"row_output: {row_output}") return f'{bold1}{row_output}{data_string}{" " * spaces}{split_nums}{bold2}' def result_data( tba_data, row_num, tba_data_bottom=None, top_split_max=None, fatigue=False ): ret_data = {} top_bold1 = f'{"" if tba_data["bold"] else ""}' top_bold2 = f'{"" if tba_data["bold"] else ""}' bot_bold1 = None bot_bold2 = None if tba_data_bottom: bot_bold1 = f'{"" if tba_data_bottom["bold"] else ""}' bot_bold2 = f'{"" if tba_data_bottom["bold"] else ""}' if tba_data_bottom is None: ret_data["2d6"] = f"{top_bold1}{int(row_num)}-{top_bold2}" ret_data["splits"] = f"{top_bold1}‎{top_bold2}" ret_data["result"] = ( f"{top_bold1}" f'{tba_data["string"]}{" •" if fatigue else ""}' f"{top_bold2}" ) else: ret_data["2d6"] = f"{top_bold1}{int(row_num)}-{top_bold2}\n" ret_data["splits"] = ( f'{top_bold1}1{"-" if top_split_max != 1 else ""}' f'{top_split_max if top_split_max != 1 else ""}{top_bold2}\n' f'{bot_bold1}{top_split_max+1}{"-20" if top_split_max != 19 else ""}{bot_bold2}' ) ret_data["result"] = ( f'{top_bold1}{tba_data["sm-string"] if "sm-string" in tba_data.keys() else tba_data["string"]}' f"{top_bold2}\n" f"{bot_bold1}" f'{tba_data_bottom["sm-string"] if "sm-string" in tba_data_bottom.keys() else tba_data_bottom["string"]}' f"{bot_bold2}" ) return ret_data def get_of(batter_hand, pitcher_hand, pull_side=True): if batter_hand == "R": return "lf" if pull_side else "rf" if batter_hand == "L": return "rf" if pull_side else "lf" if batter_hand == "S": if pitcher_hand == "L": return "rf" if pull_side else "rf" else: return "lf" if pull_side else "lf" def get_col(col_num): if col_num == "1": return "one" if col_num == "2": return "two" if col_num == "3": return "three" def write_to_csv(output_path, file_name: str, row_data: list): # Build the csv output fpath = (output_path / f"{file_name}").with_suffix(".csv") # logger.info(f'Printing following data to {file_name}:\n\n{row_data}') with fpath.open(mode="w+", newline="", encoding="utf-8") as csv_File: writer = csv.writer(csv_File) writer.writerows(row_data) def get_position_string(all_pos: list, inc_p: bool): if len(all_pos) == 0: return "dh" of_arm = None of_error = None of_innings = None lf_range = None lf_innings = 0 cf_range = None cf_innings = 0 rf_range = None rf_innings = 0 all_def = [] for x in all_pos: if x.position == "OF": of_arm = f'{"+" if "-" not in x.arm else ""}{x.arm}' of_error = x.error of_innings = x.innings elif x.position == "CF": cf_range = x.range cf_innings = x.innings elif x.position == "LF": lf_range = x.range lf_innings = x.innings elif x.position == "RF": rf_range = x.range rf_innings = x.innings elif x.position == "C": all_def.append( ( f'c-{x.range}({"+" if int(x.arm) >= 0 else ""}{x.arm}) e{x.error} T-{x.overthrow}(pb-{x.pb})', x.innings, ) ) elif "P" in x.position and not inc_p: pass else: all_def.append((f"{x.position.lower()}-{x.range}e{x.error}", x.innings)) if of_arm is not None: logger.info( f"\n\nProcessing OF player ID {all_pos[0].player_id}\nlf-{lf_range} / cf-{cf_range} / rf-{rf_range}" ) all_of = [] if lf_innings > 0: all_of.append((lf_range, lf_innings, "lf")) if cf_innings > 0: all_of.append((cf_range, cf_innings, "cf")) if rf_innings > 0: all_of.append((rf_range, rf_innings, "rf")) logger.info(f"all_of: {all_of}") if len(all_of) > 0: all_of.sort(key=lambda y: y[1], reverse=True) logger.info(f"sorted of: {all_of}") out_string = f"{all_of[0][2]}-{all_of[0][0]}({of_arm})e{of_error}" if len(all_of) >= 2: out_string += f", {all_of[1][2]}-{all_of[1][0]}e{of_error}" if len(all_of) >= 3: out_string += f", {all_of[2][2]}-{all_of[2][0]}e{of_error}" logger.info(f"of string: {out_string}") all_def.append((out_string, of_innings)) all_def.sort(key=lambda z: z[1], reverse=True) final_defense = "" for x in all_def: if len(final_defense) > 0: final_defense += ", " final_defense += f"{x[0]}" return final_defense def ordered_positions(all_pos: list) -> list: if len(all_pos) == 0: return ["DH"] all_def = [] for x in all_pos: if x.position not in ["OF", "P", "SP", "RP", "CP"]: all_def.append((x.innings, x.position)) all_def.sort(key=lambda y: y[0], reverse=True) return [x[1] for x in all_def] def ordered_pitching_positions(all_pos: list) -> list: all_def = [] for x in all_pos: if x.position in ["SP", "RP", "CP"]: all_def.append((x.innings, x.position)) all_def.sort(key=lambda y: y[0], reverse=True) return [x[1] for x in all_def] def defense_rg(all_pos: list) -> list: rg_data = [ None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, ] all_pitcher = True for line in all_pos: if "P" not in line.position: all_pitcher = False break for line in all_pos: if line.position == "P" and all_pitcher: this_pit = PitcherData.get_or_none( PitcherData.player == line.player, PitcherData.cardset == line.cardset ) if this_pit: rg_data[0] = line.range rg_data[9] = line.error rg_data[22] = this_pit.wild_pitch rg_data[23] = this_pit.balk elif line.position == "C": rg_data[1] = line.range rg_data[10] = line.error rg_data[19] = line.arm rg_data[20] = line.overthrow rg_data[21] = line.pb elif line.position == "1B": rg_data[2] = line.range rg_data[11] = line.error elif line.position == "2B": rg_data[3] = line.range rg_data[12] = line.error elif line.position == "3B": rg_data[4] = line.range rg_data[13] = line.error elif line.position == "SS": rg_data[5] = line.range rg_data[14] = line.error elif line.position == "LF": rg_data[6] = line.range elif line.position == "CF": rg_data[7] = line.range elif line.position == "RF": rg_data[8] = line.range elif line.position == "OF": rg_data[15] = line.error rg_data[16] = line.error rg_data[17] = line.error rg_data[18] = line.arm return rg_data def sanitize_chance_output(total_chances, min_chances=1.0, rounding=0.05): if total_chances < min_chances: logger.debug( f"sanitize: {total_chances} is less than min_chances ({min_chances}); returning 0" ) return 0 rounded_decimal = mround(total_chances, base=rounding) if rounding == 1.0: return rounded_decimal exact_chances = [ Decimal("1.05"), Decimal("1.1"), Decimal("1.2"), Decimal("1.25"), Decimal("1.3"), Decimal("1.35"), Decimal("1.4"), Decimal("1.5"), Decimal("1.6"), Decimal("1.65"), Decimal("1.7"), Decimal("1.75"), Decimal("1.8"), Decimal("1.9"), Decimal("1.95"), Decimal("2.1"), Decimal("2.2"), Decimal("2.25"), Decimal("2.4"), Decimal("2.5"), Decimal("2.55"), Decimal("2.6"), Decimal("2.7"), Decimal("2.75"), Decimal("2.8"), Decimal("2.85"), Decimal("3.2"), Decimal("3.25"), Decimal("3.3"), Decimal("3.4"), Decimal("3.5"), Decimal("3.6"), Decimal("3.75"), Decimal("3.8"), Decimal("3.9"), Decimal("4.2"), Decimal("4.25"), Decimal("4.5"), Decimal("4.75"), Decimal("4.8"), Decimal("5.1"), Decimal("5.4"), Decimal("5.7"), ] if rounded_decimal > exact_chances[-1]: return rounded_decimal for x in exact_chances: if rounded_decimal <= x: return float(x) raise ArithmeticError( f"Attempt to sanitize {total_chances} rounded to {rounded_decimal} and could not be matched to an exact result" ) def legacy_sanitize_chance_output( total_chances: float, min_chances: float = 1.0, rounding: float = 0.05 ): # r_val = mround(total_chances) if total_chances >= min_chances else 0 r_val = Decimal(total_chances) if total_chances >= min_chances else Decimal(0) logger.debug(f"r_val: {r_val}") rounded_val = Decimal( float(math.floor(r_val / Decimal(rounding)) * Decimal(rounding)) ).quantize(Decimal("0.05"), ROUND_HALF_EVEN) if math.floor(rounded_val) == rounded_val: return rounded_val exact_chances = [ Decimal("1.05"), Decimal("1.1"), Decimal("1.2"), Decimal("1.25"), Decimal("1.3"), Decimal("1.35"), Decimal("1.4"), Decimal("1.5"), Decimal("1.6"), Decimal("1.65"), Decimal("1.7"), Decimal("1.75"), Decimal("1.8"), Decimal("1.9"), Decimal("1.95"), Decimal("2.1"), Decimal("2.2"), Decimal("2.25"), Decimal("2.4"), Decimal("2.5"), Decimal("2.55"), Decimal("2.6"), Decimal("2.7"), Decimal("2.75"), Decimal("2.8"), Decimal("2.85"), Decimal("3.2"), Decimal("3.25"), Decimal("3.3"), Decimal("3.4"), Decimal("3.5"), Decimal("3.6"), Decimal("3.75"), Decimal("3.8"), Decimal("3.9"), Decimal("4.2"), Decimal("4.25"), Decimal("4.5"), Decimal("4.75"), Decimal("4.8"), Decimal("5.1"), Decimal("5.4"), Decimal("5.7"), ] if rounded_val > exact_chances[-1]: return rounded_val for x in exact_chances: if rounded_val <= x: return x def mlbteam_and_franchise(mlbam_playerid): api_url = ( f"https://statsapi.mlb.com/api/v1/people/{mlbam_playerid}?hydrate=currentTeam" ) logger.info(f"Calling {api_url}") p_data = {"mlbclub": None, "franchise": None} # club_list = [ # 'Arizona Diamondbacks', # 'Atlanta Braves', # 'Baltimore Orioles', # 'Boston Red Sox', # 'Chicago Cubs', # 'Chicago White Sox', # 'Cincinnati Reds', # 'Cleveland Guardians', # 'Colorado Rockies', # 'Detroit Tigers', # 'Houston Astros', # 'Kansas City Royals', # 'Los Angeles Angels', # 'Los Angeles Dodgers', # 'Miami Marlins', # 'Milwaukee Brewers', # 'Minnesota Twins', # 'New York Mets', # 'New York Yankees', # 'Oakland Athletics', # 'Philadelphia Phillies', # 'Pittsburgh Pirates', # 'San Diego Padres', # 'Seattle Mariners', # 'San Francisco Giants', # 'St Louis Cardinals', # 'Tampa Bay Rays', # 'Texas Rangers', # 'Toronto Blue Jays', # 'Washington Nationals' # ] try: resp = requests.get(api_url, timeout=2) except requests.ReadTimeout: logger.error( f"mlbteam_and_franchise - ReadTimeout pull MLB team for MLB AM player ID {mlbam_playerid}" ) return p_data if resp.status_code == 200: data = resp.json() data = data["people"][0] logger.debug(f"data: {data}") if data["currentTeam"]["name"] in CLUB_LIST.values(): p_data["mlbclub"] = data["currentTeam"]["name"] p_data["franchise"] = normalize_franchise(data["currentTeam"]["name"]) else: logger.error( f'Could not set team for {mlbam_playerid}; received {data["currentTeam"]["name"]}' ) else: logger.error( f"mlbteam_and_franchise - Bad response from mlbstatsapi: {resp.status_code}" ) return p_data def get_all_pybaseball_ids( player_id: list, key_type: str, is_custom: bool = False, full_name: str = None ): if is_custom: try: long_player_id = int(player_id[0]) if long_player_id >= 999942001: backyard_players = [ "akhan", "amkhan", "adelvecchio", "afrazier", "awebber", "bblackwood", "drobinson", "dpetrovich", "esteele", "ghasselhoff", "jsmith", "jgarcia", "kkawaguchi", "kphillips", "keckman", "lcrocket", "llui", "mluna", "mdubois", "mthomas", "psanchez", "pwheeler", "rworthington", "rjohnson", "rdobbs", "sdobbs", "swebber", "smorgan", "tdelvecchio", "vkawaguchi", ] return pd.Series( { "key_bbref": backyard_players[long_player_id - 999942001], "key_fangraphs": player_id[0], "key_mlbam": player_id[0], "bat_hand": ( "L" if long_player_id in [ 999942004, 999942007, 999942010, 999942018, 999942019, 999942020, 999942022, ] else "R" ), }, ) except Exception as e: logger.warning(e) banned_ids = { "fangraphs": [24816, 25782, 26548], "bbref": ["pagesan01", "pagespe01", "pagespe02", "garcian02"], } if player_id[0] in banned_ids[key_type]: logger.info(f"Player ID {player_id[0]} is banned in the {key_type} list.") return None q = pb.playerid_reverse_lookup(player_id, key_type=key_type) if len(q.values) > 0: return_val = q.loc[0] # Check manual players elif full_name is not None: names = full_name.split(" ") q = pb.playerid_lookup(last=names[-1], first=" ".join(names[:-1]), fuzzy=True) if len(q.values) == 0: logger.error( f"get_all_pybaseball_ids - Could not find id {player_id} / {key_type} or " f"{full_name} / full name in pybaseball" ) return None elif len(q.values) > 1: # q = q.drop(q[q['mlb_played_last'].isnull()]) # q.astype({'mlb_played_last': 'int32'}) # q = q.dropna() q = q.drop(q[q["mlb_played_last"] == ""].index) q = q.sort_values(by=["mlb_played_last"], ascending=False) return_val = q.loc[0] return_val["key_fangraphs"] = player_id[0] else: logger.error( f"get_all_pybaseball_ids - Could not find id {player_id} / {key_type} in pybaseball" ) return_val = None # p_query = await db_get('mlbplayers', params=[(f'key_{key_type}', player_id)]) # if p_query['count'] > 0: # return_val = pd.DataFrame(p_query['players']) # else: # logger.error(f'get_all_pybaseball_ids - Could not find id {player_id} / {key_type} in PD mlbplayers table') # return_val = None return return_val def sanitize_name(start_name: str) -> str: return ( start_name.replace("é", "e") .replace("á", "a") .replace(".", "") .replace("Á", "A") .replace("ñ", "n") .replace("ó", "o") .replace("í", "i") .replace("ú", "u") .replace("'", "") .replace("-", " ") ) def get_hand(df_data): try: if df_data["Name"][-1] == "*": return "L" elif df_data["Name"][-1] == "#": return "S" else: return "R" except Exception: logger.error(f'Error in get_hand for {df_data["Name"]}') return "R"