hockey-blast-common-lib 0.1.65__tar.gz

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.
Files changed (36) hide show
  1. hockey_blast_common_lib-0.1.65/MANIFEST.in +2 -0
  2. hockey_blast_common_lib-0.1.65/PKG-INFO +11 -0
  3. hockey_blast_common_lib-0.1.65/README.md +1 -0
  4. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/__init__.py +0 -0
  5. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_all_stats.py +34 -0
  6. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_goalie_stats.py +455 -0
  7. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_h2h_stats.py +262 -0
  8. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_human_stats.py +763 -0
  9. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_referee_stats.py +445 -0
  10. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_s2s_stats.py +203 -0
  11. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_scorekeeper_stats.py +367 -0
  12. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/aggregate_skater_stats.py +807 -0
  13. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/assign_skater_skill.py +79 -0
  14. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/db_connection.py +99 -0
  15. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/dump_sample_db.sh +28 -0
  16. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/embedding_utils.py +309 -0
  17. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/h2h_models.py +201 -0
  18. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
  19. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/models.py +573 -0
  20. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/options.py +59 -0
  21. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/progress_utils.py +113 -0
  22. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/restore_sample_db.sh +44 -0
  23. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/skills_in_divisions.py +266 -0
  24. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/skills_propagation.py +502 -0
  25. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/stats_models.py +924 -0
  26. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/stats_utils.py +9 -0
  27. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/utils.py +215 -0
  28. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib/wsgi.py +24 -0
  29. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib.egg-info/PKG-INFO +11 -0
  30. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib.egg-info/SOURCES.txt +34 -0
  31. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib.egg-info/dependency_links.txt +1 -0
  32. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib.egg-info/requires.txt +4 -0
  33. hockey_blast_common_lib-0.1.65/hockey_blast_common_lib.egg-info/top_level.txt +1 -0
  34. hockey_blast_common_lib-0.1.65/pyproject.toml +13 -0
  35. hockey_blast_common_lib-0.1.65/setup.cfg +4 -0
  36. hockey_blast_common_lib-0.1.65/setup.py +18 -0
@@ -0,0 +1,2 @@
1
+ include hockey_blast_common_lib/*.sh
2
+ include hockey_blast_common_lib/*.gz
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.1
2
+ Name: hockey-blast-common-lib
3
+ Version: 0.1.65
4
+ Summary: Common library for shared functionality and DB models
5
+ Author: Pavel Kletskov
6
+ Author-email: kletskov@gmail.com
7
+ Requires-Python: >=3.7
8
+ Requires-Dist: setuptools
9
+ Requires-Dist: Flask-SQLAlchemy
10
+ Requires-Dist: SQLAlchemy
11
+ Requires-Dist: requests
@@ -0,0 +1 @@
1
+ Library for DB models and some common utils to use hockey-blast frontend and backend
@@ -0,0 +1,34 @@
1
+ import os
2
+ import sys
3
+
4
+ # Add the package directory to the Python path
5
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
+
7
+ from hockey_blast_common_lib.aggregate_goalie_stats import run_aggregate_goalie_stats
8
+ from hockey_blast_common_lib.aggregate_human_stats import run_aggregate_human_stats
9
+ from hockey_blast_common_lib.aggregate_referee_stats import run_aggregate_referee_stats
10
+ from hockey_blast_common_lib.aggregate_scorekeeper_stats import (
11
+ run_aggregate_scorekeeper_stats,
12
+ )
13
+ from hockey_blast_common_lib.aggregate_skater_stats import run_aggregate_skater_stats
14
+
15
+ if __name__ == "__main__":
16
+ print("Running aggregate_skater_stats...", flush=True)
17
+ run_aggregate_skater_stats()
18
+ print("Finished running aggregate_skater_stats\n")
19
+
20
+ print("Running aggregate_goalie_stats...", flush=True)
21
+ run_aggregate_goalie_stats()
22
+ print("Finished running aggregate_goalie_stats\n")
23
+
24
+ print("Running aggregate_referee_stats...", flush=True)
25
+ run_aggregate_referee_stats()
26
+ print("Finished running aggregate_referee_stats\n")
27
+
28
+ print("Running aggregate_scorekeeper_stats...", flush=True)
29
+ run_aggregate_scorekeeper_stats()
30
+ print("Finished running aggregate_scorekeeper_stats\n")
31
+
32
+ print("Running aggregate_human_stats...", flush=True)
33
+ run_aggregate_human_stats()
34
+ print("Finished running aggregate_human_stats\n")
@@ -0,0 +1,455 @@
1
+ import os
2
+ import sys
3
+
4
+ # Add the package directory to the Python path
5
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
+
7
+ from datetime import datetime, timedelta
8
+
9
+ import sqlalchemy
10
+ from sqlalchemy.sql import func
11
+
12
+ from hockey_blast_common_lib.db_connection import create_session
13
+ from hockey_blast_common_lib.models import (
14
+ Division,
15
+ Game,
16
+ GoalieSaves,
17
+ Human,
18
+ Organization,
19
+ )
20
+ from hockey_blast_common_lib.options import (
21
+ MIN_GAMES_FOR_DIVISION_STATS,
22
+ MIN_GAMES_FOR_LEVEL_STATS,
23
+ MIN_GAMES_FOR_ORG_STATS,
24
+ )
25
+ from hockey_blast_common_lib.progress_utils import create_progress_tracker
26
+ from hockey_blast_common_lib.stats_models import (
27
+ DivisionStatsDailyGoalie,
28
+ DivisionStatsGoalie,
29
+ DivisionStatsWeeklyGoalie,
30
+ LevelStatsGoalie,
31
+ OrgStatsDailyGoalie,
32
+ OrgStatsGoalie,
33
+ OrgStatsWeeklyGoalie,
34
+ )
35
+ from hockey_blast_common_lib.stats_utils import ALL_ORGS_ID
36
+ from hockey_blast_common_lib.utils import (
37
+ assign_ranks,
38
+ get_all_division_ids_for_org,
39
+ get_non_human_ids,
40
+ get_start_datetime,
41
+ )
42
+
43
+ # Import status constants for game filtering
44
+ FINAL_STATUS = "Final"
45
+ FINAL_SO_STATUS = "Final(SO)"
46
+ FORFEIT_STATUS = "FORFEIT"
47
+ NOEVENTS_STATUS = "NOEVENTS"
48
+
49
+
50
+ def aggregate_goalie_stats(
51
+ session,
52
+ aggregation_type,
53
+ aggregation_id,
54
+ debug_human_id=None,
55
+ aggregation_window=None,
56
+ ):
57
+ human_ids_to_filter = get_non_human_ids(session)
58
+
59
+ # Get the name of the aggregation, for debug purposes
60
+ if aggregation_type == "org":
61
+ if aggregation_id == ALL_ORGS_ID:
62
+ aggregation_name = "All Orgs"
63
+ filter_condition = sqlalchemy.true() # No filter for organization
64
+ else:
65
+ aggregation_name = (
66
+ session.query(Organization)
67
+ .filter(Organization.id == aggregation_id)
68
+ .first()
69
+ .organization_name
70
+ )
71
+ filter_condition = Game.org_id == aggregation_id
72
+ print(
73
+ f"Aggregating goalie stats for {aggregation_name} with window {aggregation_window}..."
74
+ )
75
+ if aggregation_window == "Daily":
76
+ StatsModel = OrgStatsDailyGoalie
77
+ elif aggregation_window == "Weekly":
78
+ StatsModel = OrgStatsWeeklyGoalie
79
+ else:
80
+ StatsModel = OrgStatsGoalie
81
+ min_games = MIN_GAMES_FOR_ORG_STATS
82
+ elif aggregation_type == "division":
83
+ if aggregation_window == "Daily":
84
+ StatsModel = DivisionStatsDailyGoalie
85
+ elif aggregation_window == "Weekly":
86
+ StatsModel = DivisionStatsWeeklyGoalie
87
+ else:
88
+ StatsModel = DivisionStatsGoalie
89
+ min_games = MIN_GAMES_FOR_DIVISION_STATS
90
+ filter_condition = Game.division_id == aggregation_id
91
+ elif aggregation_type == "level":
92
+ StatsModel = LevelStatsGoalie
93
+ min_games = MIN_GAMES_FOR_LEVEL_STATS
94
+ filter_condition = Division.level_id == aggregation_id
95
+ # Add filter to only include games for the last 5 years
96
+ five_years_ago = datetime.now() - timedelta(days=5 * 365)
97
+ level_window_filter = (
98
+ func.cast(
99
+ func.concat(Game.date, " ", Game.time), sqlalchemy.types.TIMESTAMP
100
+ )
101
+ >= five_years_ago
102
+ )
103
+ filter_condition = filter_condition & level_window_filter
104
+ else:
105
+ raise ValueError("Invalid aggregation type")
106
+
107
+ # Delete existing items from the stats table
108
+ session.query(StatsModel).filter(
109
+ StatsModel.aggregation_id == aggregation_id
110
+ ).delete()
111
+ session.commit()
112
+
113
+ # Apply aggregation window filter
114
+ if aggregation_window:
115
+ last_game_datetime_str = (
116
+ session.query(func.max(func.concat(Game.date, " ", Game.time)))
117
+ .filter(filter_condition, Game.status.like("Final%"))
118
+ .scalar()
119
+ )
120
+ start_datetime = get_start_datetime(last_game_datetime_str, aggregation_window)
121
+ if start_datetime:
122
+ game_window_filter = func.cast(
123
+ func.concat(Game.date, " ", Game.time), sqlalchemy.types.TIMESTAMP
124
+ ).between(start_datetime, last_game_datetime_str)
125
+ filter_condition = filter_condition & game_window_filter
126
+ else:
127
+ # print(f"Warning: No valid start datetime for aggregation window '{aggregation_window}' for {aggregation_name}. No games will be included.")
128
+ return
129
+
130
+ # Aggregate games played, goals allowed, and shots faced for each goalie using GoalieSaves table
131
+ # Filter games by status upfront for performance (avoid CASE statements)
132
+ # Only count games with these statuses: FINAL, FINAL_SO, FORFEIT, NOEVENTS
133
+ query = (
134
+ session.query(
135
+ GoalieSaves.goalie_id.label("human_id"),
136
+ func.count(GoalieSaves.game_id).label("games_played"),
137
+ func.count(GoalieSaves.game_id).label(
138
+ "games_participated"
139
+ ), # Same as games_played after filtering
140
+ func.count(GoalieSaves.game_id).label(
141
+ "games_with_stats"
142
+ ), # Same as games_played after filtering
143
+ func.sum(GoalieSaves.goals_allowed).label("goals_allowed"),
144
+ func.sum(GoalieSaves.shots_against).label("shots_faced"),
145
+ func.array_agg(GoalieSaves.game_id).label("game_ids"),
146
+ )
147
+ .join(Game, GoalieSaves.game_id == Game.id)
148
+ .filter(
149
+ Game.status.in_(
150
+ [FINAL_STATUS, FINAL_SO_STATUS, FORFEIT_STATUS, NOEVENTS_STATUS]
151
+ )
152
+ )
153
+ .join(Division, Game.division_id == Division.id)
154
+ .filter(filter_condition)
155
+ )
156
+
157
+ # Filter for specific human_id if provided
158
+ if debug_human_id:
159
+ query = query.filter(GoalieSaves.goalie_id == debug_human_id)
160
+
161
+ goalie_stats = query.group_by(GoalieSaves.goalie_id).all()
162
+
163
+ # Combine the results
164
+ stats_dict = {}
165
+ for stat in goalie_stats:
166
+ if stat.human_id in human_ids_to_filter:
167
+ continue
168
+ key = (aggregation_id, stat.human_id)
169
+ if key not in stats_dict:
170
+ stats_dict[key] = {
171
+ "games_played": 0, # DEPRECATED - for backward compatibility
172
+ "games_participated": 0, # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
173
+ "games_with_stats": 0, # Games with full stats: FINAL, FINAL_SO only
174
+ "goals_allowed": 0,
175
+ "shots_faced": 0,
176
+ "goals_allowed_per_game": 0.0,
177
+ "save_percentage": 0.0,
178
+ "game_ids": [],
179
+ "first_game_id": None,
180
+ "last_game_id": None,
181
+ }
182
+ stats_dict[key]["games_played"] += stat.games_played
183
+ stats_dict[key]["games_participated"] += stat.games_participated
184
+ stats_dict[key]["games_with_stats"] += stat.games_with_stats
185
+ stats_dict[key]["goals_allowed"] += (
186
+ stat.goals_allowed if stat.goals_allowed is not None else 0
187
+ )
188
+ stats_dict[key]["shots_faced"] += (
189
+ stat.shots_faced if stat.shots_faced is not None else 0
190
+ )
191
+ stats_dict[key]["game_ids"].extend(stat.game_ids)
192
+
193
+ # Filter out entries with games_played less than min_games
194
+ stats_dict = {
195
+ key: value
196
+ for key, value in stats_dict.items()
197
+ if value["games_played"] >= min_games
198
+ }
199
+
200
+ # Calculate per game stats (using games_with_stats as denominator for accuracy)
201
+ for key, stat in stats_dict.items():
202
+ if stat["games_with_stats"] > 0:
203
+ stat["goals_allowed_per_game"] = (
204
+ stat["goals_allowed"] / stat["games_with_stats"]
205
+ )
206
+ stat["save_percentage"] = (
207
+ (stat["shots_faced"] - stat["goals_allowed"]) / stat["shots_faced"]
208
+ if stat["shots_faced"] > 0
209
+ else 0.0
210
+ )
211
+
212
+ # Ensure all keys have valid human_id values
213
+ stats_dict = {key: value for key, value in stats_dict.items() if key[1] is not None}
214
+
215
+ # Populate first_game_id and last_game_id
216
+ for key, stat in stats_dict.items():
217
+ all_game_ids = stat["game_ids"]
218
+ if all_game_ids:
219
+ first_game = (
220
+ session.query(Game)
221
+ .filter(Game.id.in_(all_game_ids))
222
+ .order_by(Game.date, Game.time)
223
+ .first()
224
+ )
225
+ last_game = (
226
+ session.query(Game)
227
+ .filter(Game.id.in_(all_game_ids))
228
+ .order_by(Game.date.desc(), Game.time.desc())
229
+ .first()
230
+ )
231
+ stat["first_game_id"] = first_game.id if first_game else None
232
+ stat["last_game_id"] = last_game.id if last_game else None
233
+
234
+ # Calculate total_in_rank
235
+ total_in_rank = len(stats_dict)
236
+
237
+ # Assign ranks within each level
238
+ assign_ranks(stats_dict, "games_played")
239
+ assign_ranks(stats_dict, "games_participated") # Rank by total participation
240
+ assign_ranks(stats_dict, "games_with_stats") # Rank by games with full stats
241
+ assign_ranks(stats_dict, "goals_allowed", reverse_rank=True)
242
+ assign_ranks(stats_dict, "shots_faced")
243
+ assign_ranks(stats_dict, "goals_allowed_per_game", reverse_rank=True)
244
+ assign_ranks(stats_dict, "save_percentage")
245
+
246
+ # Debug output for specific human
247
+ if debug_human_id:
248
+ if any(key[1] == debug_human_id for key in stats_dict):
249
+ human = session.query(Human).filter(Human.id == debug_human_id).first()
250
+ human_name = f"{human.first_name} {human.last_name}" if human else "Unknown"
251
+ print(
252
+ f"For Human {debug_human_id} ({human_name}) for {aggregation_type} {aggregation_id} ({aggregation_name}) , total_in_rank {total_in_rank} and window {aggregation_window}:"
253
+ )
254
+ for key, stat in stats_dict.items():
255
+ if key[1] == debug_human_id:
256
+ for k, v in stat.items():
257
+ print(f"{k}: {v}")
258
+
259
+ # Insert aggregated stats into the appropriate table with progress output
260
+ batch_size = 1000
261
+ for i, (key, stat) in enumerate(stats_dict.items(), 1):
262
+ aggregation_id, human_id = key
263
+ goals_allowed_per_game = (
264
+ stat["goals_allowed"] / stat["games_played"]
265
+ if stat["games_played"] > 0
266
+ else 0.0
267
+ )
268
+ save_percentage = (
269
+ (stat["shots_faced"] - stat["goals_allowed"]) / stat["shots_faced"]
270
+ if stat["shots_faced"] > 0
271
+ else 0.0
272
+ )
273
+ goalie_stat = StatsModel(
274
+ aggregation_id=aggregation_id,
275
+ human_id=human_id,
276
+ games_played=stat[
277
+ "games_played"
278
+ ], # DEPRECATED - for backward compatibility
279
+ games_participated=stat[
280
+ "games_participated"
281
+ ], # Total games: FINAL, FINAL_SO, FORFEIT, NOEVENTS
282
+ games_participated_rank=stat["games_participated_rank"],
283
+ games_with_stats=stat[
284
+ "games_with_stats"
285
+ ], # Games with full stats: FINAL, FINAL_SO only
286
+ games_with_stats_rank=stat["games_with_stats_rank"],
287
+ goals_allowed=stat["goals_allowed"],
288
+ shots_faced=stat["shots_faced"],
289
+ goals_allowed_per_game=goals_allowed_per_game,
290
+ save_percentage=save_percentage,
291
+ games_played_rank=stat["games_played_rank"],
292
+ goals_allowed_rank=stat["goals_allowed_rank"],
293
+ shots_faced_rank=stat["shots_faced_rank"],
294
+ goals_allowed_per_game_rank=stat["goals_allowed_per_game_rank"],
295
+ save_percentage_rank=stat["save_percentage_rank"],
296
+ total_in_rank=total_in_rank,
297
+ first_game_id=stat["first_game_id"],
298
+ last_game_id=stat["last_game_id"],
299
+ )
300
+ session.add(goalie_stat)
301
+ # Commit in batches
302
+ if i % batch_size == 0:
303
+ session.commit()
304
+ session.commit()
305
+
306
+
307
+ def run_aggregate_goalie_stats():
308
+ session = create_session("boss")
309
+ human_id_to_debug = None
310
+
311
+ # Get all org_id present in the Organization table
312
+ org_ids = session.query(Organization.id).all()
313
+ org_ids = [org_id[0] for org_id in org_ids]
314
+
315
+ for org_id in org_ids:
316
+ division_ids = get_all_division_ids_for_org(session, org_id)
317
+ org_name = (
318
+ session.query(Organization.organization_name)
319
+ .filter(Organization.id == org_id)
320
+ .scalar()
321
+ or f"org_id {org_id}"
322
+ )
323
+
324
+ if human_id_to_debug is None and division_ids:
325
+ # Process divisions with progress tracking
326
+ progress = create_progress_tracker(
327
+ len(division_ids),
328
+ f"Processing {len(division_ids)} divisions for {org_name}",
329
+ )
330
+ for i, division_id in enumerate(division_ids):
331
+ aggregate_goalie_stats(
332
+ session,
333
+ aggregation_type="division",
334
+ aggregation_id=division_id,
335
+ debug_human_id=human_id_to_debug,
336
+ )
337
+ aggregate_goalie_stats(
338
+ session,
339
+ aggregation_type="division",
340
+ aggregation_id=division_id,
341
+ debug_human_id=human_id_to_debug,
342
+ aggregation_window="Weekly",
343
+ )
344
+ aggregate_goalie_stats(
345
+ session,
346
+ aggregation_type="division",
347
+ aggregation_id=division_id,
348
+ debug_human_id=human_id_to_debug,
349
+ aggregation_window="Daily",
350
+ )
351
+ progress.update(i + 1)
352
+ else:
353
+ # Debug mode or no divisions - process without progress tracking
354
+ for division_id in division_ids:
355
+ aggregate_goalie_stats(
356
+ session,
357
+ aggregation_type="division",
358
+ aggregation_id=division_id,
359
+ debug_human_id=human_id_to_debug,
360
+ )
361
+ aggregate_goalie_stats(
362
+ session,
363
+ aggregation_type="division",
364
+ aggregation_id=division_id,
365
+ debug_human_id=human_id_to_debug,
366
+ aggregation_window="Weekly",
367
+ )
368
+ aggregate_goalie_stats(
369
+ session,
370
+ aggregation_type="division",
371
+ aggregation_id=division_id,
372
+ debug_human_id=human_id_to_debug,
373
+ aggregation_window="Daily",
374
+ )
375
+
376
+ # Process org-level stats with progress tracking
377
+ if human_id_to_debug is None:
378
+ org_progress = create_progress_tracker(
379
+ 3, f"Processing org-level stats for {org_name}"
380
+ )
381
+ aggregate_goalie_stats(
382
+ session,
383
+ aggregation_type="org",
384
+ aggregation_id=org_id,
385
+ debug_human_id=human_id_to_debug,
386
+ )
387
+ org_progress.update(1)
388
+ aggregate_goalie_stats(
389
+ session,
390
+ aggregation_type="org",
391
+ aggregation_id=org_id,
392
+ debug_human_id=human_id_to_debug,
393
+ aggregation_window="Weekly",
394
+ )
395
+ org_progress.update(2)
396
+ aggregate_goalie_stats(
397
+ session,
398
+ aggregation_type="org",
399
+ aggregation_id=org_id,
400
+ debug_human_id=human_id_to_debug,
401
+ aggregation_window="Daily",
402
+ )
403
+ org_progress.update(3)
404
+ else:
405
+ aggregate_goalie_stats(
406
+ session,
407
+ aggregation_type="org",
408
+ aggregation_id=org_id,
409
+ debug_human_id=human_id_to_debug,
410
+ )
411
+ aggregate_goalie_stats(
412
+ session,
413
+ aggregation_type="org",
414
+ aggregation_id=org_id,
415
+ debug_human_id=human_id_to_debug,
416
+ aggregation_window="Weekly",
417
+ )
418
+ aggregate_goalie_stats(
419
+ session,
420
+ aggregation_type="org",
421
+ aggregation_id=org_id,
422
+ debug_human_id=human_id_to_debug,
423
+ aggregation_window="Daily",
424
+ )
425
+
426
+ # Aggregate by level
427
+ level_ids = session.query(Division.level_id).distinct().all()
428
+ level_ids = [level_id[0] for level_id in level_ids if level_id[0] is not None]
429
+
430
+ if human_id_to_debug is None and level_ids:
431
+ # Process levels with progress tracking
432
+ level_progress = create_progress_tracker(
433
+ len(level_ids), f"Processing {len(level_ids)} skill levels"
434
+ )
435
+ for i, level_id in enumerate(level_ids):
436
+ aggregate_goalie_stats(
437
+ session,
438
+ aggregation_type="level",
439
+ aggregation_id=level_id,
440
+ debug_human_id=human_id_to_debug,
441
+ )
442
+ level_progress.update(i + 1)
443
+ else:
444
+ # Debug mode or no levels - process without progress tracking
445
+ for level_id in level_ids:
446
+ aggregate_goalie_stats(
447
+ session,
448
+ aggregation_type="level",
449
+ aggregation_id=level_id,
450
+ debug_human_id=human_id_to_debug,
451
+ )
452
+
453
+
454
+ if __name__ == "__main__":
455
+ run_aggregate_goalie_stats()