hockey-blast-common-lib 0.1.61__py3-none-any.whl → 0.1.63__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,13 +9,19 @@ import sqlalchemy
9
9
  from hockey_blast_common_lib.models import Game, Organization, Division, Human, GoalieSaves
10
10
  from hockey_blast_common_lib.stats_models import OrgStatsGoalie, DivisionStatsGoalie, OrgStatsWeeklyGoalie, OrgStatsDailyGoalie, DivisionStatsWeeklyGoalie, DivisionStatsDailyGoalie, LevelStatsGoalie
11
11
  from hockey_blast_common_lib.db_connection import create_session
12
- from sqlalchemy.sql import func
12
+ from sqlalchemy.sql import func, case
13
13
  from hockey_blast_common_lib.options import MIN_GAMES_FOR_ORG_STATS, MIN_GAMES_FOR_DIVISION_STATS, MIN_GAMES_FOR_LEVEL_STATS
14
14
  from hockey_blast_common_lib.utils import get_non_human_ids, get_all_division_ids_for_org, get_start_datetime
15
15
  from hockey_blast_common_lib.utils import assign_ranks
16
16
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
17
17
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
18
18
 
19
+ # Import status constants for game filtering
20
+ FINAL_STATUS = "Final"
21
+ FINAL_SO_STATUS = "Final(SO)"
22
+ FORFEIT_STATUS = "FORFEIT"
23
+ NOEVENTS_STATUS = "NOEVENTS"
24
+
19
25
  def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_human_id=None, aggregation_window=None):
20
26
  human_ids_to_filter = get_non_human_ids(session)
21
27
 
@@ -72,13 +78,19 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
72
78
 
73
79
 
74
80
  # Aggregate games played, goals allowed, and shots faced for each goalie using GoalieSaves table
81
+ # Filter games by status upfront for performance (avoid CASE statements)
82
+ # Only count games with these statuses: FINAL, FINAL_SO, FORFEIT, NOEVENTS
75
83
  query = session.query(
76
84
  GoalieSaves.goalie_id.label('human_id'),
77
85
  func.count(GoalieSaves.game_id).label('games_played'),
86
+ func.count(GoalieSaves.game_id).label('games_participated'), # Same as games_played after filtering
87
+ func.count(GoalieSaves.game_id).label('games_with_stats'), # Same as games_played after filtering
78
88
  func.sum(GoalieSaves.goals_allowed).label('goals_allowed'),
79
89
  func.sum(GoalieSaves.shots_against).label('shots_faced'),
80
90
  func.array_agg(GoalieSaves.game_id).label('game_ids')
81
- ).join(Game, GoalieSaves.game_id == Game.id).join(Division, Game.division_id == Division.id).filter(filter_condition)
91
+ ).join(Game, GoalieSaves.game_id == Game.id).filter(
92
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS])
93
+ ).join(Division, Game.division_id == Division.id).filter(filter_condition)
82
94
 
83
95
  # Filter for specific human_id if provided
84
96
  if debug_human_id:
@@ -94,7 +106,9 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
94
106
  key = (aggregation_id, stat.human_id)
95
107
  if key not in stats_dict:
96
108
  stats_dict[key] = {
97
- 'games_played': 0,
109
+ 'games_played': 0, # DEPRECATED - for backward compatibility
110
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
111
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
98
112
  'goals_allowed': 0,
99
113
  'shots_faced': 0,
100
114
  'goals_allowed_per_game': 0.0,
@@ -104,6 +118,8 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
104
118
  'last_game_id': None
105
119
  }
106
120
  stats_dict[key]['games_played'] += stat.games_played
121
+ stats_dict[key]['games_participated'] += stat.games_participated
122
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
107
123
  stats_dict[key]['goals_allowed'] += stat.goals_allowed if stat.goals_allowed is not None else 0
108
124
  stats_dict[key]['shots_faced'] += stat.shots_faced if stat.shots_faced is not None else 0
109
125
  stats_dict[key]['game_ids'].extend(stat.game_ids)
@@ -111,10 +127,10 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
111
127
  # Filter out entries with games_played less than min_games
112
128
  stats_dict = {key: value for key, value in stats_dict.items() if value['games_played'] >= min_games}
113
129
 
114
- # Calculate per game stats
130
+ # Calculate per game stats (using games_with_stats as denominator for accuracy)
115
131
  for key, stat in stats_dict.items():
116
- if stat['games_played'] > 0:
117
- stat['goals_allowed_per_game'] = stat['goals_allowed'] / stat['games_played']
132
+ if stat['games_with_stats'] > 0:
133
+ stat['goals_allowed_per_game'] = stat['goals_allowed'] / stat['games_with_stats']
118
134
  stat['save_percentage'] = (stat['shots_faced'] - stat['goals_allowed']) / stat['shots_faced'] if stat['shots_faced'] > 0 else 0.0
119
135
 
120
136
  # Ensure all keys have valid human_id values
@@ -134,6 +150,8 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
134
150
 
135
151
  # Assign ranks within each level
136
152
  assign_ranks(stats_dict, 'games_played')
153
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
154
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
137
155
  assign_ranks(stats_dict, 'goals_allowed', reverse_rank=True)
138
156
  assign_ranks(stats_dict, 'shots_faced')
139
157
  assign_ranks(stats_dict, 'goals_allowed_per_game', reverse_rank=True)
@@ -159,7 +177,11 @@ def aggregate_goalie_stats(session, aggregation_type, aggregation_id, debug_huma
159
177
  goalie_stat = StatsModel(
160
178
  aggregation_id=aggregation_id,
161
179
  human_id=human_id,
162
- games_played=stat['games_played'],
180
+ games_played=stat['games_played'], # DEPRECATED - for backward compatibility
181
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
182
+ games_participated_rank=stat['games_participated_rank'],
183
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
184
+ games_with_stats_rank=stat['games_with_stats_rank'],
163
185
  goals_allowed=stat['goals_allowed'],
164
186
  shots_faced=stat['shots_faced'],
165
187
  goals_allowed_per_game=goals_allowed_per_game,
@@ -61,7 +61,7 @@ def aggregate_human_stats(session, aggregation_type, aggregation_id, human_id_fi
61
61
 
62
62
  # Apply aggregation window filter
63
63
  if aggregation_window:
64
- last_game_datetime_str = session.query(func.max(func.concat(Game.date, ' ', Game.time))).filter(filter_condition, Game.status.like('Final%')).scalar()
64
+ last_game_datetime_str = session.query(func.max(func.concat(Game.date, ' ', Game.time))).filter(filter_condition, (Game.status.like('Final%')) | (Game.status == 'NOEVENTS')).scalar()
65
65
  start_datetime = get_start_datetime(last_game_datetime_str, aggregation_window)
66
66
  if start_datetime:
67
67
  game_window_filter = func.cast(func.concat(Game.date, ' ', Game.time), sqlalchemy.types.TIMESTAMP).between(start_datetime, last_game_datetime_str)
@@ -75,8 +75,8 @@ def aggregate_human_stats(session, aggregation_type, aggregation_id, human_id_fi
75
75
  if human_id_filter:
76
76
  human_filter = [GameRoster.human_id == human_id_filter]
77
77
 
78
- # Filter games by status
79
- game_status_filter = Game.status.like('Final%')
78
+ # Filter games by status - include both Final and NOEVENTS games
79
+ game_status_filter = (Game.status.like('Final%')) | (Game.status == 'NOEVENTS')
80
80
 
81
81
  # Aggregate skater games played
82
82
  skater_stats = session.query(
@@ -17,6 +17,12 @@ from hockey_blast_common_lib.utils import get_start_datetime
17
17
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
18
18
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
19
19
 
20
+ # Import status constants for game filtering
21
+ FINAL_STATUS = "Final"
22
+ FINAL_SO_STATUS = "Final(SO)"
23
+ FORFEIT_STATUS = "FORFEIT"
24
+ NOEVENTS_STATUS = "NOEVENTS"
25
+
20
26
  def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregation_window=None):
21
27
  human_ids_to_filter = get_non_human_ids(session)
22
28
 
@@ -72,17 +78,26 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
72
78
 
73
79
  filter_condition = filter_condition & (Division.id == Game.division_id)
74
80
  # Aggregate games reffed for each referee
81
+ # games_participated: Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
82
+ # games_with_stats: Count only FINAL, FINAL_SO (for per-game averages)
83
+ # Filter by game status upfront for performance
84
+ status_filter = Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS])
85
+
75
86
  games_reffed_stats = session.query(
76
87
  Game.referee_1_id.label('human_id'),
77
88
  func.count(Game.id).label('games_reffed'),
89
+ func.count(Game.id).label('games_participated'), # Same as games_reffed after filtering
90
+ func.count(Game.id).label('games_with_stats'), # Same as games_reffed after filtering
78
91
  func.array_agg(Game.id).label('game_ids')
79
- ).filter(filter_condition).group_by(Game.referee_1_id).all()
92
+ ).filter(filter_condition, status_filter).group_by(Game.referee_1_id).all()
80
93
 
81
94
  games_reffed_stats_2 = session.query(
82
95
  Game.referee_2_id.label('human_id'),
83
96
  func.count(Game.id).label('games_reffed'),
97
+ func.count(Game.id).label('games_participated'), # Same as games_reffed after filtering
98
+ func.count(Game.id).label('games_with_stats'), # Same as games_reffed after filtering
84
99
  func.array_agg(Game.id).label('game_ids')
85
- ).filter(filter_condition).group_by(Game.referee_2_id).all()
100
+ ).filter(filter_condition, status_filter).group_by(Game.referee_2_id).all()
86
101
 
87
102
  # Aggregate penalties given for each referee
88
103
  penalties_given_stats = session.query(
@@ -101,7 +116,9 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
101
116
  key = (aggregation_id, stat.human_id)
102
117
  if key not in stats_dict:
103
118
  stats_dict[key] = {
104
- 'games_reffed': 0,
119
+ 'games_reffed': 0, # DEPRECATED - for backward compatibility
120
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
121
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
105
122
  'penalties_given': 0,
106
123
  'gm_given': 0,
107
124
  'penalties_per_game': 0.0,
@@ -111,6 +128,8 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
111
128
  'last_game_id': None
112
129
  }
113
130
  stats_dict[key]['games_reffed'] += stat.games_reffed
131
+ stats_dict[key]['games_participated'] += stat.games_participated
132
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
114
133
  stats_dict[key]['game_ids'].extend(stat.game_ids)
115
134
 
116
135
  for stat in games_reffed_stats_2:
@@ -119,7 +138,9 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
119
138
  key = (aggregation_id, stat.human_id)
120
139
  if key not in stats_dict:
121
140
  stats_dict[key] = {
122
- 'games_reffed': 0,
141
+ 'games_reffed': 0, # DEPRECATED - for backward compatibility
142
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
143
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
123
144
  'penalties_given': 0,
124
145
  'gm_given': 0,
125
146
  'penalties_per_game': 0.0,
@@ -129,6 +150,8 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
129
150
  'last_game_id': None
130
151
  }
131
152
  stats_dict[key]['games_reffed'] += stat.games_reffed
153
+ stats_dict[key]['games_participated'] += stat.games_participated
154
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
132
155
  stats_dict[key]['game_ids'].extend(stat.game_ids)
133
156
 
134
157
  # Filter out entries with games_reffed less than min_games
@@ -149,11 +172,11 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
149
172
  stats_dict[key]['gm_given'] += stat.gm_given / 2
150
173
  stats_dict[key]['game_ids'].append(stat.game_id)
151
174
 
152
- # Calculate per game stats
175
+ # Calculate per game stats (using games_with_stats as denominator for accuracy)
153
176
  for key, stat in stats_dict.items():
154
- if stat['games_reffed'] > 0:
155
- stat['penalties_per_game'] = stat['penalties_given'] / stat['games_reffed']
156
- stat['gm_per_game'] = stat['gm_given'] / stat['games_reffed']
177
+ if stat['games_with_stats'] > 0:
178
+ stat['penalties_per_game'] = stat['penalties_given'] / stat['games_with_stats']
179
+ stat['gm_per_game'] = stat['gm_given'] / stat['games_with_stats']
157
180
 
158
181
  # Ensure all keys have valid human_id values
159
182
  stats_dict = {key: value for key, value in stats_dict.items() if key[1] is not None}
@@ -172,6 +195,8 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
172
195
 
173
196
  # Assign ranks
174
197
  assign_ranks(stats_dict, 'games_reffed')
198
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
199
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
175
200
  assign_ranks(stats_dict, 'penalties_given')
176
201
  assign_ranks(stats_dict, 'penalties_per_game')
177
202
  assign_ranks(stats_dict, 'gm_given')
@@ -185,7 +210,11 @@ def aggregate_referee_stats(session, aggregation_type, aggregation_id, aggregati
185
210
  referee_stat = StatsModel(
186
211
  aggregation_id=aggregation_id,
187
212
  human_id=human_id,
188
- games_reffed=stat['games_reffed'],
213
+ games_reffed=stat['games_reffed'], # DEPRECATED - for backward compatibility
214
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
215
+ games_participated_rank=stat['games_participated_rank'],
216
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
217
+ games_with_stats_rank=stat['games_with_stats_rank'],
189
218
  penalties_given=stat['penalties_given'],
190
219
  penalties_per_game=stat['penalties_per_game'],
191
220
  gm_given=stat['gm_given'],
@@ -17,6 +17,12 @@ from hockey_blast_common_lib.utils import get_start_datetime
17
17
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
18
18
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
19
19
 
20
+ # Import status constants for game filtering
21
+ FINAL_STATUS = "Final"
22
+ FINAL_SO_STATUS = "Final(SO)"
23
+ FORFEIT_STATUS = "FORFEIT"
24
+ NOEVENTS_STATUS = "NOEVENTS"
25
+
20
26
  def calculate_quality_score(avg_max_saves_5sec, avg_max_saves_20sec, peak_max_saves_5sec, peak_max_saves_20sec):
21
27
  """
22
28
  Calculate a quality score based on excessive clicking patterns.
@@ -89,9 +95,14 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
89
95
 
90
96
 
91
97
  # Aggregate scorekeeper quality data for each human
98
+ # games_participated: Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
99
+ # games_with_stats: Count only FINAL, FINAL_SO (for per-game averages)
100
+ # Filter by game status upfront for performance
92
101
  scorekeeper_quality_stats = session.query(
93
102
  ScorekeeperSaveQuality.scorekeeper_id.label('human_id'),
94
103
  func.count(ScorekeeperSaveQuality.game_id).label('games_recorded'),
104
+ func.count(ScorekeeperSaveQuality.game_id).label('games_participated'), # Same as games_recorded after filtering
105
+ func.count(ScorekeeperSaveQuality.game_id).label('games_with_stats'), # Same as games_recorded after filtering
95
106
  func.sum(ScorekeeperSaveQuality.total_saves_recorded).label('total_saves_recorded'),
96
107
  func.avg(ScorekeeperSaveQuality.total_saves_recorded).label('avg_saves_per_game'),
97
108
  func.avg(ScorekeeperSaveQuality.max_saves_per_5sec).label('avg_max_saves_per_5sec'),
@@ -99,7 +110,9 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
99
110
  func.max(ScorekeeperSaveQuality.max_saves_per_5sec).label('peak_max_saves_per_5sec'),
100
111
  func.max(ScorekeeperSaveQuality.max_saves_per_20sec).label('peak_max_saves_per_20sec'),
101
112
  func.array_agg(ScorekeeperSaveQuality.game_id).label('game_ids')
102
- ).join(Game, Game.id == ScorekeeperSaveQuality.game_id)
113
+ ).join(Game, Game.id == ScorekeeperSaveQuality.game_id).filter(
114
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS])
115
+ )
103
116
 
104
117
 
105
118
  scorekeeper_quality_stats = scorekeeper_quality_stats.filter(filter_condition).group_by(ScorekeeperSaveQuality.scorekeeper_id).all()
@@ -120,7 +133,9 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
120
133
  )
121
134
 
122
135
  stats_dict[key] = {
123
- 'games_recorded': stat.games_recorded,
136
+ 'games_recorded': stat.games_recorded, # DEPRECATED - for backward compatibility
137
+ 'games_participated': stat.games_participated, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
138
+ 'games_with_stats': stat.games_with_stats, # Games with full stats: FINAL, FINAL_SO only
124
139
  'sog_given': stat.total_saves_recorded, # Legacy field name mapping
125
140
  'sog_per_game': stat.avg_saves_per_game or 0.0, # Legacy field name mapping
126
141
  'total_saves_recorded': stat.total_saves_recorded,
@@ -152,6 +167,8 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
152
167
 
153
168
  # Assign ranks - note: for quality metrics, lower values are better (reverse_rank=True for avg and peak clicking)
154
169
  assign_ranks(stats_dict, 'games_recorded')
170
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
171
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
155
172
  assign_ranks(stats_dict, 'sog_given') # Legacy field
156
173
  assign_ranks(stats_dict, 'sog_per_game') # Legacy field
157
174
  assign_ranks(stats_dict, 'total_saves_recorded')
@@ -169,7 +186,11 @@ def aggregate_scorekeeper_stats(session, aggregation_type, aggregation_id, aggre
169
186
  scorekeeper_stat = StatsModel(
170
187
  aggregation_id=aggregation_id,
171
188
  human_id=human_id,
172
- games_recorded=stat['games_recorded'],
189
+ games_recorded=stat['games_recorded'], # DEPRECATED - for backward compatibility
190
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
191
+ games_participated_rank=stat['games_participated_rank'],
192
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
193
+ games_with_stats_rank=stat['games_with_stats_rank'],
173
194
  sog_given=stat['sog_given'], # Legacy field mapping
174
195
  sog_per_game=stat['sog_per_game'], # Legacy field mapping
175
196
  total_saves_recorded=stat['total_saves_recorded'],
@@ -18,50 +18,65 @@ from collections import defaultdict
18
18
  from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
19
19
  from hockey_blast_common_lib.progress_utils import create_progress_tracker
20
20
 
21
+ # Import status constants for game filtering
22
+ FINAL_STATUS = "Final"
23
+ FINAL_SO_STATUS = "Final(SO)"
24
+ FORFEIT_STATUS = "FORFEIT"
25
+ NOEVENTS_STATUS = "NOEVENTS"
26
+
21
27
  def calculate_current_point_streak(session, human_id, filter_condition):
22
28
  """
23
29
  Calculate the current point streak for a player.
24
30
  A point streak is consecutive games (from the most recent game backward) where the player had at least one point.
25
31
  Returns a tuple: (streak_length, average_points_during_streak)
32
+
33
+ Optimized to use CASE statements for conditional aggregation in a single query.
26
34
  """
27
- # Get all games for this player ordered by date/time descending (most recent first)
28
- games = session.query(Game).join(GameRoster, Game.id == GameRoster.game_id).filter(
35
+ # Get all games with their point totals in ONE query using CASE for conditional counting
36
+ game_points = session.query(
37
+ Game.id,
38
+ Game.date,
39
+ Game.time,
40
+ func.sum(case((Goal.goal_scorer_id == human_id, 1), else_=0)).label('goals'),
41
+ func.sum(case(
42
+ ((Goal.assist_1_id == human_id) | (Goal.assist_2_id == human_id), 1),
43
+ else_=0
44
+ )).label('assists')
45
+ ).join(
46
+ GameRoster, Game.id == GameRoster.game_id
47
+ ).outerjoin(
48
+ Goal, Game.id == Goal.game_id
49
+ ).filter(
29
50
  GameRoster.human_id == human_id,
30
51
  ~GameRoster.role.ilike('g'), # Exclude goalie games
31
52
  filter_condition,
32
- Game.status.like('Final%') # Only final games
33
- ).order_by(Game.date.desc(), Game.time.desc()).all()
34
-
35
- if not games:
53
+ (Game.status.like('Final%')) | (Game.status == 'NOEVENTS') # Include Final and NOEVENTS games
54
+ ).group_by(
55
+ Game.id, Game.date, Game.time
56
+ ).order_by(
57
+ Game.date.desc(), Game.time.desc()
58
+ ).all()
59
+
60
+ if not game_points:
36
61
  return 0, 0.0
37
-
62
+
38
63
  current_streak = 0
39
64
  total_points_in_streak = 0
40
-
41
- for game in games:
42
- # Check if the player had any points in this game
43
- goals = session.query(Goal).filter(
44
- Goal.game_id == game.id,
45
- Goal.goal_scorer_id == human_id
46
- ).count()
47
-
48
- assists = session.query(Goal).filter(
49
- Goal.game_id == game.id,
50
- ((Goal.assist_1_id == human_id) | (Goal.assist_2_id == human_id))
51
- ).count()
52
-
53
- total_points = goals + assists
54
-
65
+
66
+ # Iterate through games from most recent to oldest
67
+ for game in game_points:
68
+ total_points = (game.goals or 0) + (game.assists or 0)
69
+
55
70
  if total_points > 0:
56
71
  current_streak += 1
57
72
  total_points_in_streak += total_points
58
73
  else:
59
74
  # Streak is broken, stop counting
60
75
  break
61
-
76
+
62
77
  # Calculate average points during streak
63
78
  avg_points_during_streak = total_points_in_streak / current_streak if current_streak > 0 else 0.0
64
-
79
+
65
80
  return current_streak, avg_points_during_streak
66
81
 
67
82
  def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_human_id=None, aggregation_window=None):
@@ -123,7 +138,7 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
123
138
 
124
139
  # Apply aggregation window filter
125
140
  if aggregation_window:
126
- last_game_datetime_str = session.query(func.max(func.concat(Game.date, ' ', Game.time))).filter(filter_condition, Game.status.like('Final%')).scalar()
141
+ last_game_datetime_str = session.query(func.max(func.concat(Game.date, ' ', Game.time))).filter(filter_condition, (Game.status.like('Final%')) | (Game.status == 'NOEVENTS')).scalar()
127
142
  start_datetime = get_start_datetime(last_game_datetime_str, aggregation_window)
128
143
  if start_datetime:
129
144
  game_window_filter = func.cast(func.concat(Game.date, ' ', Game.time), sqlalchemy.types.TIMESTAMP).between(start_datetime, last_game_datetime_str)
@@ -138,16 +153,22 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
138
153
  # human_filter = [GameRoster.human_id == debug_human_id]
139
154
 
140
155
  # Aggregate games played for each human in each division, excluding goalies
156
+ # Filter games by status upfront for performance (avoid CASE statements on 200K+ rows)
157
+ # Only count games with these statuses: FINAL, FINAL_SO, FORFEIT, NOEVENTS
141
158
  games_played_query = session.query(
142
159
  GameRoster.human_id,
143
160
  func.count(Game.id).label('games_played'),
161
+ func.count(Game.id).label('games_participated'), # Same as games_played after filtering
162
+ func.count(Game.id).label('games_with_stats'), # Same as games_played after filtering
144
163
  func.array_agg(Game.id).label('game_ids')
145
- ).join(Game, Game.id == GameRoster.game_id)
146
-
164
+ ).join(Game, Game.id == GameRoster.game_id).filter(
165
+ Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS])
166
+ )
167
+
147
168
  # Only join Division if not level aggregation (since we filter on Game.division_id directly for levels)
148
169
  if aggregation_type != 'level':
149
170
  games_played_query = games_played_query.join(Division, Game.division_id == Division.id)
150
-
171
+
151
172
  games_played_stats = games_played_query.filter(filter_condition, ~GameRoster.role.ilike('g'), *human_filter).group_by(GameRoster.human_id).all()
152
173
 
153
174
  # Aggregate goals for each human in each division, excluding goalies
@@ -206,7 +227,9 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
206
227
  key = (aggregation_id, stat.human_id)
207
228
  if key not in stats_dict:
208
229
  stats_dict[key] = {
209
- 'games_played': 0,
230
+ 'games_played': 0, # DEPRECATED - for backward compatibility
231
+ 'games_participated': 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
232
+ 'games_with_stats': 0, # Games with full stats: FINAL, FINAL_SO only
210
233
  'goals': 0,
211
234
  'assists': 0,
212
235
  'penalties': 0,
@@ -224,6 +247,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
224
247
  'last_game_id': None
225
248
  }
226
249
  stats_dict[key]['games_played'] += stat.games_played
250
+ stats_dict[key]['games_participated'] += stat.games_participated
251
+ stats_dict[key]['games_with_stats'] += stat.games_with_stats
227
252
  stats_dict[key]['game_ids'].extend(stat.game_ids)
228
253
 
229
254
  # Filter out entries with games_played less than min_games
@@ -253,34 +278,61 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
253
278
  stats_dict[key]['penalties'] += stat.penalties
254
279
  stats_dict[key]['gm_penalties'] += stat.gm_penalties # Update GM penalties
255
280
 
256
- # Calculate per game stats
281
+ # Calculate per game stats (using games_with_stats as denominator for accuracy)
257
282
  for key, stat in stats_dict.items():
258
- if stat['games_played'] > 0:
259
- stat['goals_per_game'] = stat['goals'] / stat['games_played']
260
- stat['points_per_game'] = stat['points'] / stat['games_played']
261
- stat['assists_per_game'] = stat['assists'] / stat['games_played']
262
- stat['penalties_per_game'] = stat['penalties'] / stat['games_played']
263
- stat['gm_penalties_per_game'] = stat['gm_penalties'] / stat['games_played'] # Calculate GM penalties per game
283
+ if stat['games_with_stats'] > 0:
284
+ stat['goals_per_game'] = stat['goals'] / stat['games_with_stats']
285
+ stat['points_per_game'] = stat['points'] / stat['games_with_stats']
286
+ stat['assists_per_game'] = stat['assists'] / stat['games_with_stats']
287
+ stat['penalties_per_game'] = stat['penalties'] / stat['games_with_stats']
288
+ stat['gm_penalties_per_game'] = stat['gm_penalties'] / stat['games_with_stats'] # Calculate GM penalties per game
264
289
 
265
290
  # Ensure all keys have valid human_id values
266
291
  stats_dict = {key: value for key, value in stats_dict.items() if key[1] is not None}
267
292
 
268
293
  # Populate first_game_id and last_game_id
269
- for key, stat in stats_dict.items():
270
- all_game_ids = stat['game_ids']
271
- if all_game_ids:
272
- first_game = session.query(Game).filter(Game.id.in_(all_game_ids)).order_by(Game.date, Game.time).first()
273
- last_game = session.query(Game).filter(Game.id.in_(all_game_ids)).order_by(Game.date.desc(), Game.time.desc()).first()
274
- stat['first_game_id'] = first_game.id if first_game else None
275
- stat['last_game_id'] = last_game.id if last_game else None
294
+ # Only show progress for "All Orgs" with no window (all-time stats) - the slowest case
295
+ total_players = len(stats_dict)
296
+ if aggregation_type == 'org' and aggregation_id == ALL_ORGS_ID and aggregation_window is None and total_players > 1000:
297
+ progress = create_progress_tracker(total_players, f"Processing {total_players} players for {aggregation_name}")
298
+ for idx, (key, stat) in enumerate(stats_dict.items()):
299
+ all_game_ids = stat['game_ids']
300
+ if all_game_ids:
301
+ first_game = session.query(Game).filter(Game.id.in_(all_game_ids)).order_by(Game.date, Game.time).first()
302
+ last_game = session.query(Game).filter(Game.id.in_(all_game_ids)).order_by(Game.date.desc(), Game.time.desc()).first()
303
+ stat['first_game_id'] = first_game.id if first_game else None
304
+ stat['last_game_id'] = last_game.id if last_game else None
305
+ if (idx + 1) % 100 == 0 or (idx + 1) == total_players: # Update every 100 players
306
+ progress.update(idx + 1)
307
+ else:
308
+ # No progress tracking for all other cases
309
+ for key, stat in stats_dict.items():
310
+ all_game_ids = stat['game_ids']
311
+ if all_game_ids:
312
+ first_game = session.query(Game).filter(Game.id.in_(all_game_ids)).order_by(Game.date, Game.time).first()
313
+ last_game = session.query(Game).filter(Game.id.in_(all_game_ids)).order_by(Game.date.desc(), Game.time.desc()).first()
314
+ stat['first_game_id'] = first_game.id if first_game else None
315
+ stat['last_game_id'] = last_game.id if last_game else None
276
316
 
277
317
  # Calculate current point streak (only for all-time stats)
278
318
  if aggregation_window is None:
279
- for key, stat in stats_dict.items():
280
- aggregation_id, human_id = key
281
- streak_length, avg_points = calculate_current_point_streak(session, human_id, filter_condition)
282
- stat['current_point_streak'] = streak_length
283
- stat['current_point_streak_avg_points'] = avg_points
319
+ total_players = len(stats_dict)
320
+ # Show progress for All Orgs - this is the slowest part
321
+ if aggregation_type == 'org' and aggregation_id == ALL_ORGS_ID and total_players > 1000:
322
+ progress = create_progress_tracker(total_players, f"Calculating point streaks for {total_players} players")
323
+ for idx, (key, stat) in enumerate(stats_dict.items()):
324
+ agg_id, human_id = key
325
+ streak_length, avg_points = calculate_current_point_streak(session, human_id, filter_condition)
326
+ stat['current_point_streak'] = streak_length
327
+ stat['current_point_streak_avg_points'] = avg_points
328
+ if (idx + 1) % 100 == 0 or (idx + 1) == total_players:
329
+ progress.update(idx + 1)
330
+ else:
331
+ for key, stat in stats_dict.items():
332
+ agg_id, human_id = key
333
+ streak_length, avg_points = calculate_current_point_streak(session, human_id, filter_condition)
334
+ stat['current_point_streak'] = streak_length
335
+ stat['current_point_streak_avg_points'] = avg_points
284
336
 
285
337
  # Calculate total_in_rank
286
338
  total_in_rank = len(stats_dict)
@@ -292,6 +344,8 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
292
344
  stats_dict[key][f'{field}_rank'] = rank
293
345
 
294
346
  assign_ranks(stats_dict, 'games_played')
347
+ assign_ranks(stats_dict, 'games_participated') # Rank by total participation
348
+ assign_ranks(stats_dict, 'games_with_stats') # Rank by games with full stats
295
349
  assign_ranks(stats_dict, 'goals')
296
350
  assign_ranks(stats_dict, 'assists')
297
351
  assign_ranks(stats_dict, 'points')
@@ -330,7 +384,11 @@ def aggregate_skater_stats(session, aggregation_type, aggregation_id, debug_huma
330
384
  skater_stat = StatsModel(
331
385
  aggregation_id=aggregation_id,
332
386
  human_id=human_id,
333
- games_played=stat['games_played'],
387
+ games_played=stat['games_played'], # DEPRECATED - for backward compatibility
388
+ games_participated=stat['games_participated'], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
389
+ games_participated_rank=stat['games_participated_rank'],
390
+ games_with_stats=stat['games_with_stats'], # Games with full stats: FINAL, FINAL_SO only
391
+ games_with_stats_rank=stat['games_with_stats_rank'],
334
392
  goals=stat['goals'],
335
393
  assists=stat['assists'],
336
394
  points=stat['goals'] + stat['assists'],
@@ -99,6 +99,7 @@ class Goal(db.Model):
99
99
  sequence_number = db.Column(db.Integer)
100
100
  __table_args__ = (
101
101
  db.UniqueConstraint('game_id', 'scoring_team_id', 'sequence_number', name='_goal_team_sequence_uc'),
102
+ db.UniqueConstraint('game_id', 'period', 'time', 'goal_scorer_id', 'scoring_team_id', name='uq_goals_no_duplicates'),
102
103
  )
103
104
 
104
105
  class Human(db.Model):
@@ -4,26 +4,34 @@ from datetime import datetime, timedelta
4
4
  class ProgressTracker:
5
5
  """
6
6
  Reusable progress tracker with ETA calculation for stats aggregation processes.
7
+ Supports optional custom counters for tracking additional metrics.
7
8
  """
8
-
9
- def __init__(self, total_items, description="Processing"):
9
+
10
+ def __init__(self, total_items, description="Processing", custom_counters=None):
10
11
  self.total_items = total_items
11
12
  self.description = description
12
13
  self.start_time = time.time()
13
14
  self.processed_items = 0
14
15
  self.last_update_time = self.start_time
16
+ self.custom_counters = custom_counters if custom_counters else {}
15
17
 
16
- def update(self, processed_count=None):
18
+ def update(self, processed_count=None, **kwargs):
17
19
  """
18
20
  Update progress. If processed_count is None, increment by 1.
21
+ kwargs can be used to update custom counters.
19
22
  """
20
23
  if processed_count is not None:
21
24
  self.processed_items = processed_count
22
25
  else:
23
26
  self.processed_items += 1
24
-
27
+
28
+ # Update custom counters
29
+ for key, value in kwargs.items():
30
+ if key in self.custom_counters:
31
+ self.custom_counters[key] = value
32
+
25
33
  current_time = time.time()
26
-
34
+
27
35
  # Only update display if at least 0.1 seconds have passed (to avoid spamming)
28
36
  if current_time - self.last_update_time >= 0.1 or self.processed_items == self.total_items:
29
37
  self._display_progress()
@@ -52,10 +60,15 @@ class ProgressTracker:
52
60
  eta_str = self._format_time(eta_seconds)
53
61
 
54
62
  elapsed_str = self._format_time(elapsed_time)
55
-
63
+
56
64
  progress_msg = f"\r{self.description}: {self.processed_items}/{self.total_items} ({percentage:.1f}%) | "
57
65
  progress_msg += f"Elapsed: {elapsed_str} | ETA: {eta_str}"
58
-
66
+
67
+ # Add custom counters to the display
68
+ if self.custom_counters:
69
+ counter_parts = [f"{key}: {value}" for key, value in self.custom_counters.items()]
70
+ progress_msg += " | " + ", ".join(counter_parts)
71
+
59
72
  print(progress_msg, end="", flush=True)
60
73
 
61
74
  # Add newline when complete
@@ -84,8 +97,9 @@ class ProgressTracker:
84
97
  self.processed_items = self.total_items
85
98
  self._display_progress()
86
99
 
87
- def create_progress_tracker(total_items, description="Processing"):
100
+ def create_progress_tracker(total_items, description="Processing", custom_counters=None):
88
101
  """
89
102
  Factory function to create a progress tracker.
103
+ custom_counters: dict of counter_name: initial_value for additional metrics to track
90
104
  """
91
- return ProgressTracker(total_items, description)
105
+ return ProgressTracker(total_items, description, custom_counters)
@@ -52,8 +52,12 @@ class BaseStatsSkater(db.Model):
52
52
  __abstract__ = True
53
53
  id = db.Column(db.Integer, primary_key=True)
54
54
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
55
- games_played = db.Column(db.Integer, default=0)
55
+ games_played = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
56
56
  games_played_rank = db.Column(db.Integer, default=0)
57
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
58
+ games_participated_rank = db.Column(db.Integer, default=0)
59
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO
60
+ games_with_stats_rank = db.Column(db.Integer, default=0)
57
61
  goals = db.Column(db.Integer, default=0)
58
62
  goals_rank = db.Column(db.Integer, default=0)
59
63
  assists = db.Column(db.Integer, default=0)
@@ -104,8 +108,12 @@ class BaseStatsGoalie(db.Model):
104
108
  __abstract__ = True
105
109
  id = db.Column(db.Integer, primary_key=True)
106
110
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
107
- games_played = db.Column(db.Integer, default=0)
111
+ games_played = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
108
112
  games_played_rank = db.Column(db.Integer, default=0)
113
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
114
+ games_participated_rank = db.Column(db.Integer, default=0)
115
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO
116
+ games_with_stats_rank = db.Column(db.Integer, default=0)
109
117
  goals_allowed = db.Column(db.Integer, default=0)
110
118
  goals_allowed_rank = db.Column(db.Integer, default=0)
111
119
  goals_allowed_per_game = db.Column(db.Float, default=0.0)
@@ -137,8 +145,12 @@ class BaseStatsReferee(db.Model):
137
145
  __abstract__ = True
138
146
  id = db.Column(db.Integer, primary_key=True)
139
147
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
140
- games_reffed = db.Column(db.Integer, default=0)
148
+ games_reffed = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
141
149
  games_reffed_rank = db.Column(db.Integer, default=0)
150
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
151
+ games_participated_rank = db.Column(db.Integer, default=0)
152
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO (for per-game averages)
153
+ games_with_stats_rank = db.Column(db.Integer, default=0)
142
154
  penalties_given = db.Column(db.Integer, default=0)
143
155
  penalties_given_rank = db.Column(db.Integer, default=0)
144
156
  penalties_per_game = db.Column(db.Float, default=0.0)
@@ -170,8 +182,12 @@ class BaseStatsScorekeeper(db.Model):
170
182
  __abstract__ = True
171
183
  id = db.Column(db.Integer, primary_key=True)
172
184
  human_id = db.Column(db.Integer, db.ForeignKey('humans.id'), nullable=False)
173
- games_recorded = db.Column(db.Integer, default=0)
185
+ games_recorded = db.Column(db.Integer, default=0) # DEPRECATED - use games_participated instead
174
186
  games_recorded_rank = db.Column(db.Integer, default=0)
187
+ games_participated = db.Column(db.Integer, default=0) # Count FINAL, FINAL_SO, FORFEIT, NOEVENTS
188
+ games_participated_rank = db.Column(db.Integer, default=0)
189
+ games_with_stats = db.Column(db.Integer, default=0) # Count only FINAL, FINAL_SO (for per-game averages)
190
+ games_with_stats_rank = db.Column(db.Integer, default=0)
175
191
  sog_given = db.Column(db.Integer, default=0)
176
192
  sog_given_rank = db.Column(db.Integer, default=0)
177
193
  sog_per_game = db.Column(db.Float, default=0.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hockey-blast-common-lib
3
- Version: 0.1.61
3
+ Version: 0.1.63
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -1,28 +1,28 @@
1
1
  hockey_blast_common_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  hockey_blast_common_lib/aggregate_all_stats.py,sha256=QhuSvGjuk4jVywNRcgxB-9ooJAoAbZRkaLjLe9Q1hEM,1363
3
- hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=WmKqGdM0SZLYXailCsBM4Wdpy8kOcmPtF9JOxLxzmRc,13217
3
+ hockey_blast_common_lib/aggregate_goalie_stats.py,sha256=J0hY5mK6UATScjqtPqbTfEeApnZy6lC3qlMGN02qhpY,14831
4
4
  hockey_blast_common_lib/aggregate_h2h_stats.py,sha256=dC5TcJZGkpIQTiq3z40kOX6EjEhFbGv5EL0P1EClBQ0,11117
5
- hockey_blast_common_lib/aggregate_human_stats.py,sha256=JZcXJofF1wmFPcLbalM_GiXrq22eUJO28R_xyypw4rI,25456
6
- hockey_blast_common_lib/aggregate_referee_stats.py,sha256=oX9UgD_mvvDJv4Sy1jJemUKWNi3N-1sOjfHNSg4VzOE,13351
5
+ hockey_blast_common_lib/aggregate_human_stats.py,sha256=wRw0X_57-AUWHHn5SXRLovC7KQwQtTJm7Twb7XIQv_M,25560
6
+ hockey_blast_common_lib/aggregate_referee_stats.py,sha256=zwMpQxq-tONNKSv87hwM_l94L1wBpu2ZVW-bvyV-zjY,15561
7
7
  hockey_blast_common_lib/aggregate_s2s_stats.py,sha256=urYN0Q06twwLO-XWGlSMVAVOTVR_D2AWdmoGsxIYHXE,6737
8
- hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=_SWt7qqUl31As1pkEJ40YLh9ItXdBuen3RjtSzHGP8U,11997
9
- hockey_blast_common_lib/aggregate_skater_stats.py,sha256=Y3zDeyEWuEae-PdnFMz7aGRzP4BnYNTut_zqRgR56yI,22715
8
+ hockey_blast_common_lib/aggregate_scorekeeper_stats.py,sha256=SBwZ6kBMbgsBgyiLIuCTmgRHpseqjIlqjvR63rBvyiY,13500
9
+ hockey_blast_common_lib/aggregate_skater_stats.py,sha256=d_4PRCZw458FMp6Yv4_QM8hbkXAH_dkX5O3_jMtaoU8,26547
10
10
  hockey_blast_common_lib/assign_skater_skill.py,sha256=Asq6iRMPsCMDnvuNSd-M3s4Gee4kDocP9Eznwju_9kA,2749
11
11
  hockey_blast_common_lib/db_connection.py,sha256=HvPxDvOj7j5H85RfslGvHVNevfg7mKCd0syJ6NX21mU,1890
12
12
  hockey_blast_common_lib/dump_sample_db.sh,sha256=MY3lnzTXBoWd76-ZlZr9nWsKMEVgyRsUn-LZ2d1JWZs,810
13
13
  hockey_blast_common_lib/h2h_models.py,sha256=0st4xoJO0U6ONfx3BV03BQvHjZE31e_PqZfphAJMoSU,7968
14
- hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=vJK83rusOv9VoljFTC03-0leXhGBHa1WV0XvM2ri870,4648906
15
- hockey_blast_common_lib/models.py,sha256=ccM886RcSFDiJ3yj2l9OqRi_PwR1L6WU8AsqzgV3_t0,19598
14
+ hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz,sha256=u5lGgEUTbJddCd-y2hu8LsMRoZW0Ox_jNzWYqNR1sbc,4648908
15
+ hockey_blast_common_lib/models.py,sha256=rbpgLI6iol_Eh-zvL1jSVqA7t08H6aFQoSJvMzjp81U,19724
16
16
  hockey_blast_common_lib/options.py,sha256=XecGGlizbul7BnMePrvIqvEq5_w49UoG3Yu9iv961gg,1499
17
- hockey_blast_common_lib/progress_utils.py,sha256=H_zRFOsb2qQQpGw56wJghZ1nUe_m6zqGeR9hZ33Y1Uo,3229
17
+ hockey_blast_common_lib/progress_utils.py,sha256=7cqyUTMmW3xAIh5JKFlhnBiybCJ9WvGDz7ihH59Lc_0,3953
18
18
  hockey_blast_common_lib/restore_sample_db.sh,sha256=7W3lzRZeu9zXIu1Bvtnaw8EHc1ulHmFM4mMh86oUQJo,2205
19
19
  hockey_blast_common_lib/skills_in_divisions.py,sha256=m-UEwMwn1KM7wOYvDstgsOEeH57M9V6yrkBoghzGYKE,7005
20
20
  hockey_blast_common_lib/skills_propagation.py,sha256=nUxntyK8M4xWjHpkfze8f0suaBeunxicgDCduGmNJ-A,18468
21
- hockey_blast_common_lib/stats_models.py,sha256=64sUq_iWhNXi_b_V_1INuQ1RusKaTASjurkRo5gQOs4,26703
21
+ hockey_blast_common_lib/stats_models.py,sha256=yLfsR0RhSc95-8ULdJ8tuvLd6RjIFkgZ74ejubJYVUw,28187
22
22
  hockey_blast_common_lib/stats_utils.py,sha256=DXsPO4jw8XsdRUN46TGF_IiBAfz3GCIVBswCGp5ELDk,284
23
23
  hockey_blast_common_lib/utils.py,sha256=1YJRAj1lhftjIAM2frFi4A4K90kCJaxWlgBQ1-77xZY,6486
24
24
  hockey_blast_common_lib/wsgi.py,sha256=y3NxoJfWjdzX3iP7RGvDEer6zcnPyCanpqSgW1BlXgg,779
25
- hockey_blast_common_lib-0.1.61.dist-info/METADATA,sha256=AtOG9FeESKu9wi75z2Q9cOctyOnmyoNYfu6hkiCFn0c,318
26
- hockey_blast_common_lib-0.1.61.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
27
- hockey_blast_common_lib-0.1.61.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
28
- hockey_blast_common_lib-0.1.61.dist-info/RECORD,,
25
+ hockey_blast_common_lib-0.1.63.dist-info/METADATA,sha256=VFQNc4UUo8qUlyk2Bd_kykrsxVYwclpckA2eq2OCTgc,318
26
+ hockey_blast_common_lib-0.1.63.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
27
+ hockey_blast_common_lib-0.1.63.dist-info/top_level.txt,sha256=wIR4LIkE40npoA2QlOdfCYlgFeGbsHR8Z6r0h46Vtgc,24
28
+ hockey_blast_common_lib-0.1.63.dist-info/RECORD,,