wsba-hockey 1.1.6__py3-none-any.whl → 1.1.8__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.
wsba_hockey/__init__.py CHANGED
@@ -1 +1 @@
1
- from wsba_hockey.wsba_main import nhl_scrape_game,nhl_scrape_schedule,nhl_scrape_season,nhl_scrape_seasons_info,nhl_scrape_standings,nhl_scrape_roster,nhl_scrape_draft_rankings,nhl_scrape_prospects,nhl_calculate_stats,nhl_shooting_impacts,nhl_apply_xG,nhl_plot_skaters_shots,nhl_plot_games,repo_load_rosters,repo_load_schedule,repo_load_teaminfo,repo_load_pbp,repo_load_seasons
1
+ from wsba_hockey.wsba_main import nhl_scrape_game,nhl_scrape_schedule,nhl_scrape_season,nhl_scrape_seasons_info,nhl_scrape_standings,nhl_scrape_roster,nhl_scrape_draft_rankings,nhl_scrape_prospects,nhl_calculate_stats,nhl_apply_xG,nhl_plot_skaters_shots,nhl_plot_games,repo_load_rosters,repo_load_schedule,repo_load_teaminfo,repo_load_pbp,repo_load_seasons
@@ -90,14 +90,7 @@ def schedule_info(season: int):
90
90
 
91
91
  @app.get("/nhl/games/{game_id}")
92
92
  def pbp(game_id: int):
93
- info = rs.get(f'https://api-web.nhle.com/v1/gamecenter/{game_id}/play-by-play').json()
94
-
95
- season = info['season']
96
- dataset = ds.dataset(f's3://weakside-breakout/pbp/parquet/nhl_pbp_{season}.parquet', format='parquet')
97
- filter_expr = (ds.field('game_id')==game_id)
98
-
99
- table = dataset.to_table(use_threads=True,filter=filter_expr)
100
- df = table.to_pandas()
93
+ df = pd.read_csv(f'data/sources/20242025/{game_id}.csv')
101
94
 
102
95
  df = df.fillna('')
103
96
 
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import warnings
3
3
  import os
4
+ import asyncio
4
5
  import numpy as np
5
6
  import pandas as pd
6
7
  import requests as rs
@@ -179,7 +180,7 @@ def get_game_info(game_id):
179
180
  'coaches':get_game_coaches(game_id),
180
181
  'json_shifts':json_shifts}
181
182
 
182
- def parse_json(info):
183
+ async def parse_json(info):
183
184
  #Given game info, return JSON document
184
185
 
185
186
  #Retreive data
@@ -340,7 +341,7 @@ def clean_html_pbp(info):
340
341
 
341
342
  return cleaned_html
342
343
 
343
- def parse_html(info):
344
+ async def parse_html(info):
344
345
  #Given game info, return HTML event data
345
346
 
346
347
  #Retreive game information and html events
@@ -561,7 +562,7 @@ def espn_game_id(date,away,home):
561
562
  #Return: ESPN game id
562
563
  return game_id
563
564
 
564
- def parse_espn(date,away,home):
565
+ async def parse_espn(date,away,home):
565
566
  #Given a date formatted as YYYY-MM-DD and teams, return game events
566
567
  game_id = espn_game_id(date,away,home)
567
568
  url = f'https://www.espn.com/nhl/playbyplay/_/gameId/{game_id}'
@@ -711,15 +712,24 @@ def assign_target(data):
711
712
  #Revert sort and return dataframe
712
713
  return data.reset_index()
713
714
 
714
- def combine_pbp(info,sources):
715
+ async def combine_pbp(info,sources):
715
716
  #Given game info, return complete play-by-play data for provided game
716
717
 
717
- html_pbp = parse_html(info)
718
+ #Create tasks
719
+ html_task = asyncio.create_task(parse_html(info))
720
+ if info['season'] in [20052006, 20062007, 20072008, 20082009, 20092010]:
721
+ json_task = asyncio.create_task(parse_espn(str(info['game_date']),info['away_team_abbr'],info['home_team_abbr']))
722
+ json_type = 'espn'
723
+ else:
724
+ json_task = asyncio.create_task(parse_json(info))
725
+ json_type = 'nhl'
718
726
 
727
+ html_pbp, json_pbp = await asyncio.gather(html_task, json_task)
728
+
719
729
  #Route data combining - json if season is after 2009-2010:
720
- if str(info['season']) in ['20052006','20062007','20072008','20082009','20092010']:
730
+ if json_type == 'espn':
721
731
  #ESPN x HTML
722
- espn_pbp = parse_espn(str(info['game_date']),info['away_team_abbr'],info['home_team_abbr']).rename(columns={'coords_x':'x',"coords_y":'y'}).sort_values(['period','seconds_elapsed']).reset_index()
732
+ espn_pbp = json_pbp.rename(columns={'coords_x':'x',"coords_y":'y'}).sort_values(['period','seconds_elapsed']).reset_index()
723
733
  merge_col = ['period','seconds_elapsed','event_type','event_team_abbr']
724
734
 
725
735
  #Merge pbp
@@ -727,8 +737,6 @@ def combine_pbp(info,sources):
727
737
 
728
738
  else:
729
739
  #JSON x HTML
730
- json_pbp = parse_json(info)
731
-
732
740
  if sources:
733
741
  dirs_html = f'sources/{info['season']}/HTML/'
734
742
  dirs_json = f'sources/{info['season']}/JSON/'
@@ -1077,12 +1085,10 @@ def combine_shifts(info,sources):
1077
1085
  #Return: full shifts data converted to play-by-play format
1078
1086
  return full_shifts
1079
1087
 
1080
- def combine_data(info,sources):
1088
+ async def combine_data(info,sources):
1081
1089
  #Given game info, return complete play-by-play data
1082
1090
 
1083
- game_id = info['game_id']
1084
-
1085
- pbp = combine_pbp(info,sources)
1091
+ pbp = await combine_pbp(info,sources)
1086
1092
  shifts = combine_shifts(info,sources)
1087
1093
 
1088
1094
  #Combine data
wsba_hockey/workspace.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import numpy as np
2
2
  import pandas as pd
3
- import wsba_main as wsba
4
- import data_pipelines as data
3
+ import wsba_hockey as wsba
4
+ import wsba_hockey.data_pipelines as data
5
5
  import numpy as np
6
6
 
7
7
  season_load = wsba.repo_load_seasons()
wsba_hockey/wsba_main.py CHANGED
@@ -2,7 +2,9 @@ import random
2
2
  import os
3
3
  import requests as rs
4
4
  import pandas as pd
5
+ import asyncio
5
6
  import time
7
+ from typing import Literal, Union
6
8
  from datetime import datetime, timedelta, date
7
9
  from wsba_hockey.tools.scraping import *
8
10
  from wsba_hockey.tools.xg_model import *
@@ -14,44 +16,44 @@ from wsba_hockey.tools.plotting import *
14
16
 
15
17
  ## GLOBAL VARIABLES ##
16
18
  SEASONS = [
17
- '20072008',
18
- '20082009',
19
- '20092010',
20
- '20102011',
21
- '20112012',
22
- '20122013',
23
- '20132014',
24
- '20142015',
25
- '20152016',
26
- '20162017',
27
- '20172018',
28
- '20182019',
29
- '20192020',
30
- '20202021',
31
- '20212022',
32
- '20222023',
33
- '20232024',
34
- '20242025'
19
+ 20072008,
20
+ 20082009,
21
+ 20092010,
22
+ 20102011,
23
+ 20112012,
24
+ 20122013,
25
+ 20132014,
26
+ 20142015,
27
+ 20152016,
28
+ 20162017,
29
+ 20172018,
30
+ 20182019,
31
+ 20192020,
32
+ 20202021,
33
+ 20212022,
34
+ 20222023,
35
+ 20232024,
36
+ 20242025
35
37
  ]
36
38
 
37
- CONVERT_SEASONS = {'2007': '20072008',
38
- '2008': '20082009',
39
- '2009': '20092010',
40
- '2010': '20102011',
41
- '2011': '20112012',
42
- '2012': '20122013',
43
- '2013': '20132014',
44
- '2014': '20142015',
45
- '2015': '20152016',
46
- '2016': '20162017',
47
- '2017': '20172018',
48
- '2018': '20182019',
49
- '2019': '20192020',
50
- '2020': '20202021',
51
- '2021': '20212022',
52
- '2022': '20222023',
53
- '2023': '20232024',
54
- '2024': '20242025'}
39
+ CONVERT_SEASONS = {2007: 20072008,
40
+ 2008: 20082009,
41
+ 2009: 20092010,
42
+ 2010: 20102011,
43
+ 2011: 20112012,
44
+ 2012: 20122013,
45
+ 2013: 20132014,
46
+ 2014: 20142015,
47
+ 2015: 20152016,
48
+ 2016: 20162017,
49
+ 2017: 20172018,
50
+ 2018: 20182019,
51
+ 2019: 20192020,
52
+ 2020: 20202021,
53
+ 2021: 20212022,
54
+ 2022: 20222023,
55
+ 2023: 20232024,
56
+ 2024: 20242025}
55
57
 
56
58
  CONVERT_TEAM_ABBR = {'L.A':'LAK',
57
59
  'N.J':'NJD',
@@ -64,46 +66,25 @@ PER_SIXTY = ['Fi','xGi','Gi','A1','A2','P1','P','Si','OZF','NZF','DZF','FF','FA'
64
66
  #Some games in the API are specifically known to cause errors in scraping.
65
67
  #This list is updated as frequently as necessary
66
68
  KNOWN_PROBS = {
67
- '2007020011':'Missing shifts data for game between Chicago and Minnesota.',
68
- '2007021178':'Game between the Bruins and Sabres is missing data after the second period, for some reason.',
69
- '2008020259':'HTML data is completely missing for this game.',
70
- '2008020409':'HTML data is completely missing for this game.',
71
- '2008021077':'HTML data is completely missing for this game.',
72
- '2009020081':'HTML pbp for this game between Pittsburgh and Carolina is missing all but the period start and first faceoff events, for some reason.',
73
- '2009020658':'Missing shifts data for game between New York Islanders and Dallas.',
74
- '2009020885':'Missing shifts data for game between Sharks and Blue Jackets.',
75
- '2010020124':'Game between Capitals and Hurricanes is sporadically missing player on-ice data',
76
- '2012020018':'HTML events contain mislabeled events.',
77
- '2013020971':'On March 10th, 2014, Stars forward Rich Peverley suffered from a cardiac episode midgame and as a result, the remainder of the game was postponed. \nThe game resumed on April 9th, and the only goal scorer in the game, Blue Jackets forward Nathan Horton, did not appear in the resumed game due to injury. Interestingly, Horton would never play in the NHL again.',
78
- '2018021133':'Game between Lightning and Capitals has incorrectly labeled event teams (i.e. WSH TAKEAWAY - #71 CIRELLI (Cirelli is a Tampa Bay skater in this game)).',
79
- '2019020876':'Due to the frightening collapse of Blues defensemen Jay Bouwmeester, a game on February 2nd, 2020 between the Ducks and Blues was postponed. \nWhen the game resumed, Ducks defensemen Hampus Lindholm, who assisted on a goal in the inital game, did not play in the resumed match.'
69
+ 2007020011:'Missing shifts data for game between Chicago and Minnesota.',
70
+ 2007021178:'Game between the Bruins and Sabres is missing data after the second period, for some reason.',
71
+ 2008020259:'HTML data is completely missing for this game.',
72
+ 2008020409:'HTML data is completely missing for this game.',
73
+ 2008021077:'HTML data is completely missing for this game.',
74
+ 2009020081:'HTML pbp for this game between Pittsburgh and Carolina is missing all but the period start and first faceoff events, for some reason.',
75
+ 2009020658:'Missing shifts data for game between New York Islanders and Dallas.',
76
+ 2009020885:'Missing shifts data for game between Sharks and Blue Jackets.',
77
+ 2010020124:'Game between Capitals and Hurricanes is sporadically missing player on-ice data',
78
+ 2012020018:'HTML events contain mislabeled events.',
79
+ 2013020971:'On March 10th, 2014, Stars forward Rich Peverley suffered from a cardiac episode midgame and as a result, the remainder of the game was postponed. \nThe game resumed on April 9th, and the only goal scorer in the game, Blue Jackets forward Nathan Horton, did not appear in the resumed game due to injury. Interestingly, Horton would never play in the NHL again.',
80
+ 2018021133:'Game between Lightning and Capitals has incorrectly labeled event teams (i.e. WSH TAKEAWAY - #71 CIRELLI (Cirelli is a Tampa Bay skater in this game)).',
81
+ 2019020876:'Due to the frightening collapse of Blues defensemen Jay Bouwmeester, a game on February 2nd, 2020 between the Ducks and Blues was postponed. \nWhen the game resumed, Ducks defensemen Hampus Lindholm, who assisted on a goal in the inital game, did not play in the resumed match.'
80
82
  }
81
83
 
82
84
  SHOT_TYPES = ['wrist','deflected','tip-in','slap','backhand','snap','wrap-around','poke','bat','cradle','between-legs']
83
85
 
84
86
  NEW = 2024
85
87
 
86
- STANDINGS_END = {
87
- '20072008':'04-06',
88
- '20082009':'04-12',
89
- '20092010':'04-11',
90
- '20102011':'04-10',
91
- '20112012':'04-07',
92
- '20122013':'04-28',
93
- '20132014':'04-13',
94
- '20142015':'04-11',
95
- '20152016':'04-10',
96
- '20162017':'04-09',
97
- '20172018':'04-08',
98
- '20182019':'04-06',
99
- '20192020':'03-11',
100
- '20202021':'05-19',
101
- '20212022':'04-01',
102
- '20222023':'04-14',
103
- '20232024':'04-18',
104
- '20242025':'04-17'
105
- }
106
-
107
88
  EVENTS = ['faceoff','hit','giveaway','takeaway','blocked-shot','missed-shot','shot-on-goal','goal','penalty']
108
89
 
109
90
  DIR = os.path.dirname(os.path.realpath(__file__))
@@ -112,29 +93,41 @@ INFO_PATH = os.path.join(DIR,'tools\\teaminfo\\nhl_teaminfo.csv')
112
93
  DEFAULT_ROSTER = os.path.join(DIR,'tools\\rosters\\nhl_rosters.csv')
113
94
 
114
95
  ## SCRAPE FUNCTIONS ##
115
- def nhl_scrape_game(game_ids,split_shifts = False, remove = ['period-start','period-end','challenge','stoppage','shootout-complete','game-end'],verbose = False, sources = False, errors = False):
116
- #Given a set of game_ids (NHL API), return complete play-by-play information as requested
117
- # param 'game_ids' - NHL game ids (or list formatted as ['random', num_of_games, start_year, end_year])
118
- # param 'split_shifts' - boolean which splits pbp and shift events if true
119
- # param 'remove' - list of events to remove from final dataframe
120
- # param 'xg' - xG model to apply to pbp for aggregation
121
- # param 'verbose' - boolean which adds additional event info if true
122
- # param 'sources - boolean scraping the html and json sources to a master directory if true
123
- # param 'errors' - boolean returning game ids which did not scrape if true
124
-
96
+ def nhl_scrape_game(game_ids:list[int], split_shifts:bool = False, remove:list[str] = [], verbose:bool = False, sources:bool = False, errors:bool = False):
97
+ """
98
+ Given a set of game_ids (NHL API), return complete play-by-play information as requested.
99
+
100
+ Args:
101
+ game_ids (List[int] or ['random', int, int, int]):
102
+ List of NHL game IDs to scrape or use ['random', n, start_year, end_year] to fetch n random games.
103
+ split_shifts (bool, optional):
104
+ If True, returns a dict with separate 'pbp' and 'shifts' DataFrames. Default is False.
105
+ remove (List[str], optional):
106
+ List of event types to remove from the result. Default is an empty list.
107
+ verbose (bool, optional):
108
+ If True, generates extra event features (such as those required to calculate xG). Default is False.
109
+ sources (bool, optional):
110
+ If True, saves raw HTML, JSON, SHIFTS, and single-game full play-by-play to a separate folder in the working directory. Default is False.
111
+ errors (bool, optional):
112
+ If True, includes a list of game IDs that failed to scrape in the return. Default is False.
113
+
114
+ Returns:
115
+ pd.DataFrame:
116
+ If split_shifts is False, returns a single DataFrame of play-by-play data.
117
+ dict[str, pd.DataFrame]:
118
+ If split_shifts is True, returns a dictionary with keys:
119
+ - 'pbp': play-by-play events
120
+ - 'shifts': shift change events
121
+ - 'errors' (optional): list of game IDs that failed if errors=True
122
+ """
123
+
125
124
  pbps = []
126
125
  if game_ids[0] == 'random':
127
126
  #Randomize selection of game_ids
128
127
  #Some ids returned may be invalid (for example, 2020021300)
129
128
  num = game_ids[1]
130
- try:
131
- start = game_ids[2]
132
- except:
133
- start = 2007
134
- try:
135
- end = game_ids[3]
136
- except:
137
- end = (date.today().year)-1
129
+ start = game_ids[2] if len(game_ids) > 1 else 2007
130
+ end = game_ids[3] if len(game_ids) > 2 else (date.today().year)-1
138
131
 
139
132
  game_ids = []
140
133
  i = 0
@@ -161,13 +154,13 @@ def nhl_scrape_game(game_ids,split_shifts = False, remove = ['period-start','per
161
154
  error_ids = []
162
155
  prog = 0
163
156
  for game_id in game_ids:
164
- print("Scraping data from game " + str(game_id) + "...",end="")
157
+ print(f'Scraping data from game {game_id}...',end='')
165
158
  start = time.perf_counter()
166
159
 
167
160
  try:
168
161
  #Retrieve data
169
162
  info = get_game_info(game_id)
170
- data = combine_data(info, sources)
163
+ data = asyncio.run(combine_data(info, sources))
171
164
 
172
165
  #Append data to list
173
166
  pbps.append(data)
@@ -186,19 +179,19 @@ def nhl_scrape_game(game_ids,split_shifts = False, remove = ['period-start','per
186
179
  data.to_csv(f'{dirs}{info['game_id']}.csv',index=False)
187
180
 
188
181
  print(f" finished in {secs:.2f} seconds. {prog}/{len(game_ids)} ({(prog/len(game_ids))*100:.2f}%)")
189
- except:
182
+ except Exception as e:
190
183
  #Games such as the all-star game and pre-season games will incur this error
191
184
  #Other games have known problems
192
185
  if game_id in KNOWN_PROBS.keys():
193
186
  print(f"\nGame {game_id} has a known problem: {KNOWN_PROBS[game_id]}")
194
187
  else:
195
- print(f"\nUnable to scrape game {game_id}. Ensure the ID is properly inputted and formatted.")
188
+ print(f"\nUnable to scrape game {game_id}. Exception: {e}")
196
189
 
197
190
  #Track error
198
191
  error_ids.append(game_id)
199
192
 
200
193
  #Add all pbps together
201
- if len(pbps) == 0:
194
+ if not pbps:
202
195
  print("\rNo data returned.")
203
196
  return pd.DataFrame()
204
197
  df = pd.concat(pbps)
@@ -210,7 +203,7 @@ def nhl_scrape_game(game_ids,split_shifts = False, remove = ['period-start','per
210
203
  ""
211
204
 
212
205
  #Print final message
213
- if len(error_ids) > 0:
206
+ if error_ids:
214
207
  print(f'\rScrape of provided games finished.\nThe following games failed to scrape: {error_ids}')
215
208
  else:
216
209
  print('\rScrape of provided games finished.')
@@ -218,7 +211,7 @@ def nhl_scrape_game(game_ids,split_shifts = False, remove = ['period-start','per
218
211
  #Split pbp and shift events if necessary
219
212
  #Return: complete play-by-play with data removed or split as necessary
220
213
 
221
- if split_shifts == True:
214
+ if split_shifts:
222
215
  remove.append('change')
223
216
 
224
217
  #Return: dict with pbp and shifts seperated
@@ -242,22 +235,40 @@ def nhl_scrape_game(game_ids,split_shifts = False, remove = ['period-start','per
242
235
  else:
243
236
  return pbp
244
237
 
245
- def nhl_scrape_schedule(season,start = "09-01", end = "08-01"):
246
- #Given a season, return schedule data
247
- # param 'season' - NHL season to scrape
248
- # param 'start' - Start date in season
249
- # param 'end' - End date in season
238
+ def nhl_scrape_schedule(season:int, start:str = '', end:str = ''):
239
+ """
240
+ Given season and an optional date range, retrieve NHL schedule data.
241
+
242
+ Args:
243
+ season (int):
244
+ The NHL season formatted such as "20242025".
245
+ start (str, optional):
246
+ The date string (MM-DD) to start the schedule scrape at. Default is a blank string.
247
+ end (str, optional):
248
+ The date string (MM-DD) to end the schedule scrape at. Default is a blank string.
249
+
250
+ Returns:
251
+ pd.DataFrame:
252
+ A DataFrame containing the schedule data for the specified season and date range.
253
+ """
250
254
 
251
255
  api = "https://api-web.nhle.com/v1/schedule/"
252
256
 
253
- #Determine how to approach scraping; if month in season is after the new year the year must be adjusted
254
- new_year = ["01","02","03","04","05","06"]
255
- if start[:2] in new_year:
256
- start = str(int(season[:4])+1)+"-"+start
257
- end = str(season[:-4])+"-"+end
257
+ #If either start or end are blank then find start and endpoints for specified season
258
+ if start == '' or end == '':
259
+ season_data = rs.get('https://api.nhle.com/stats/rest/en/season').json()['data']
260
+ season_data = [s for s in season_data if s['id'] == season][0]
261
+ start = season_data['startDate'][0:10]
262
+ end = season_data['endDate'][0:10]
258
263
  else:
259
- start = str(season[:4])+"-"+start
260
- end = str(season[:-4])+"-"+end
264
+ #Determine how to approach scraping; if month in season is after the new year the year must be adjusted
265
+ new_year = ["01","02","03","04","05","06"]
266
+ if start[:2] in new_year:
267
+ start = f'{int(str(season)[:4])+1}-{start}'
268
+ end = f'{str(season)[:-4]}-{end}'
269
+ else:
270
+ start = f'{int(str(season)[:4])}-{start}'
271
+ end = f'{str(season)[:-4]}-{end}'
261
272
 
262
273
  form = '%Y-%m-%d'
263
274
 
@@ -274,9 +285,9 @@ def nhl_scrape_schedule(season,start = "09-01", end = "08-01"):
274
285
  for i in range(day):
275
286
  #For each day, call NHL api and retreive info on all games of selected game
276
287
  inc = start+timedelta(days=i)
277
- print("Scraping games on " + str(inc)[:10]+"...")
288
+ print(f'Scraping games on {str(inc)[:10]}...')
278
289
 
279
- get = rs.get(api+str(inc)[:10]).json()
290
+ get = rs.get(f'{api}{str(inc)[:10]}').json()
280
291
  gameWeek = pd.json_normalize(list(pd.json_normalize(get['gameWeek'])['games'])[0])
281
292
 
282
293
  #Return nothing if there's nothing
@@ -302,43 +313,81 @@ def nhl_scrape_schedule(season,start = "09-01", end = "08-01"):
302
313
  #Return: specificed schedule data
303
314
  return df
304
315
 
305
- def nhl_scrape_season(season,split_shifts = False, season_types = [2,3], remove = ['period-start','period-end','game-end','challenge','stoppage'], start = "09-01", end = "08-01", local=False, local_path = SCHEDULE_PATH, verbose = False, sources = False, errors = False):
306
- #Given season, scrape all play-by-play occuring within the season
307
- # param 'season' - NHL season to scrape
308
- # param 'split_shifts' - boolean which splits pbp and shift events if true
309
- # param 'remove' - list of events to remove from final dataframe
310
- # param 'start' - Start date in season
311
- # param 'end' - End date in season
312
- # param 'local' - boolean indicating whether to use local file to scrape game_ids
313
- # param 'local_path' - path of local file
314
- # param 'verbose' - boolean which adds additional event info if true
315
- # param 'sources - boolean scraping the html and json sources to a master directory if true
316
- # param 'errors' - boolean returning game ids which did not scrape if true
317
-
316
+ def nhl_scrape_season(season:int, split_shifts:bool = False, season_types:list[int] = [2,3], remove:list[str] = [], start:str = '', end:str = '', local:bool=False, local_path:str = SCHEDULE_PATH, verbose:bool = False, sources:bool = False, errors:bool = False):
317
+ """
318
+ Given season, scrape all play-by-play occuring within the season.
319
+
320
+ Args:
321
+ season (int):
322
+ The NHL season formatted such as "20242025".
323
+ split_shifts (bool, optional):
324
+ If True, returns a dict with separate 'pbp' and 'shifts' DataFrames. Default is False.
325
+ season_types (List[int], optional):
326
+ List of season_types to include in scraping process. Default is all regular season and playoff games which are 2 and 3 respectfully.
327
+ remove (List[str], optional):
328
+ List of event types to remove from the result. Default is an empty list.
329
+ start (str, optional):
330
+ The date string (MM-DD) to start the schedule scrape at. Default is a blank string.
331
+ end (str, optional):
332
+ The date string (MM-DD) to end the schedule scrape at. Default is a blank string.
333
+ local (bool, optional):
334
+ If True, use local file to retreive schedule data.
335
+ local_path (bool, optional):
336
+ If True, specifies the path with schedule data necessary to scrape a season's games (only relevant if local = True).
337
+ verbose (bool, optional):
338
+ If True, generates extra event features (such as those required to calculate xG). Default is False.
339
+ sources (bool, optional):
340
+ If True, saves raw HTML, JSON, SHIFTS, and single-game full play-by-play to a separate folder in the working directory. Default is False.
341
+ errors (bool, optional):
342
+ If True, includes a list of game IDs that failed to scrape in the return. Default is False.
343
+
344
+ Returns:
345
+ pd.DataFrame:
346
+ If split_shifts is False, returns a single DataFrame of play-by-play data.
347
+ dict[str, pd.DataFrame]:
348
+ If split_shifts is True, returns a dictionary with keys:
349
+ - 'pbp': play-by-play events
350
+ - 'shifts': shift change events
351
+ - 'errors' (optional): list of game IDs that failed if errors=True
352
+ """
353
+
318
354
  #Determine whether to use schedule data in repository or to scrape
319
355
  if local:
320
356
  load = pd.read_csv(local_path)
321
357
  load['date'] = pd.to_datetime(load['date'])
322
-
323
- start = f'{(season[0:4] if int(start[0:2])>=9 else season[4:8])}-{int(start[0:2])}-{int(start[3:5])}'
324
- end = f'{(season[0:4] if int(end[0:2])>=9 else season[4:8])}-{int(end[0:2])}-{int(end[3:5])}'
325
-
326
- load = load.loc[(load['season'].astype(str)==season)&
358
+
359
+ if start == '' or end == '':
360
+ season_data = rs.get('https://api.nhle.com/stats/rest/en/season').json()['data']
361
+ season_data = [s for s in season_data if s['id'] == season][0]
362
+ start = season_data['startDate'][0:10]
363
+ end = season_data['endDate'][0:10]
364
+
365
+ form = '%Y-%m-%d'
366
+
367
+ #Create datetime values from dates
368
+ start = datetime.strptime(start,form)
369
+ end = datetime.strptime(end,form)
370
+
371
+ else:
372
+ start = f'{(str(season)[0:4] if int(start[0:2])>=9 else str(season)[4:8])}-{start[0:2]}-{start[3:5]}'
373
+ end = f'{(str(season)[0:4] if int(end[0:2])>=9 else str(season)[4:8])}-{end[0:2]}-{end[3:5]}'
374
+
375
+ load = load.loc[(load['season']==season)&
327
376
  (load['season_type'].isin(season_types))&
328
377
  (load['date']>=start)&(load['date']<=end)]
329
378
 
330
- game_ids = list(load['id'].astype(str))
379
+ game_ids = load['id'].to_list()
331
380
  else:
332
381
  load = nhl_scrape_schedule(season,start,end)
333
- load = load.loc[(load['season'].astype(str)==season)&(load['season_type'].isin(season_types))]
334
- game_ids = list(load['id'].astype(str))
382
+ load = load.loc[(load['season']==season)&(load['season_type'].isin(season_types))]
383
+ game_ids = load['id'].to_list()
335
384
 
336
385
  #If no games found, terminate the process
337
386
  if not game_ids:
338
387
  print('No games found for dates in season...')
339
388
  return ""
340
389
 
341
- print(f"Scraping games from {season[0:4]}-{season[4:8]} season...")
390
+ print(f"Scraping games from {str(season)[0:4]}-{str(season)[4:8]} season...")
342
391
  start = time.perf_counter()
343
392
 
344
393
  #Perform scrape
@@ -354,11 +403,22 @@ def nhl_scrape_season(season,split_shifts = False, season_types = [2,3], remove
354
403
  #Return: Complete pbp and shifts data for specified season as well as dataframe of game_ids which failed to return data
355
404
  return data
356
405
 
357
- def nhl_scrape_seasons_info(seasons = []):
358
- #Returns info related to NHL seasons (by default, all seasons are included)
406
+ def nhl_scrape_seasons_info(seasons:list[int] = []):
407
+ """
408
+ Returns info related to NHL seasons (by default, all seasons are included)
409
+ Args:
410
+ seasons (List[int], optional):
411
+ The NHL season formatted such as "20242025".
412
+
413
+ Returns:
414
+ pd.DataFrame:
415
+ A DataFrame containing the information for requested seasons.
416
+ """
417
+
418
+ #
359
419
  # param 'season' - list of seasons to include
360
420
 
361
- print("Scraping info for seasons: " + str(seasons))
421
+ print(f'Scraping info for seasons: {seasons}')
362
422
  api = "https://api.nhle.com/stats/rest/en/season"
363
423
  info = "https://api-web.nhle.com/v1/standings-season"
364
424
  data = rs.get(api).json()['data']
@@ -374,12 +434,20 @@ def nhl_scrape_seasons_info(seasons = []):
374
434
  else:
375
435
  return df.sort_values(by=['id'])
376
436
 
377
- def nhl_scrape_standings(arg = "now", season_type = 2):
378
- #Returns standings
379
- # param 'arg' - by default, this is "now" returning active NHL standings. May also be a specific date formatted as YYYY-MM-DD, a season (scrapes the last standings date for the season) or a year (for playoffs).
380
- # param 'season_type' - by default, this scrapes the regular season standings. If set to 3, it returns the playoff bracket for the specified season
437
+ def nhl_scrape_standings(arg:str | int = "now", season_type:int = 2):
438
+ """
439
+ Returns standings or playoff bracket
440
+ Args:
441
+ arg (str or int, optional):
442
+ Date formatted as 'YYYY-MM-DD' to scrape standings, NHL season such as "20242025", or 'now' for current standings. Default is 'now'.
443
+ season_type (int, optional):
444
+ Part of season to scrape. If 3 (playoffs) then scrape the playoff bracket for the season implied by arg. When arg = 'now' this is ignored. Default is 2.
445
+
446
+ Returns:
447
+ pd.DataFrame:
448
+ A DataFrame containing the standings information (or playoff bracket).
449
+ """
381
450
 
382
- #arg param is ignored when set to "now" if season_type param is 3
383
451
  if season_type == 3:
384
452
  if arg == "now":
385
453
  arg = NEW
@@ -399,14 +467,28 @@ def nhl_scrape_standings(arg = "now", season_type = 2):
399
467
  else:
400
468
  print(f"Scraping standings for date: {arg}")
401
469
 
402
- api = f"https://api-web.nhle.com/v1/standings/{arg[4:8]}-{STANDINGS_END[arg]}"
470
+ season_data = rs.get('https://api.nhle.com/stats/rest/en/season').json()['data']
471
+ season_data = [s for s in season_data if s['id'] == arg][0]
472
+ end = season_data['regularSeasonEndDate'][0:10]
473
+
474
+ api = f"https://api-web.nhle.com/v1/standings/{end}"
403
475
  data = rs.get(api).json()['standings']
404
476
 
405
477
  return pd.json_normalize(data)
406
478
 
407
- def nhl_scrape_roster(season):
408
- #Given a nhl season, return rosters for all participating teams
409
- # param 'season' - NHL season to scrape
479
+ def nhl_scrape_roster(season: int):
480
+ """
481
+ Returns rosters for all teams in a given season.
482
+
483
+ Args:
484
+ season (int):
485
+ The NHL season formatted such as "20242025".
486
+
487
+ Returns:
488
+ pd.DataFrame:
489
+ A DataFrame containing the rosters for all teams in the specified season.
490
+ """
491
+
410
492
  print("Scrpaing rosters for the "+ season + "season...")
411
493
  teaminfo = pd.read_csv(info_path)
412
494
 
@@ -435,8 +517,18 @@ def nhl_scrape_roster(season):
435
517
 
436
518
  return pd.concat(rosts)
437
519
 
438
- def nhl_scrape_prospects(team):
439
- #Given team abbreviation, retreive current team prospects
520
+ def nhl_scrape_prospects(team:str):
521
+ """
522
+ Returns prospects for specified team
523
+
524
+ Args:
525
+ team (str):
526
+ Three character team abbreviation such as 'BOS'
527
+
528
+ Returns:
529
+ pd.DataFrame:
530
+ A DataFrame containing the prospect data for the specified team.
531
+ """
440
532
 
441
533
  api = f'https://api-web.nhle.com/v1/prospects/{team}'
442
534
 
@@ -452,10 +544,20 @@ def nhl_scrape_prospects(team):
452
544
  #Return: team prospects
453
545
  return prospects
454
546
 
455
- def nhl_scrape_team_info(country = False):
456
- #Given option to return franchise or country, return team information
547
+ def nhl_scrape_team_info(country:bool = False):
548
+ """
549
+ Returns team or country information from the NHL API.
550
+
551
+ Args:
552
+ country (bool, optional):
553
+ If True, returns country information instead of NHL team information.
457
554
 
458
- print('Scraping team information...')
555
+ Returns:
556
+ pd.DataFrame:
557
+ A DataFrame containing team or country information from the NHL API.
558
+ """
559
+
560
+ print(f'Scraping {'country' if country else 'team'} information...')
459
561
  api = f'https://api.nhle.com/stats/rest/en/{'country' if country else 'team'}'
460
562
 
461
563
  data = pd.json_normalize(rs.get(api).json()['data'])
@@ -467,8 +569,19 @@ def nhl_scrape_team_info(country = False):
467
569
 
468
570
  return data.sort_values(by=(['country3Code','countryCode','iocCode','countryName'] if country else ['fullName','triCode','id']))
469
571
 
470
- def nhl_scrape_player_data(player_ids):
471
- #Given player id, return player information
572
+ def nhl_scrape_player_data(player_ids:list[int]):
573
+ """
574
+ Returns player data for specified players.
575
+
576
+ Args:
577
+ player_ids (list[int]):
578
+ List of NHL API player IDs to retrieve information for.
579
+
580
+ Returns:
581
+ pd.DataFrame:
582
+ A DataFrame containing player data for specified players.
583
+ """
584
+
472
585
  infos = []
473
586
  for player_id in player_ids:
474
587
  player_id = int(player_id)
@@ -489,15 +602,28 @@ def nhl_scrape_player_data(player_ids):
489
602
  else:
490
603
  return pd.DataFrame()
491
604
 
492
- def nhl_scrape_draft_rankings(arg = 'now', category = ''):
493
- #Given url argument for timeframe and prospect category, return draft rankings
494
- #Category 1 is North American Skaters
495
- #Category 2 is International Skaters
496
- #Category 3 is North American Goalie
497
- #Category 4 is International Goalie
605
+ def nhl_scrape_draft_rankings(arg:str = 'now', category:int = 0):
606
+ """
607
+ Returns draft rankings
608
+ Args:
609
+ arg (str, optional):
610
+ Date formatted as 'YYYY-MM-DD' to scrape draft rankings for specific date or 'now' for current draft rankings. Default is 'now'.
611
+ category (int, optional):
612
+ Category number for prospects. When arg = 'now' this does not apply.
613
+
614
+ - Category 1 is North American Skaters.
615
+ - Category 2 is International Skaters.
616
+ - Category 3 is North American Goalie.
617
+ - Category 4 is International Goalie
618
+
619
+ Default is 0 (all prospects).
620
+ Returns:
621
+ pd.DataFrame:
622
+ A DataFrame containing draft rankings.
623
+ """
498
624
 
499
625
  #Player category only applies when requesting a specific season
500
- api = f"https://api-web.nhle.com/v1/draft/rankings/{arg}/{category}" if category != "" else f"https://api-web.nhle.com/v1/draft/rankings/{arg}"
626
+ api = f"https://api-web.nhle.com/v1/draft/rankings/{arg}/{category}" if category > 0 else f"https://api-web.nhle.com/v1/draft/rankings/{arg}"
501
627
  data = pd.json_normalize(rs.get(api).json()['rankings'])
502
628
 
503
629
  #Add player name columns
@@ -506,10 +632,16 @@ def nhl_scrape_draft_rankings(arg = 'now', category = ''):
506
632
  #Return: prospect rankings
507
633
  return data
508
634
 
509
- def nhl_apply_xG(pbp):
510
- #Given play-by-play data, return this data with xG-related columns
511
-
512
- #param 'pbp' - play-by-play data
635
+ def nhl_apply_xG(pbp: pd.DataFrame):
636
+ """
637
+ Given play-by-play data, return this data with xG-related columns
638
+ Args:
639
+ pbp (pd.DataFrame):
640
+ A DataFrame containing play-by-play data generated within the WBSA Hockey package.
641
+ Returns:
642
+ pd.DataFrame:
643
+ A DataFrame containing input play-by-play data with xG column.
644
+ """
513
645
 
514
646
  print(f'Applying WSBA xG to model with seasons: {pbp['season'].drop_duplicates().to_list()}')
515
647
 
@@ -518,7 +650,7 @@ def nhl_apply_xG(pbp):
518
650
 
519
651
  return pbp
520
652
 
521
- def nhl_shooting_impacts(agg,type):
653
+ def shooting_impacts(agg, type):
522
654
  #Given stats table generated from the nhl_calculate_stats function, return table with shot impacts
523
655
  #Only 5v5 is supported as of now
524
656
 
@@ -868,7 +1000,7 @@ def nhl_shooting_impacts(agg,type):
868
1000
  #Return: skater stats with shooting impacts
869
1001
  return df
870
1002
 
871
- def nhl_calculate_stats(pbp,type,season_types,game_strength,split_game=False,roster_path=DEFAULT_ROSTER,shot_impact=False):
1003
+ def nhl_calculate_stats(pbp:pd.DataFrame, type:Literal['skater','goalie','team'], season_types:list[int], game_strength: Union[Literal['all'], list[str]], split_game:bool = False, roster_path:str = DEFAULT_ROSTER, shot_impact:bool = False):
872
1004
  #Given play-by-play, seasonal information, game_strength, rosters, and xG model, return aggregated stats
873
1005
  # param 'pbp' - play-by-play dataframe
874
1006
  # param 'type' - type of stats to calculate ('skater', 'goalie', or 'team')
@@ -879,6 +1011,33 @@ def nhl_calculate_stats(pbp,type,season_types,game_strength,split_game=False,ros
879
1011
  # param 'roster_path' - path to roster file
880
1012
  # param 'shot_impact' - boolean determining if the shot impact model will be applied to the dataset
881
1013
 
1014
+ """
1015
+ Given play-by-play data, seasonal information, game strength, rosters, and an xG model,
1016
+ return aggregated statistics at the skater, goalie, or team level.
1017
+
1018
+ Args:
1019
+ pbp (pd.DataFrame):
1020
+ A DataFrame containing play-by-play event data.
1021
+ type (Literal['skater', 'goalie', 'team']):
1022
+ Type of statistics to calculate. Must be one of 'skater', 'goalie', or 'team'.
1023
+ season (int):
1024
+ The NHL season formatted such as "20242025".
1025
+ season_types (List[int], optional):
1026
+ List of season_types to include in scraping process. Default is all regular season and playoff games which are 2 and 3 respectfully.
1027
+ game_strength (str or list[str]):
1028
+ List of game strength states to include (e.g., ['5v5','5v4','4v5']).
1029
+ split_game (bool, optional):
1030
+ If True, aggregates stats separately for each game; otherwise, stats are aggregated across all games. Default is False.
1031
+ roster_path (str, optional):
1032
+ File path to the roster data used for mapping players and teams.
1033
+ shot_impact (bool, optional):
1034
+ If True, applies shot impact metrics to the stats DataFrame. Default is False.
1035
+
1036
+ Returns:
1037
+ pd.DataFrame:
1038
+ A DataFrame containing the aggregated statistics according to the selected parameters.
1039
+ """
1040
+
882
1041
  print(f"Calculating statistics for all games in the provided play-by-play data at {game_strength} for {type}s...\nSeasons included: {pbp['season'].drop_duplicates().to_list()}...")
883
1042
  start = time.perf_counter()
884
1043
 
@@ -970,7 +1129,7 @@ def nhl_calculate_stats(pbp,type,season_types,game_strength,split_game=False,ros
970
1129
 
971
1130
  #Apply shot impacts if necessary
972
1131
  if shot_impact:
973
- complete = nhl_shooting_impacts(complete,'goalie')
1132
+ complete = shooting_impacts(complete,'goalie')
974
1133
 
975
1134
  end = time.perf_counter()
976
1135
  length = end-start
@@ -1014,7 +1173,7 @@ def nhl_calculate_stats(pbp,type,season_types,game_strength,split_game=False,ros
1014
1173
  ]+[f'{stat}/60' for stat in PER_SIXTY[11:len(PER_SIXTY)]]]
1015
1174
  #Apply shot impacts if necessary
1016
1175
  if shot_impact:
1017
- complete = nhl_shooting_impacts(complete,'team')
1176
+ complete = shooting_impacts(complete,'team')
1018
1177
 
1019
1178
  end = time.perf_counter()
1020
1179
  length = end-start
@@ -1117,7 +1276,7 @@ def nhl_calculate_stats(pbp,type,season_types,game_strength,split_game=False,ros
1117
1276
 
1118
1277
  #Apply shot impacts if necessary (Note: this will remove skaters with fewer than 150 minutes of TOI due to the shot impact TOI rule)
1119
1278
  if shot_impact:
1120
- complete = nhl_shooting_impacts(complete,'skater')
1279
+ complete = shooting_impacts(complete,'skater')
1121
1280
 
1122
1281
  end = time.perf_counter()
1123
1282
  length = end-start
@@ -1125,16 +1284,34 @@ def nhl_calculate_stats(pbp,type,season_types,game_strength,split_game=False,ros
1125
1284
 
1126
1285
  return complete
1127
1286
 
1128
- def nhl_plot_skaters_shots(pbp,skater_dict,strengths,marker_dict=event_markers,onice = 'indv',title = True,legend=False):
1129
- #Returns dict of plots for specified skaters
1130
- # param 'pbp' - pbp to plot data
1131
- # param 'skater_dict' - skaters to plot shots for (format: {'Patrice Bergeron':['20242025','BOS']})
1132
- # param 'strengths' - strengths to include in plotting
1133
- # param 'marker_dict' - dict with markers to use for events
1134
- # param 'onice' - can set which shots to include in plotting for the specified skater ('indv', 'for', 'against')
1135
- # param 'title' - bool including title when true
1136
- # param 'legend' - bool which includes legend if true
1137
- # param 'xg' - xG model to apply to pbp for plotting
1287
+ def nhl_plot_skaters_shots(pbp:pd.DataFrame, skater_dict:dict, strengths:Union[Literal['all'], list[str]], marker_dict:dict = event_markers, onice:Literal['indv','for','against'] = ['indv'], title:bool = True, legend:bool = False):
1288
+ """
1289
+ Return a dictionary of shot plots for the specified skaters.
1290
+
1291
+ Args:
1292
+ pbp (pd.DataFrame):
1293
+ A DataFrame containing play-by-play event data to be visualized.
1294
+ skater_dict (dict[str, list[str]]):
1295
+ Dictionary of skaters to plot, where each key is a player name and the value is a list
1296
+ with season and team info (e.g., {'Patrice Bergeron': ['20242025', 'BOS']}).
1297
+ strengths (str or list[str]):
1298
+ List of game strength states to include (e.g., ['5v5','5v4','4v5']).
1299
+ marker_dict (dict[str, dict], optional):
1300
+ Dictionary of event types mapped to marker styles used in plotting.
1301
+ onice (Literal['indv', 'for', 'against'], optional):
1302
+ Determines which shot events to include for the player:
1303
+ - 'indv': only the player's own shots,
1304
+ - 'for': shots taken by the player's team while they are on ice,
1305
+ - 'against': shots taken by the opposing team while the player is on ice.
1306
+ title (bool, optional):
1307
+ Whether to include a plot title.
1308
+ legend (bool, optional):
1309
+ Whether to include a legend on the plots.
1310
+
1311
+ Returns:
1312
+ dict[str, matplotlib.figure.Figure]:
1313
+ A dictionary mapping each skater’s name to their corresponding matplotlib shot plot figure.
1314
+ """
1138
1315
 
1139
1316
  print(f'Plotting the following skater shots: {skater_dict}...')
1140
1317
 
@@ -1149,15 +1326,28 @@ def nhl_plot_skaters_shots(pbp,skater_dict,strengths,marker_dict=event_markers,o
1149
1326
  #Return: list of plotted skater shot charts
1150
1327
  return skater_plots
1151
1328
 
1152
- def nhl_plot_games(pbp,events,strengths,game_ids='all',marker_dict=event_markers,team_colors={'away':'primary','home':'primary'},legend=False):
1153
- #Returns dict of plots for specified games
1154
- # param 'pbp' - pbp to plot data
1155
- # param 'events' - type of events to plot
1156
- # param 'strengths' - strengths to include in plotting
1157
- # param 'game_ids' - games to plot (list if not set to 'all')
1158
- # param 'marker_dict' - dict with colors to use for events
1159
- # param 'legend' - bool which includes legend if true
1160
- # param 'xg' - xG model to apply to pbp for plotting
1329
+ def nhl_plot_games(pbp:pd.DataFrame, events:list[str], strengths:Union[Literal['all'], list[str]], game_ids: Union[Literal['all'], list[int]] = 'all', marker_dict:dict = event_markers, team_colors:dict = {'away':'primary','home':'primary'}, legend:bool =False):
1330
+ """
1331
+ Returns a dictionary of event plots for the specified games.
1332
+
1333
+ Args:
1334
+ pbp (pd.DataFrame):
1335
+ A DataFrame containing play-by-play event data.
1336
+ events (list[str]):
1337
+ List of event types to include in the plot (e.g., ['shot-on-goal', 'goal']).
1338
+ strengths (str or list[str]):
1339
+ List of game strength states to include (e.g., ['5v5','5v4','4v5']).
1340
+ game_ids (str or list[int]):
1341
+ List of game IDs to plot. If set to 'all', plots will be generated for all games in the DataFrame.
1342
+ marker_dict (dict[str, dict]):
1343
+ Dictionary mapping event types to marker styles and/or colors used in plotting.
1344
+ legend (bool):
1345
+ Whether to include a legend on the plots.
1346
+
1347
+ Returns:
1348
+ dict[int, matplotlib.figure.Figure]:
1349
+ A dictionary mapping each game ID to its corresponding matplotlib event plot figure.
1350
+ """
1161
1351
 
1162
1352
  #Find games to scrape
1163
1353
  if game_ids == 'all':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wsba_hockey
3
- Version: 1.1.6
3
+ Version: 1.1.8
4
4
  Summary: WeakSide Breakout's complete Python package of access to hockey data, primairly including the scraping of National Hockey League schedule, play-by-play, and shifts information.
5
5
  Author-email: Owen Singh <owenbksingh@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/owensingh38/wsba_hockey/
@@ -1,8 +1,8 @@
1
- wsba_hockey/__init__.py,sha256=yfr8z5PA503iaIQv30ngancwT_WnsuK-tZETKlHcI0M,377
1
+ wsba_hockey/__init__.py,sha256=qye0rq22KeaUzBPH__pqjBA_igwsmHemOAbaY_G2tNY,356
2
2
  wsba_hockey/data_pipelines.py,sha256=SITapG3nbea6-_EsXujMW2JBQxtRaQ33XMcE6ohn2Ko,10853
3
- wsba_hockey/workspace.py,sha256=MwuyqyLW0dHNa06WEm60RkvbFoCn8LBXhnki66V-ttY,954
4
- wsba_hockey/wsba_main.py,sha256=Ucies8d27gWtzf8xprnu7hEcqGGHvOza8HCE0O80X-s,54031
5
- wsba_hockey/api/api/index.py,sha256=tABWg5cYCY-fPaNJ6W_bMJKEYrjn93YGy84VlkHzIXA,6853
3
+ wsba_hockey/workspace.py,sha256=DrOm6DYPEmk9FEzAxpFgn_8zd0wNTca7muED0eDMdlc,968
4
+ wsba_hockey/wsba_main.py,sha256=7RJtcpqsAuSWEtxs1CgZXkfZU3GR1MJc0_HG_S7gb08,61959
5
+ wsba_hockey/api/api/index.py,sha256=r2keq105Ve8V0JAsSZMIPs9geVHX2Fuxyi4MqnzCt48,6537
6
6
  wsba_hockey/evidence/weakside-breakout/node_modules/duckdb/vendor.py,sha256=lmu0TB0rIYkAuV9-csFJgW-1hJojso_-EZpEoorUUKM,4949
7
7
  wsba_hockey/evidence/weakside-breakout/node_modules/flatted/python/flatted.py,sha256=ke8FuEflns-WlphCcQ9CC0qJqWqX3zEEuak74o6rgE8,3879
8
8
  wsba_hockey/evidence/weakside-breakout/node_modules/flatted/python/test.py,sha256=uTOn6HJd7KeY_PTRvvufv60dmvON3KWp3nnqACj8IlA,2129
@@ -134,15 +134,15 @@ wsba_hockey/flask/app.py,sha256=J51iA65h9xyJfLgdH0h2sVSbfIR7xgGd2Oy8bJsmpAk,1873
134
134
  wsba_hockey/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
135
  wsba_hockey/tools/agg.py,sha256=OkIYd-ApvGVYe2JJLOI21jnDIN5LH8nkeH7eo0reWFI,23364
136
136
  wsba_hockey/tools/plotting.py,sha256=81hBaM7tcwUNB4-tovPn7QreOUz6B2NuI_SR4-djVSk,6029
137
- wsba_hockey/tools/scraping.py,sha256=h6C016U0qmNQpHWMh7Xvn3ud57zKzRbRQ06Odl-rC_I,52573
137
+ wsba_hockey/tools/scraping.py,sha256=-sv29886AWAMhhpJ14282WTolBZni8eXBvj4OtNVY-U,52863
138
138
  wsba_hockey/tools/xg_model.py,sha256=nOr_2RBijLgPmJ0TTs4wbSsORYmRqWCKRjLKDm7sAhI,18342
139
139
  wsba_hockey/tools/archive/old_scraping.py,sha256=hEjMI1RtfeZnf0RBiJFI38oXkLZ3WofeH5xqcF4pzgM,49585
140
140
  wsba_hockey/tools/utils/__init__.py,sha256=vccXhOtzARoR99fmEWU1OEI3qCIdQ9Z42AlRA_BUhrs,114
141
141
  wsba_hockey/tools/utils/config.py,sha256=D3Uk05-YTyrhfReMTTLfNI3HN_rON2uo_CDE9oER3Lg,351
142
142
  wsba_hockey/tools/utils/save_pages.py,sha256=CsyL_0n-b-4pJoUauwU3HpnCO6n69-RlBMJQBd_qGDc,4979
143
143
  wsba_hockey/tools/utils/shared.py,sha256=dH_JwZfia5fib8rksy5sW-mBp0pluBPvw37Vdr8Kap0,14211
144
- wsba_hockey-1.1.6.dist-info/licenses/LICENSE,sha256=Nr_Um1Pd5FQJTWWgm7maZArdtYMbDhzXYSwyJIZDGik,1114
145
- wsba_hockey-1.1.6.dist-info/METADATA,sha256=2CLs8qTA1iS8P7ToF4My86KkMRrt5zYoX9ynbQTS4zk,3566
146
- wsba_hockey-1.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
- wsba_hockey-1.1.6.dist-info/top_level.txt,sha256=acU7s3x-RZC1zGiqCOmO0g267iqCg34lzIfdmYxxGmQ,12
148
- wsba_hockey-1.1.6.dist-info/RECORD,,
144
+ wsba_hockey-1.1.8.dist-info/licenses/LICENSE,sha256=Nr_Um1Pd5FQJTWWgm7maZArdtYMbDhzXYSwyJIZDGik,1114
145
+ wsba_hockey-1.1.8.dist-info/METADATA,sha256=qacIIM92WbKqxYPI96wimlHYw2NLxCMZkmwTQHnO-5M,3566
146
+ wsba_hockey-1.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
147
+ wsba_hockey-1.1.8.dist-info/top_level.txt,sha256=acU7s3x-RZC1zGiqCOmO0g267iqCg34lzIfdmYxxGmQ,12
148
+ wsba_hockey-1.1.8.dist-info/RECORD,,