feat: game importance factor in hype scoring
Adds calculate_game_importance() that boosts Priority for high-stakes regular-season matchups based on season progress (sharp ramp after game 55), playoff bubble proximity (wildcard rank ~17-19 = max relevance), and divisional/conference rivalry (1.4x/1.2x multipliers). Max bonus 150 pts applied to both LIVE and PRE games; playoff and FINAL games are unaffected. Extends standings schema with division, conference, games_played, and wildcard_sequence fields fetched from the NHL API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
84
app/games.py
84
app/games.py
@@ -52,7 +52,7 @@ def parse_games(scoreboard_data):
|
|||||||
"Intermission": game["clock"]["inIntermission"]
|
"Intermission": game["clock"]["inIntermission"]
|
||||||
if game_state == "LIVE"
|
if game_state == "LIVE"
|
||||||
else "N/A",
|
else "N/A",
|
||||||
"Priority": calculate_game_priority(game) + get_comeback_bonus(game),
|
"Priority": calculate_game_priority(game) + get_comeback_bonus(game) + calculate_game_importance(game),
|
||||||
"Start Time": get_start_time(game),
|
"Start Time": get_start_time(game),
|
||||||
"Home Record": format_record(game["homeTeam"]["record"])
|
"Home Record": format_record(game["homeTeam"]["record"])
|
||||||
if game["gameState"] in ["PRE", "FUT"]
|
if game["gameState"] in ["PRE", "FUT"]
|
||||||
@@ -290,20 +290,94 @@ def get_team_standings(team_name):
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT league_sequence, league_l10_sequence
|
SELECT league_sequence, league_l10_sequence,
|
||||||
|
division_abbrev, conference_abbrev,
|
||||||
|
games_played, wildcard_sequence
|
||||||
FROM standings
|
FROM standings
|
||||||
WHERE team_common_name = ?
|
WHERE team_common_name = ?
|
||||||
""",
|
""",
|
||||||
(team_name,),
|
(team_name,),
|
||||||
)
|
)
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
if result:
|
||||||
|
return {
|
||||||
|
"league_sequence": result[0],
|
||||||
|
"league_l10_sequence": result[1],
|
||||||
|
"division_abbrev": result[2],
|
||||||
|
"conference_abbrev": result[3],
|
||||||
|
"games_played": result[4],
|
||||||
|
"wildcard_sequence": result[5],
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"league_sequence": result[0] if result else 0,
|
"league_sequence": 0,
|
||||||
"league_l10_sequence": result[1] if result else 0,
|
"league_l10_sequence": 0,
|
||||||
|
"division_abbrev": None,
|
||||||
|
"conference_abbrev": None,
|
||||||
|
"games_played": 0,
|
||||||
|
"wildcard_sequence": 32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_game_importance(game):
|
||||||
|
# Playoff games already have elevated priorities; don't double-count
|
||||||
|
if game.get("gameType", 2) != 2:
|
||||||
|
return 0
|
||||||
|
# FINAL/OFF games must sort below LIVE and PRE games
|
||||||
|
if game["gameState"] in ("FINAL", "OFF"):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
home_st = get_team_standings(game["homeTeam"]["name"]["default"])
|
||||||
|
away_st = get_team_standings(game["awayTeam"]["name"]["default"])
|
||||||
|
|
||||||
|
# Season weight — near-zero before game 30, sharp ramp 55-70, max at 82
|
||||||
|
avg_gp = (home_st["games_played"] + away_st["games_played"]) / 2
|
||||||
|
if avg_gp <= 30:
|
||||||
|
season_weight = 0.05
|
||||||
|
else:
|
||||||
|
t = (avg_gp - 30) / (82 - 30)
|
||||||
|
season_weight = min(t ** 1.8, 1.0)
|
||||||
|
|
||||||
|
# Playoff relevance — peaks for bubble teams (wildcard rank ~17-19)
|
||||||
|
best_wc = min(home_st["wildcard_sequence"] or 32, away_st["wildcard_sequence"] or 32)
|
||||||
|
if best_wc <= 12:
|
||||||
|
playoff_relevance = 0.60
|
||||||
|
elif best_wc <= 16:
|
||||||
|
playoff_relevance = 0.85
|
||||||
|
elif best_wc <= 19:
|
||||||
|
playoff_relevance = 1.00
|
||||||
|
elif best_wc <= 23:
|
||||||
|
playoff_relevance = 0.65
|
||||||
|
else:
|
||||||
|
playoff_relevance = 0.15
|
||||||
|
|
||||||
|
# Division/conference rivalry multiplier
|
||||||
|
home_div = home_st["division_abbrev"]
|
||||||
|
away_div = away_st["division_abbrev"]
|
||||||
|
home_conf = home_st["conference_abbrev"]
|
||||||
|
away_conf = away_st["conference_abbrev"]
|
||||||
|
if home_div and away_div and home_div == away_div:
|
||||||
|
rivalry_multiplier = 1.4
|
||||||
|
elif home_conf and away_conf and home_conf == away_conf:
|
||||||
|
rivalry_multiplier = 1.2
|
||||||
|
else:
|
||||||
|
rivalry_multiplier = 1.0
|
||||||
|
|
||||||
|
raw = season_weight * playoff_relevance * rivalry_multiplier
|
||||||
|
importance = int((raw / 1.4) * 150)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"importance components — season_weight: %.3f, playoff_relevance: %.2f, "
|
||||||
|
"rivalry: %.1f, importance: %s",
|
||||||
|
season_weight,
|
||||||
|
playoff_relevance,
|
||||||
|
rivalry_multiplier,
|
||||||
|
importance,
|
||||||
|
)
|
||||||
|
|
||||||
|
return max(0, min(importance, 150))
|
||||||
|
|
||||||
|
|
||||||
def utc_to_eastern(utc_time):
|
def utc_to_eastern(utc_time):
|
||||||
utc_datetime = datetime.strptime(utc_time, "%Y-%m-%dT%H:%M:%SZ")
|
utc_datetime = datetime.strptime(utc_time, "%Y-%m-%dT%H:%M:%SZ")
|
||||||
eastern_datetime = utc_datetime.replace(tzinfo=timezone.utc).astimezone(EASTERN)
|
eastern_datetime = utc_datetime.replace(tzinfo=timezone.utc).astimezone(EASTERN)
|
||||||
|
|||||||
@@ -14,12 +14,31 @@ def create_standings_table(conn):
|
|||||||
CREATE TABLE IF NOT EXISTS standings (
|
CREATE TABLE IF NOT EXISTS standings (
|
||||||
team_common_name TEXT,
|
team_common_name TEXT,
|
||||||
league_sequence INTEGER,
|
league_sequence INTEGER,
|
||||||
league_l10_sequence INTEGER
|
league_l10_sequence INTEGER,
|
||||||
|
division_abbrev TEXT,
|
||||||
|
conference_abbrev TEXT,
|
||||||
|
games_played INTEGER,
|
||||||
|
wildcard_sequence INTEGER
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_standings_table(conn):
|
||||||
|
cursor = conn.cursor()
|
||||||
|
for col_name, col_type in [
|
||||||
|
("division_abbrev", "TEXT"),
|
||||||
|
("conference_abbrev", "TEXT"),
|
||||||
|
("games_played", "INTEGER"),
|
||||||
|
("wildcard_sequence", "INTEGER"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"ALTER TABLE standings ADD COLUMN {col_name} {col_type}")
|
||||||
|
conn.commit()
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # Column already exists
|
||||||
|
|
||||||
|
|
||||||
def truncate_standings_table(conn):
|
def truncate_standings_table(conn):
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("DELETE FROM standings")
|
cursor.execute("DELETE FROM standings")
|
||||||
@@ -31,13 +50,20 @@ def insert_standings(conn, standings):
|
|||||||
for team in standings:
|
for team in standings:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO standings (team_common_name, league_sequence, league_l10_sequence)
|
INSERT INTO standings (
|
||||||
VALUES (?, ?, ?)
|
team_common_name, league_sequence, league_l10_sequence,
|
||||||
|
division_abbrev, conference_abbrev, games_played, wildcard_sequence
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
team["team_common_name"],
|
team["team_common_name"],
|
||||||
team["league_sequence"],
|
team["league_sequence"],
|
||||||
team["league_l10_sequence"],
|
team["league_l10_sequence"],
|
||||||
|
team["division_abbrev"],
|
||||||
|
team["conference_abbrev"],
|
||||||
|
team["games_played"],
|
||||||
|
team["wildcard_sequence"],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -56,6 +82,10 @@ def fetch_standings():
|
|||||||
"team_common_name": team["teamCommonName"]["default"],
|
"team_common_name": team["teamCommonName"]["default"],
|
||||||
"league_sequence": team["leagueSequence"],
|
"league_sequence": team["leagueSequence"],
|
||||||
"league_l10_sequence": team["leagueL10Sequence"],
|
"league_l10_sequence": team["leagueL10Sequence"],
|
||||||
|
"division_abbrev": team["divisionAbbrev"],
|
||||||
|
"conference_abbrev": team["conferenceAbbrev"],
|
||||||
|
"games_played": team["gamesPlayed"],
|
||||||
|
"wildcard_sequence": team["wildcardSequence"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return standings
|
return standings
|
||||||
@@ -67,6 +97,7 @@ def fetch_standings():
|
|||||||
def refresh_standings():
|
def refresh_standings():
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
create_standings_table(conn)
|
create_standings_table(conn)
|
||||||
|
migrate_standings_table(conn)
|
||||||
standings = fetch_standings()
|
standings = fetch_standings()
|
||||||
if standings:
|
if standings:
|
||||||
truncate_standings_table(conn)
|
truncate_standings_table(conn)
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ def flask_client(tmp_path, monkeypatch):
|
|||||||
conn = sqlite3.connect(str(db_path))
|
conn = sqlite3.connect(str(db_path))
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE standings "
|
"CREATE TABLE standings "
|
||||||
"(team_common_name TEXT, league_sequence INTEGER, league_l10_sequence INTEGER)"
|
"(team_common_name TEXT, league_sequence INTEGER, league_l10_sequence INTEGER, "
|
||||||
|
"division_abbrev TEXT, conference_abbrev TEXT, games_played INTEGER, wildcard_sequence INTEGER)"
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import app.games
|
import app.games
|
||||||
from tests.conftest import make_game
|
from tests.conftest import make_game
|
||||||
from app.games import (
|
from app.games import (
|
||||||
|
calculate_game_importance,
|
||||||
calculate_game_priority,
|
calculate_game_priority,
|
||||||
convert_game_state,
|
convert_game_state,
|
||||||
format_record,
|
format_record,
|
||||||
@@ -413,3 +414,123 @@ class TestGetComebackBonus:
|
|||||||
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3)
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3)
|
||||||
game = make_game(home_score=2, away_score=3, period=4)
|
game = make_game(home_score=2, away_score=3, period=4)
|
||||||
assert get_comeback_bonus(game) == 100
|
assert get_comeback_bonus(game) == 100
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculateGameImportance:
|
||||||
|
def _standings(
|
||||||
|
self,
|
||||||
|
league_seq=10,
|
||||||
|
l10_seq=10,
|
||||||
|
div="ATL",
|
||||||
|
conf="E",
|
||||||
|
gp=65,
|
||||||
|
wc=18,
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"league_sequence": league_seq,
|
||||||
|
"league_l10_sequence": l10_seq,
|
||||||
|
"division_abbrev": div,
|
||||||
|
"conference_abbrev": conf,
|
||||||
|
"games_played": gp,
|
||||||
|
"wildcard_sequence": wc,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_returns_zero_for_playoff_game(self):
|
||||||
|
game = make_game(game_type=3)
|
||||||
|
assert calculate_game_importance(game) == 0
|
||||||
|
|
||||||
|
def test_returns_zero_for_final_game(self):
|
||||||
|
game = make_game(game_state="OFF")
|
||||||
|
assert calculate_game_importance(game) == 0
|
||||||
|
|
||||||
|
def test_near_zero_early_in_season(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value=self._standings(gp=10, wc=18),
|
||||||
|
)
|
||||||
|
game = make_game(game_state="FUT")
|
||||||
|
assert calculate_game_importance(game) <= 10
|
||||||
|
|
||||||
|
def test_max_bonus_late_season_bubble_division_game(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value=self._standings(gp=82, wc=18, div="ATL", conf="E"),
|
||||||
|
)
|
||||||
|
game = make_game(game_state="FUT")
|
||||||
|
assert calculate_game_importance(game) == 150
|
||||||
|
|
||||||
|
def test_same_division_beats_same_conference(self, mocker):
|
||||||
|
home_st = self._standings(gp=70, wc=18, div="ATL", conf="E")
|
||||||
|
away_st_same_div = self._standings(gp=70, wc=18, div="ATL", conf="E")
|
||||||
|
away_st_diff_div = self._standings(gp=70, wc=18, div="MET", conf="E")
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
side_effect=[home_st, away_st_same_div],
|
||||||
|
)
|
||||||
|
result_div = calculate_game_importance(make_game(game_state="FUT"))
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
side_effect=[home_st, away_st_diff_div],
|
||||||
|
)
|
||||||
|
result_conf = calculate_game_importance(make_game(game_state="FUT"))
|
||||||
|
|
||||||
|
assert result_div > result_conf
|
||||||
|
|
||||||
|
def test_same_conference_beats_different_conference(self, mocker):
|
||||||
|
home_st = self._standings(gp=70, wc=18, div="ATL", conf="E")
|
||||||
|
away_same_conf = self._standings(gp=70, wc=18, div="MET", conf="E")
|
||||||
|
away_diff_conf = self._standings(gp=70, wc=18, div="PAC", conf="W")
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
side_effect=[home_st, away_same_conf],
|
||||||
|
)
|
||||||
|
result_same = calculate_game_importance(make_game(game_state="FUT"))
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
side_effect=[home_st, away_diff_conf],
|
||||||
|
)
|
||||||
|
result_diff = calculate_game_importance(make_game(game_state="FUT"))
|
||||||
|
|
||||||
|
assert result_same > result_diff
|
||||||
|
|
||||||
|
def test_bubble_teams_beat_safely_in_teams(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value=self._standings(gp=70, wc=18),
|
||||||
|
)
|
||||||
|
result_bubble = calculate_game_importance(make_game(game_state="FUT"))
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value=self._standings(gp=70, wc=5),
|
||||||
|
)
|
||||||
|
result_safe = calculate_game_importance(make_game(game_state="FUT"))
|
||||||
|
|
||||||
|
assert result_bubble > result_safe
|
||||||
|
|
||||||
|
def test_eliminated_teams_have_lowest_relevance(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value=self._standings(gp=70, wc=30),
|
||||||
|
)
|
||||||
|
assert calculate_game_importance(make_game(game_state="FUT")) < 30
|
||||||
|
|
||||||
|
def test_result_is_non_negative_int(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value=self._standings(gp=0, wc=32),
|
||||||
|
)
|
||||||
|
result = calculate_game_importance(make_game(game_state="FUT"))
|
||||||
|
assert isinstance(result, int)
|
||||||
|
assert result >= 0
|
||||||
|
|
||||||
|
def test_result_never_exceeds_150(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value=self._standings(gp=82, wc=18, div="ATL", conf="E"),
|
||||||
|
)
|
||||||
|
assert calculate_game_importance(make_game(game_state="FUT")) <= 150
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from app.standings import (
|
|||||||
create_standings_table,
|
create_standings_table,
|
||||||
fetch_standings,
|
fetch_standings,
|
||||||
insert_standings,
|
insert_standings,
|
||||||
|
migrate_standings_table,
|
||||||
refresh_standings,
|
refresh_standings,
|
||||||
truncate_standings_table,
|
truncate_standings_table,
|
||||||
)
|
)
|
||||||
@@ -16,11 +17,19 @@ SAMPLE_API_RESPONSE = {
|
|||||||
"teamCommonName": {"default": "Bruins"},
|
"teamCommonName": {"default": "Bruins"},
|
||||||
"leagueSequence": 1,
|
"leagueSequence": 1,
|
||||||
"leagueL10Sequence": 2,
|
"leagueL10Sequence": 2,
|
||||||
|
"divisionAbbrev": "ATL",
|
||||||
|
"conferenceAbbrev": "E",
|
||||||
|
"gamesPlayed": 60,
|
||||||
|
"wildcardSequence": 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"teamCommonName": {"default": "Maple Leafs"},
|
"teamCommonName": {"default": "Maple Leafs"},
|
||||||
"leagueSequence": 5,
|
"leagueSequence": 5,
|
||||||
"leagueL10Sequence": 3,
|
"leagueL10Sequence": 3,
|
||||||
|
"divisionAbbrev": "ATL",
|
||||||
|
"conferenceAbbrev": "E",
|
||||||
|
"gamesPlayed": 61,
|
||||||
|
"wildcardSequence": 8,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -38,6 +47,10 @@ class TestFetchStandings:
|
|||||||
"team_common_name": "Bruins",
|
"team_common_name": "Bruins",
|
||||||
"league_sequence": 1,
|
"league_sequence": 1,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 60,
|
||||||
|
"wildcard_sequence": 5,
|
||||||
}
|
}
|
||||||
assert result[1]["team_common_name"] == "Maple Leafs"
|
assert result[1]["team_common_name"] == "Maple Leafs"
|
||||||
|
|
||||||
@@ -98,6 +111,10 @@ class TestTruncateStandingsTable:
|
|||||||
"team_common_name": "Bruins",
|
"team_common_name": "Bruins",
|
||||||
"league_sequence": 1,
|
"league_sequence": 1,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 60,
|
||||||
|
"wildcard_sequence": 5,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -119,11 +136,19 @@ class TestInsertStandings:
|
|||||||
"team_common_name": "Bruins",
|
"team_common_name": "Bruins",
|
||||||
"league_sequence": 1,
|
"league_sequence": 1,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 60,
|
||||||
|
"wildcard_sequence": 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"team_common_name": "Maple Leafs",
|
"team_common_name": "Maple Leafs",
|
||||||
"league_sequence": 5,
|
"league_sequence": 5,
|
||||||
"league_l10_sequence": 3,
|
"league_l10_sequence": 3,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 61,
|
||||||
|
"wildcard_sequence": 8,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
insert_standings(conn, data)
|
insert_standings(conn, data)
|
||||||
@@ -142,6 +167,10 @@ class TestInsertStandings:
|
|||||||
"team_common_name": "Bruins",
|
"team_common_name": "Bruins",
|
||||||
"league_sequence": 1,
|
"league_sequence": 1,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 60,
|
||||||
|
"wildcard_sequence": 5,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -162,6 +191,10 @@ class TestRefreshStandings:
|
|||||||
"team_common_name": "Bruins",
|
"team_common_name": "Bruins",
|
||||||
"league_sequence": 1,
|
"league_sequence": 1,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 60,
|
||||||
|
"wildcard_sequence": 5,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
mocker.patch("app.standings.fetch_standings", return_value=standings)
|
mocker.patch("app.standings.fetch_standings", return_value=standings)
|
||||||
@@ -183,6 +216,10 @@ class TestRefreshStandings:
|
|||||||
"team_common_name": "Bruins",
|
"team_common_name": "Bruins",
|
||||||
"league_sequence": 1,
|
"league_sequence": 1,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 60,
|
||||||
|
"wildcard_sequence": 5,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
mocker.patch("app.standings.fetch_standings", return_value=first)
|
mocker.patch("app.standings.fetch_standings", return_value=first)
|
||||||
@@ -193,11 +230,19 @@ class TestRefreshStandings:
|
|||||||
"team_common_name": "Oilers",
|
"team_common_name": "Oilers",
|
||||||
"league_sequence": 3,
|
"league_sequence": 3,
|
||||||
"league_l10_sequence": 1,
|
"league_l10_sequence": 1,
|
||||||
|
"division_abbrev": "PAC",
|
||||||
|
"conference_abbrev": "W",
|
||||||
|
"games_played": 62,
|
||||||
|
"wildcard_sequence": 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"team_common_name": "Jets",
|
"team_common_name": "Jets",
|
||||||
"league_sequence": 4,
|
"league_sequence": 4,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "CEN",
|
||||||
|
"conference_abbrev": "W",
|
||||||
|
"games_played": 61,
|
||||||
|
"wildcard_sequence": 4,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
mocker.patch("app.standings.fetch_standings", return_value=second)
|
mocker.patch("app.standings.fetch_standings", return_value=second)
|
||||||
@@ -218,6 +263,10 @@ class TestRefreshStandings:
|
|||||||
"team_common_name": "Bruins",
|
"team_common_name": "Bruins",
|
||||||
"league_sequence": 1,
|
"league_sequence": 1,
|
||||||
"league_l10_sequence": 2,
|
"league_l10_sequence": 2,
|
||||||
|
"division_abbrev": "ATL",
|
||||||
|
"conference_abbrev": "E",
|
||||||
|
"games_played": 60,
|
||||||
|
"wildcard_sequence": 5,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
mocker.patch("app.standings.fetch_standings", return_value=seed)
|
mocker.patch("app.standings.fetch_standings", return_value=seed)
|
||||||
@@ -231,3 +280,29 @@ class TestRefreshStandings:
|
|||||||
count = conn.execute("SELECT COUNT(*) FROM standings").fetchone()[0]
|
count = conn.execute("SELECT COUNT(*) FROM standings").fetchone()[0]
|
||||||
conn.close()
|
conn.close()
|
||||||
assert count == 1
|
assert count == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestMigrateStandingsTable:
|
||||||
|
def test_adds_missing_columns_to_existing_table(self, tmp_path):
|
||||||
|
conn = sqlite3.connect(str(tmp_path / "test.db"))
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE standings "
|
||||||
|
"(team_common_name TEXT, league_sequence INTEGER, league_l10_sequence INTEGER)"
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
migrate_standings_table(conn)
|
||||||
|
|
||||||
|
cols = [row[1] for row in conn.execute("PRAGMA table_info(standings)").fetchall()]
|
||||||
|
assert "division_abbrev" in cols
|
||||||
|
assert "conference_abbrev" in cols
|
||||||
|
assert "games_played" in cols
|
||||||
|
assert "wildcard_sequence" in cols
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_is_idempotent(self, tmp_path):
|
||||||
|
conn = sqlite3.connect(str(tmp_path / "test.db"))
|
||||||
|
create_standings_table(conn)
|
||||||
|
migrate_standings_table(conn)
|
||||||
|
migrate_standings_table(conn) # must not raise
|
||||||
|
conn.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user