hockey-blast-common-lib 0.1.65__tar.gz → 0.1.67__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 (41) hide show
  1. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/PKG-INFO +1 -1
  2. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_all_stats.py +14 -0
  3. hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_game_stats_all.py +150 -0
  4. hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_game_stats_goalie.py +257 -0
  5. hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_game_stats_skater.py +361 -0
  6. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_goalie_stats.py +79 -0
  7. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_human_stats.py +12 -0
  8. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_referee_stats.py +75 -0
  9. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_scorekeeper_stats.py +91 -0
  10. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_skater_stats.py +118 -0
  11. hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_team_goalie_stats.py +265 -0
  12. hockey_blast_common_lib-0.1.67/hockey_blast_common_lib/aggregate_team_skater_stats.py +313 -0
  13. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/hockey_blast_sample_backup.sql.gz +0 -0
  14. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/models.py +16 -1
  15. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/stats_models.py +211 -5
  16. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/utils.py +65 -0
  17. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/PKG-INFO +1 -1
  18. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/SOURCES.txt +5 -0
  19. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/setup.py +1 -1
  20. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/MANIFEST.in +0 -0
  21. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/README.md +0 -0
  22. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/__init__.py +0 -0
  23. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_h2h_stats.py +0 -0
  24. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/aggregate_s2s_stats.py +0 -0
  25. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/assign_skater_skill.py +0 -0
  26. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/db_connection.py +0 -0
  27. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/dump_sample_db.sh +0 -0
  28. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/embedding_utils.py +0 -0
  29. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/h2h_models.py +0 -0
  30. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/options.py +0 -0
  31. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/progress_utils.py +0 -0
  32. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/restore_sample_db.sh +0 -0
  33. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/skills_in_divisions.py +0 -0
  34. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/skills_propagation.py +0 -0
  35. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/stats_utils.py +0 -0
  36. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib/wsgi.py +0 -0
  37. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/dependency_links.txt +0 -0
  38. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/requires.txt +0 -0
  39. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/hockey_blast_common_lib.egg-info/top_level.txt +0 -0
  40. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/pyproject.toml +0 -0
  41. {hockey_blast_common_lib-0.1.65 → hockey_blast_common_lib-0.1.67}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hockey-blast-common-lib
3
- Version: 0.1.65
3
+ Version: 0.1.67
4
4
  Summary: Common library for shared functionality and DB models
5
5
  Author: Pavel Kletskov
6
6
  Author-email: kletskov@gmail.com
@@ -11,6 +11,12 @@ from hockey_blast_common_lib.aggregate_scorekeeper_stats import (
11
11
  run_aggregate_scorekeeper_stats,
12
12
  )
13
13
  from hockey_blast_common_lib.aggregate_skater_stats import run_aggregate_skater_stats
14
+ from hockey_blast_common_lib.aggregate_team_goalie_stats import (
15
+ run_aggregate_team_goalie_stats,
16
+ )
17
+ from hockey_blast_common_lib.aggregate_team_skater_stats import (
18
+ run_aggregate_team_skater_stats,
19
+ )
14
20
 
15
21
  if __name__ == "__main__":
16
22
  print("Running aggregate_skater_stats...", flush=True)
@@ -32,3 +38,11 @@ if __name__ == "__main__":
32
38
  print("Running aggregate_human_stats...", flush=True)
33
39
  run_aggregate_human_stats()
34
40
  print("Finished running aggregate_human_stats\n")
41
+
42
+ print("Running aggregate_team_skater_stats...", flush=True)
43
+ run_aggregate_team_skater_stats()
44
+ print("Finished running aggregate_team_skater_stats\n")
45
+
46
+ print("Running aggregate_team_goalie_stats...", flush=True)
47
+ run_aggregate_team_goalie_stats()
48
+ print("Finished running aggregate_team_goalie_stats\n")
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Orchestrator for per-game statistics aggregation.
4
+
5
+ This script provides a unified interface for running both skater and goalie
6
+ per-game statistics aggregation. It supports full and append modes and can
7
+ run aggregations for specific roles or all roles together.
8
+
9
+ Usage examples:
10
+ # Full regeneration of all per-game stats
11
+ python aggregate_game_stats_all.py --mode full --role all
12
+
13
+ # Append new games for skaters only
14
+ python aggregate_game_stats_all.py --mode append --role skater
15
+
16
+ # Append new games for goalies only
17
+ python aggregate_game_stats_all.py --mode append --role goalie
18
+
19
+ The script automatically manages sentinel record tracking across both stat types
20
+ to ensure consistent append mode behavior.
21
+ """
22
+
23
+ import argparse
24
+ import os
25
+ import sys
26
+ from datetime import datetime
27
+
28
+ # Add the package directory to the Python path
29
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30
+
31
+ from hockey_blast_common_lib.aggregate_game_stats_goalie import aggregate_game_stats_goalie
32
+ from hockey_blast_common_lib.aggregate_game_stats_skater import aggregate_game_stats_skater
33
+ from hockey_blast_common_lib.db_connection import create_session
34
+
35
+
36
+ def run_aggregation(mode="full", role="all", human_id=None):
37
+ """Run per-game statistics aggregation.
38
+
39
+ Args:
40
+ mode: "full" to regenerate all records, "append" to process new games only
41
+ role: "skater", "goalie", or "all" to specify which stats to aggregate
42
+ human_id: Optional human_id to process only one player (for testing/debugging)
43
+ """
44
+ session = create_session("boss")
45
+
46
+ start_time = datetime.now()
47
+ print(f"\n{'='*80}")
48
+ print(f"PER-GAME STATISTICS AGGREGATION")
49
+ print(f"Mode: {mode.upper()}")
50
+ print(f"Role: {role.upper()}")
51
+ if human_id:
52
+ print(f"Human ID Filter: {human_id}")
53
+ print(f"Started: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
54
+ print(f"{'='*80}\n")
55
+
56
+ # Run skater aggregation
57
+ if role in ["skater", "all"]:
58
+ try:
59
+ aggregate_game_stats_skater(session, mode=mode, human_id=human_id)
60
+ except Exception as e:
61
+ print(f"\nERROR: Skater aggregation failed: {e}")
62
+ import traceback
63
+ traceback.print_exc()
64
+ if role == "skater":
65
+ sys.exit(1)
66
+ # If running all, continue to goalie even if skater fails
67
+ print("\nContinuing to goalie aggregation...\n")
68
+
69
+ # Run goalie aggregation
70
+ if role in ["goalie", "all"]:
71
+ try:
72
+ aggregate_game_stats_goalie(session, mode=mode, human_id=human_id)
73
+ except Exception as e:
74
+ print(f"\nERROR: Goalie aggregation failed: {e}")
75
+ import traceback
76
+ traceback.print_exc()
77
+ sys.exit(1)
78
+
79
+ end_time = datetime.now()
80
+ duration = end_time - start_time
81
+
82
+ print(f"\n{'='*80}")
83
+ print(f"AGGREGATION COMPLETE")
84
+ print(f"Duration: {duration}")
85
+ print(f"Finished: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
86
+ print(f"{'='*80}\n")
87
+
88
+
89
+ def main():
90
+ """Main entry point for CLI."""
91
+ parser = argparse.ArgumentParser(
92
+ description="Aggregate per-game statistics for skaters and goalies",
93
+ formatter_class=argparse.RawDescriptionHelpFormatter,
94
+ epilog="""
95
+ Examples:
96
+ # Full regeneration of all stats
97
+ %(prog)s --mode full --role all
98
+
99
+ # Append new games for skaters only
100
+ %(prog)s --mode append --role skater
101
+
102
+ # Append new games for goalies only
103
+ %(prog)s --mode append --role goalie
104
+
105
+ Notes:
106
+ - Full mode deletes and regenerates all records
107
+ - Append mode uses sentinel tracking with 1-day overlap
108
+ - Only saves non-zero records (RAG optimization)
109
+ - Skater stats: saves games with goals, assists, or penalties
110
+ - Goalie stats: saves games where goalie faced shots
111
+ """,
112
+ )
113
+
114
+ parser.add_argument(
115
+ "--mode",
116
+ choices=["full", "append"],
117
+ default="full",
118
+ help="Aggregation mode (default: full)",
119
+ )
120
+
121
+ parser.add_argument(
122
+ "--role",
123
+ choices=["skater", "goalie", "all"],
124
+ default="all",
125
+ help="Which role stats to aggregate (default: all)",
126
+ )
127
+
128
+ parser.add_argument(
129
+ "--human-id",
130
+ type=int,
131
+ default=None,
132
+ help="Optional: Limit processing to specific human_id (for testing)",
133
+ )
134
+
135
+ args = parser.parse_args()
136
+
137
+ try:
138
+ run_aggregation(mode=args.mode, role=args.role, human_id=args.human_id)
139
+ except KeyboardInterrupt:
140
+ print("\n\nAggregation cancelled by user.")
141
+ sys.exit(130)
142
+ except Exception as e:
143
+ print(f"\n\nFATAL ERROR: {e}")
144
+ import traceback
145
+ traceback.print_exc()
146
+ sys.exit(1)
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()
@@ -0,0 +1,257 @@
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
+ from sqlalchemy import and_, func
10
+ from sqlalchemy.exc import IntegrityError
11
+
12
+ from hockey_blast_common_lib.db_connection import create_session
13
+ from hockey_blast_common_lib.models import Division, Game, GameRoster, GoalieSaves, Human
14
+ from hockey_blast_common_lib.progress_utils import create_progress_tracker
15
+ from hockey_blast_common_lib.stats_models import GameStatsGoalie
16
+ from hockey_blast_common_lib.utils import get_non_human_ids
17
+
18
+ # Import status constants for game filtering
19
+ FINAL_STATUS = "Final"
20
+ FINAL_SO_STATUS = "Final(SO)"
21
+
22
+
23
+ def aggregate_game_stats_goalie(session, mode="full", human_id=None):
24
+ """Aggregate per-game goalie statistics.
25
+
26
+ Args:
27
+ session: Database session
28
+ mode: "full" to regenerate all records, "append" to process new games only
29
+ human_id: Optional human_id to process only one goalie (for testing/debugging)
30
+
31
+ The function stores individual game performance for each goalie with non-zero stats.
32
+ Only games where the goalie faced at least one shot are saved.
33
+ This sparse storage is optimized for RAG system queries.
34
+
35
+ Uses Incognito Human sentinel record (game_id=-1) to track last processed timestamp
36
+ for append mode with 1-day overlap to catch data corrections.
37
+
38
+ Note: Uses GameStatsGoalie table but shares sentinel tracking with GameStatsSkater
39
+ since both are per-game stats that should be processed together.
40
+ """
41
+
42
+ # Get Incognito Human for sentinel tracking (first_name="Incognito", middle_name="", last_name="Human")
43
+ incognito_human = session.query(Human).filter_by(
44
+ first_name="Incognito", middle_name="", last_name="Human"
45
+ ).first()
46
+ if not incognito_human:
47
+ raise RuntimeError("Incognito Human not found in database - required for sentinel tracking")
48
+ incognito_human_id = incognito_human.id
49
+
50
+ non_human_ids = get_non_human_ids(session)
51
+
52
+ # Add human_id to filter if specified
53
+ if human_id:
54
+ human = session.query(Human).filter_by(id=human_id).first()
55
+ if not human:
56
+ print(f"ERROR: Human ID {human_id} not found in database")
57
+ return
58
+ print(f"Limiting to human_id={human_id}: {human.first_name} {human.last_name}\n")
59
+
60
+ print(f"\n{'='*80}")
61
+ print(f"Aggregating per-game goalie statistics (mode: {mode})")
62
+ print(f"{'='*80}\n")
63
+
64
+ # Determine game filtering based on mode
65
+ # Note: We check GameStatsSkater for sentinel since they're processed together
66
+ if mode == "append":
67
+ # Import here to avoid circular dependency
68
+ from hockey_blast_common_lib.stats_models import GameStatsSkater
69
+
70
+ # Query sentinel record for last processed timestamp
71
+ sentinel = (
72
+ session.query(GameStatsSkater)
73
+ .filter(
74
+ GameStatsSkater.human_id == incognito_human_id,
75
+ GameStatsSkater.game_id == -1,
76
+ )
77
+ .first()
78
+ )
79
+
80
+ if sentinel:
81
+ last_processed = datetime.combine(sentinel.game_date, sentinel.game_time)
82
+ # Subtract 1 day for overlap to catch data corrections
83
+ start_datetime = last_processed - timedelta(days=1)
84
+ print(f"Append mode: Processing games after {start_datetime}")
85
+ print(f"(1-day overlap from last processed: {last_processed})\n")
86
+
87
+ # Delete records for games in the overlap window
88
+ delete_count = (
89
+ session.query(GameStatsGoalie)
90
+ .filter(
91
+ GameStatsGoalie.human_id != incognito_human_id,
92
+ func.cast(
93
+ func.concat(GameStatsGoalie.game_date, " ", GameStatsGoalie.game_time),
94
+ func.TIMESTAMP,
95
+ ) >= start_datetime,
96
+ )
97
+ .delete(synchronize_session=False)
98
+ )
99
+ session.commit()
100
+ print(f"Deleted {delete_count} existing records in overlap window\n")
101
+ else:
102
+ # No sentinel found, treat as full mode
103
+ print("No sentinel record found - treating as full mode\n")
104
+ mode = "full"
105
+ start_datetime = None
106
+ else:
107
+ start_datetime = None
108
+
109
+ if mode == "full":
110
+ # Delete all existing records (no sentinel for goalie table)
111
+ delete_count = session.query(GameStatsGoalie).delete(synchronize_session=False)
112
+ session.commit()
113
+ print(f"Full mode: Deleted {delete_count} existing records\n")
114
+
115
+ # Build game filter for eligible games
116
+ game_filter = Game.status.in_([FINAL_STATUS, FINAL_SO_STATUS])
117
+ if mode == "append" and start_datetime:
118
+ game_filter = and_(
119
+ game_filter,
120
+ func.cast(
121
+ func.concat(Game.date, " ", Game.time),
122
+ func.TIMESTAMP,
123
+ ) >= start_datetime,
124
+ )
125
+
126
+ # Count total GoalieSaves records to process for progress tracking
127
+ total_saves_records = (
128
+ session.query(GoalieSaves)
129
+ .join(Game, GoalieSaves.game_id == Game.id)
130
+ .filter(game_filter)
131
+ .count()
132
+ )
133
+ print(f"Processing {total_saves_records} goalie save records...\n")
134
+
135
+ if total_saves_records == 0:
136
+ print("No goalie records to process.\n")
137
+ return
138
+
139
+ # Query goalie saves joined with game metadata and roster
140
+ # GoalieSaves already has per-game goalie data
141
+ goalie_query = (
142
+ session.query(
143
+ GoalieSaves.game_id,
144
+ GoalieSaves.goalie_id.label("human_id"),
145
+ GameRoster.team_id,
146
+ Game.org_id,
147
+ Division.level_id,
148
+ Game.date.label("game_date"),
149
+ Game.time.label("game_time"),
150
+ GoalieSaves.goals_allowed,
151
+ GoalieSaves.shots_against.label("shots_faced"),
152
+ GoalieSaves.saves_count.label("saves"),
153
+ )
154
+ .join(Game, GoalieSaves.game_id == Game.id)
155
+ .join(Division, Game.division_id == Division.id)
156
+ .join(
157
+ GameRoster,
158
+ and_(
159
+ GameRoster.game_id == GoalieSaves.game_id,
160
+ GameRoster.human_id == GoalieSaves.goalie_id,
161
+ ),
162
+ )
163
+ .filter(
164
+ game_filter,
165
+ GoalieSaves.goalie_id.notin_(non_human_ids), # Filter placeholder humans
166
+ )
167
+ )
168
+
169
+ # Add human_id filter if specified
170
+ if human_id:
171
+ goalie_query = goalie_query.filter(GoalieSaves.goalie_id == human_id)
172
+
173
+ goalie_records = goalie_query.all()
174
+
175
+ print(f"Found {len(goalie_records)} goalie save records\n")
176
+
177
+ # Filter to only non-zero stats (CRITICAL for RAG efficiency)
178
+ # Only save records where goalie faced at least one shot
179
+ print("Filtering to non-zero records...")
180
+ nonzero_records = [record for record in goalie_records if record.shots_faced > 0]
181
+
182
+ print(f"Filtered: {len(nonzero_records)} non-zero records (from {len(goalie_records)} total)\n")
183
+
184
+ # Insert records in batches with progress tracking
185
+ batch_size = 1000
186
+ total_records = len(nonzero_records)
187
+
188
+ if total_records == 0:
189
+ print("No non-zero records to insert.\n")
190
+ else:
191
+ progress = create_progress_tracker(total_records, "Inserting per-game goalie stats")
192
+
193
+ records_to_insert = []
194
+ for i, record in enumerate(nonzero_records, 1):
195
+ # Calculate save percentage
196
+ if record.shots_faced > 0:
197
+ save_percentage = (record.shots_faced - record.goals_allowed) / record.shots_faced
198
+ else:
199
+ save_percentage = 0.0
200
+
201
+ game_stats_record = GameStatsGoalie(
202
+ game_id=record.game_id,
203
+ human_id=record.human_id,
204
+ team_id=record.team_id,
205
+ org_id=record.org_id,
206
+ level_id=record.level_id,
207
+ game_date=record.game_date,
208
+ game_time=record.game_time,
209
+ goals_allowed=record.goals_allowed,
210
+ shots_faced=record.shots_faced,
211
+ saves=record.saves,
212
+ save_percentage=save_percentage,
213
+ created_at=datetime.utcnow(),
214
+ )
215
+
216
+ records_to_insert.append(game_stats_record)
217
+
218
+ # Commit in batches
219
+ if i % batch_size == 0 or i == total_records:
220
+ session.bulk_save_objects(records_to_insert)
221
+ session.commit()
222
+ records_to_insert = []
223
+ progress.update(i)
224
+
225
+ print("\nInsert complete.\n")
226
+
227
+ print(f"\n{'='*80}")
228
+ print("Per-game goalie statistics aggregation complete")
229
+ print(f"{'='*80}\n")
230
+
231
+
232
+ def run_aggregate_game_stats_goalie():
233
+ """Main entry point for goalie per-game aggregation."""
234
+ import argparse
235
+
236
+ parser = argparse.ArgumentParser(description="Aggregate per-game goalie statistics")
237
+ parser.add_argument(
238
+ "--mode",
239
+ choices=["full", "append"],
240
+ default="full",
241
+ help="Aggregation mode: 'full' to regenerate all, 'append' to add new games only",
242
+ )
243
+ parser.add_argument(
244
+ "--human-id",
245
+ type=int,
246
+ default=None,
247
+ help="Optional: Limit processing to specific human_id (for testing)",
248
+ )
249
+
250
+ args = parser.parse_args()
251
+
252
+ session = create_session("boss")
253
+ aggregate_game_stats_goalie(session, mode=args.mode, human_id=args.human_id)
254
+
255
+
256
+ if __name__ == "__main__":
257
+ run_aggregate_game_stats_goalie()