fix: guard against T4 rarity upgrade collision in live-series pipeline (#57)
Closes #57 - Add RARITY_LADDER and rarity_is_downgrade() to rarity_thresholds.py - Add get_fully_evolved_players() to db_calls.py — queries a to-be-created database endpoint; returns empty set safely if endpoint is unavailable - In batters/creation.py post_player_updates(): pre-flight check identifies players where OPS rarity would downgrade, then guards the rarity write to skip any downgrade for fully-evolved (T4) cards - Same guard added to pitchers/creation.py post_player_updates() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ae73ce6755
commit
aaa2eaa252
@ -18,11 +18,11 @@ from creation_helpers import (
|
||||
calculate_rarity_cost_adjustment,
|
||||
DEFAULT_BATTER_OPS,
|
||||
)
|
||||
from db_calls import db_post, db_get, db_put, db_patch
|
||||
from db_calls import db_post, db_get, db_put, db_patch, get_fully_evolved_players
|
||||
from . import calcs_batter as cba
|
||||
from defenders import calcs_defense as cde
|
||||
from exceptions import logger
|
||||
from rarity_thresholds import get_batter_thresholds
|
||||
from rarity_thresholds import get_batter_thresholds, rarity_is_downgrade
|
||||
|
||||
|
||||
async def pd_battingcards_df(cardset_id: int):
|
||||
@ -384,6 +384,24 @@ async def post_player_updates(
|
||||
)
|
||||
|
||||
player_updates = {} # { <player_id> : [ (param pairs) ] }
|
||||
|
||||
# T4 rarity guard: identify players where OPS-derived rarity would be a
|
||||
# downgrade and check whether any of them have a fully-evolved refractor
|
||||
# state. If so, their current (T4-earned) rarity is the floor and must
|
||||
# not be overwritten. Falls back to empty set if the API endpoint is
|
||||
# unavailable, so the guard degrades safely without blocking the pipeline.
|
||||
downgrade_candidates = player_data[
|
||||
player_data.apply(
|
||||
lambda r: rarity_is_downgrade(r["rarity"], r["new_rarity_id"]), axis=1
|
||||
)
|
||||
]["player_id"].tolist()
|
||||
t4_protected_ids = await get_fully_evolved_players(downgrade_candidates)
|
||||
if t4_protected_ids:
|
||||
logger.info(
|
||||
f"batters.creation.post_player_updates - {len(t4_protected_ids)} player(s) "
|
||||
f"protected from rarity downgrade by T4 refractor floor: {t4_protected_ids}"
|
||||
)
|
||||
|
||||
rarity_group = player_data.query("rarity == new_rarity_id").groupby("rarity")
|
||||
average_ops = rarity_group["total_OPS"].mean().to_dict()
|
||||
|
||||
@ -454,13 +472,27 @@ async def post_player_updates(
|
||||
)
|
||||
|
||||
elif df_data["rarity"] != df_data["new_rarity_id"]:
|
||||
# Calculate adjusted cost for rarity change using lookup table
|
||||
new_cost = calculate_rarity_cost_adjustment(
|
||||
old_rarity=df_data["rarity"],
|
||||
new_rarity=df_data["new_rarity_id"],
|
||||
old_cost=df_data["cost"],
|
||||
)
|
||||
params.extend([("cost", new_cost), ("rarity_id", df_data["new_rarity_id"])])
|
||||
# T4 guard: skip rarity downgrades for fully-evolved cards so that
|
||||
# a T4-earned rarity bump is not silently reverted by the pipeline.
|
||||
if (
|
||||
rarity_is_downgrade(df_data["rarity"], df_data["new_rarity_id"])
|
||||
and df_data.player_id in t4_protected_ids
|
||||
):
|
||||
logger.info(
|
||||
f"batters.creation.post_player_updates - Skipping rarity downgrade "
|
||||
f"for player_id={df_data.player_id}: T4 floor rarity={df_data['rarity']}, "
|
||||
f"OPS rarity={df_data['new_rarity_id']}"
|
||||
)
|
||||
else:
|
||||
# Calculate adjusted cost for rarity change using lookup table
|
||||
new_cost = calculate_rarity_cost_adjustment(
|
||||
old_rarity=df_data["rarity"],
|
||||
new_rarity=df_data["new_rarity_id"],
|
||||
old_cost=df_data["cost"],
|
||||
)
|
||||
params.extend(
|
||||
[("cost", new_cost), ("rarity_id", df_data["new_rarity_id"])]
|
||||
)
|
||||
|
||||
if len(params) > 0:
|
||||
if df_data.player_id not in player_updates.keys():
|
||||
|
||||
25
db_calls.py
25
db_calls.py
@ -186,6 +186,31 @@ async def db_delete(endpoint: str, object_id: int, api_ver: int = 2, timeout=3)
|
||||
raise ValueError(f"DB: {e}")
|
||||
|
||||
|
||||
async def get_fully_evolved_players(player_ids: list) -> set:
|
||||
"""Return the subset of player_ids that have any fully-evolved (T4) refractor card state.
|
||||
|
||||
Calls GET /api/v2/refractor/fully-evolved with a comma-separated player_ids
|
||||
query parameter. Returns an empty set if the endpoint is unavailable (404,
|
||||
error, or missing field) so the caller degrades safely — no T4 protection is
|
||||
applied rather than blocking the pipeline.
|
||||
|
||||
NOTE: This endpoint does not yet exist in the database API. It must return
|
||||
{"player_ids": [<int>, ...]} listing player IDs with fully_evolved=True.
|
||||
Until it is added, this function always returns an empty set.
|
||||
"""
|
||||
if not player_ids:
|
||||
return set()
|
||||
ids_param = ",".join(str(pid) for pid in player_ids)
|
||||
result = await db_get(
|
||||
"refractor/fully-evolved",
|
||||
params=[("player_ids", ids_param)],
|
||||
none_okay=True,
|
||||
)
|
||||
if result is None or "player_ids" not in result:
|
||||
return set()
|
||||
return set(result["player_ids"])
|
||||
|
||||
|
||||
def get_player_data(
|
||||
player_id: str,
|
||||
id_type: Literal["bbref", "fangraphs"],
|
||||
|
||||
@ -17,11 +17,11 @@ from creation_helpers import (
|
||||
DEFAULT_STARTER_OPS,
|
||||
DEFAULT_RELIEVER_OPS,
|
||||
)
|
||||
from db_calls import db_post, db_get, db_put, db_patch
|
||||
from db_calls import db_post, db_get, db_put, db_patch, get_fully_evolved_players
|
||||
from defenders import calcs_defense as cde
|
||||
from . import calcs_pitcher as cpi
|
||||
from exceptions import logger
|
||||
from rarity_thresholds import get_pitcher_thresholds
|
||||
from rarity_thresholds import get_pitcher_thresholds, rarity_is_downgrade
|
||||
|
||||
|
||||
def get_pitching_stats(
|
||||
@ -463,6 +463,20 @@ async def post_player_updates(
|
||||
)
|
||||
|
||||
player_updates = {} # { <player_id> : [ (param pairs) ] }
|
||||
|
||||
# T4 rarity guard: same protection as batters — see batters/creation.py.
|
||||
downgrade_candidates = player_data[
|
||||
player_data.apply(
|
||||
lambda r: rarity_is_downgrade(r["rarity"], r["new_rarity_id"]), axis=1
|
||||
)
|
||||
]["player_id"].tolist()
|
||||
t4_protected_ids = await get_fully_evolved_players(downgrade_candidates)
|
||||
if t4_protected_ids:
|
||||
logger.info(
|
||||
f"pitchers.creation.post_player_updates - {len(t4_protected_ids)} player(s) "
|
||||
f"protected from rarity downgrade by T4 refractor floor: {t4_protected_ids}"
|
||||
)
|
||||
|
||||
sp_rarity_group = player_data.query(
|
||||
"rarity == new_rarity_id and starter_rating >= 4"
|
||||
).groupby("rarity")
|
||||
@ -551,13 +565,27 @@ async def post_player_updates(
|
||||
)
|
||||
|
||||
elif df_data["rarity"] != df_data["new_rarity_id"]:
|
||||
# Calculate adjusted cost for rarity change using lookup table
|
||||
new_cost = calculate_rarity_cost_adjustment(
|
||||
old_rarity=df_data["rarity"],
|
||||
new_rarity=df_data["new_rarity_id"],
|
||||
old_cost=df_data["cost"],
|
||||
)
|
||||
params.extend([("cost", new_cost), ("rarity_id", df_data["new_rarity_id"])])
|
||||
# T4 guard: skip rarity downgrades for fully-evolved cards so that
|
||||
# a T4-earned rarity bump is not silently reverted by the pipeline.
|
||||
if (
|
||||
rarity_is_downgrade(df_data["rarity"], df_data["new_rarity_id"])
|
||||
and df_data.player_id in t4_protected_ids
|
||||
):
|
||||
logger.info(
|
||||
f"pitchers.creation.post_player_updates - Skipping rarity downgrade "
|
||||
f"for player_id={df_data.player_id}: T4 floor rarity={df_data['rarity']}, "
|
||||
f"OPS rarity={df_data['new_rarity_id']}"
|
||||
)
|
||||
else:
|
||||
# Calculate adjusted cost for rarity change using lookup table
|
||||
new_cost = calculate_rarity_cost_adjustment(
|
||||
old_rarity=df_data["rarity"],
|
||||
new_rarity=df_data["new_rarity_id"],
|
||||
old_cost=df_data["cost"],
|
||||
)
|
||||
params.extend(
|
||||
[("cost", new_cost), ("rarity_id", df_data["new_rarity_id"])]
|
||||
)
|
||||
|
||||
if len(params) > 0:
|
||||
if df_data.player_id not in player_updates.keys():
|
||||
|
||||
@ -144,3 +144,25 @@ def get_batter_thresholds(season: int) -> BatterRarityThresholds:
|
||||
return BATTER_THRESHOLDS_2025
|
||||
else:
|
||||
return BATTER_THRESHOLDS_2024
|
||||
|
||||
|
||||
# Ordered from worst to best rarity. Used for ladder comparisons such as the
|
||||
# T4 refractor guard — do not change the order.
|
||||
RARITY_LADDER = [5, 4, 3, 2, 1, 99] # Common → Bronze → Silver → Gold → Diamond → HoF
|
||||
|
||||
|
||||
def rarity_is_downgrade(current_rarity_id: int, new_rarity_id: int) -> bool:
|
||||
"""Return True if new_rarity_id is a less prestigious tier than current_rarity_id.
|
||||
|
||||
Uses the RARITY_LADDER ordering. Unknown IDs are treated as position 0
|
||||
(worst), so an unknown current rarity will never trigger a downgrade guard.
|
||||
"""
|
||||
try:
|
||||
current_pos = RARITY_LADDER.index(current_rarity_id)
|
||||
except ValueError:
|
||||
return False
|
||||
try:
|
||||
new_pos = RARITY_LADDER.index(new_rarity_id)
|
||||
except ValueError:
|
||||
return False
|
||||
return current_pos > new_pos
|
||||
|
||||
Loading…
Reference in New Issue
Block a user