mlbrecaps 0.0.1__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.
mlbrecaps/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .clip import Clip
2
+ from .clips import Clips
3
+ from .date import Date
4
+ from .date_range import DateRange
5
+ from .date_generator import DateGenerator
6
+ from .game import Game
7
+ from .game_generator import GameGenerator
8
+ from .play import Play
9
+ from .player import Player
10
+ from .team import Team
11
+ from .scripts import get_highlights
12
+
13
+ __all__ = ["Clip", "Clips", "Date", "DateRange", "DateGenerator", "Game", "Play", "Player", "GameGenerator", "Team", "scripts"]
mlbrecaps/__main__.py ADDED
@@ -0,0 +1,21 @@
1
+ from .scripts import get_highlights
2
+
3
+ from .date import Date
4
+ from .team import Team
5
+
6
+ if __name__ == "__main__":
7
+ # Get teams for the search
8
+ team = Team(input("Input team name: "))
9
+ date = Date(input("Date MM/DD/YYYY: "))
10
+ file_path = input("Downloads filepath: ")
11
+
12
+ if not file_path.endswith("/"):
13
+ file_path += "/"
14
+
15
+ game_clips = get_highlights(team, date)
16
+
17
+ for index, clips in enumerate(game_clips):
18
+ # Download all highlight clips
19
+ print(f"Game {index + 1}: {clips.plays[0].game}")
20
+
21
+ clips.download(f"{file_path}{index}", verbose=True)
mlbrecaps/clip.py ADDED
@@ -0,0 +1,103 @@
1
+ from bs4 import BeautifulSoup
2
+
3
+ import requests
4
+ import json
5
+ import subprocess
6
+
7
+ from .play import Play
8
+
9
+ class Clip():
10
+
11
+ def __init__(self, play: Play, broadcast_type: str | None=None):
12
+ if not isinstance(play, Play):
13
+ raise ValueError("Play must be a Play object")
14
+
15
+ self._play: Play = play
16
+
17
+ match broadcast_type: # Enforce broad_type types
18
+ case "HOME" | "AWAY" | None:
19
+ self.broadcast_type: str | None = broadcast_type
20
+ case _:
21
+ raise ValueError("BroadcastType must be None, \"HOME\", or \"AWAY\"")
22
+
23
+ self._clip_url: str = self.__generate()
24
+
25
+ @property
26
+ def clip_url(self) -> str:
27
+ return self._clip_url
28
+
29
+ def __str__(self) -> str:
30
+ return self.clip_url
31
+
32
+ @property
33
+ def play(self) -> Play:
34
+ return self._play
35
+
36
+ # gets the url of the clip to be downloaded from the savant clip
37
+ def __get_url(self, site_url: str) -> str:
38
+ # Get the savant site
39
+ site: requests.Response = requests.get(site_url)
40
+
41
+ # Find the video element of the savant clip, find the source url of the clip
42
+ soup= BeautifulSoup(site.text, features="lxml")
43
+ video_obj = soup.find("video", id="sporty")
44
+
45
+ if not video_obj:
46
+ return ""
47
+
48
+ source = video_obj.find('source')
49
+ clip_url: str = source.get('src')
50
+
51
+ # Return the source url of the clip so it can be downloaded later
52
+ return clip_url
53
+
54
+
55
+ # finds the savant clip based on the given at-bat information
56
+ # row must be a pandas dataframe row
57
+ def __generate(self) -> str:
58
+ # load the given game's json file
59
+ game_json = self._play.game.game_json
60
+
61
+ # find the broadcast type so it's always corresponding
62
+ # to the given batter's home team's broadcast
63
+ if self.broadcast_type:
64
+ broadcast_type = self.broadcast_type
65
+ elif self._play.inning_topbot == "TOP":
66
+ broadcast_type = "AWAY"
67
+ else:
68
+ broadcast_type = "HOME"
69
+
70
+ # with the play id find the url for the savant clip
71
+ site_url = f"https://baseballsavant.mlb.com/sporty-videos?playId={self._play.play_id}&videoType={broadcast_type}"
72
+ clip_url = self.__get_url(site_url)
73
+
74
+ # if the clip is alright return it
75
+ if clip_url != "":
76
+ return clip_url
77
+
78
+ # if the clip is screwed up then it was a national tv game
79
+ # return the correct national tv clip url
80
+ site_url = f"https://baseballsavant.mlb.com/sporty-videos?playId={self._play.play_id}&videoType=NETWORK"
81
+ clip_url = self.__get_url(site_url)
82
+
83
+ return clip_url
84
+
85
+ def download(self, path: str, verbose: bool =False) -> None:
86
+ # create response object
87
+ # if a time out happens, try five more times before crashing the entire program
88
+ for z in range(5):
89
+ try:
90
+ r = requests.get(self._clip_url, stream=True, timeout=60)
91
+ break
92
+ except requests.exceptions.Timeout:
93
+ print(f'Timeout has been raised. Link: {self._clip_url}')
94
+
95
+ # download the file to the specific location
96
+ # honestly copied and pasted code, can't say much else
97
+ with open(path, 'wb') as f:
98
+ for chunk in r.iter_content(chunk_size = 1024*1024):
99
+ if chunk:
100
+ f.write(chunk)
101
+
102
+ if verbose:
103
+ print(f"Successfully downloaded: {path}")
mlbrecaps/clips.py ADDED
@@ -0,0 +1,51 @@
1
+ from typing import List, Literal
2
+ from functools import singledispatchmethod
3
+
4
+ from .play import Play
5
+ from .game import Game
6
+ from .clip import Clip
7
+ from .utils import async_run
8
+
9
+ class Clips():
10
+ def __init__(self, plays: List[Play] | Play, broadcast_type: Literal["HOME", "AWAY"] | None=None):
11
+ self.__set_plays(plays)
12
+
13
+ match broadcast_type: # Enforce broad_type types
14
+ case "HOME" | "AWAY" | None:
15
+ self._broadcast_type: str | None = broadcast_type
16
+ case _:
17
+ raise ValueError("BroadcastType must be None, \"HOME\", or \"AWAY\"")
18
+
19
+ # Generate clip objects for each play passed
20
+ self._clips = async_run(Clip, self._plays, self._broadcast_type)
21
+
22
+ @singledispatchmethod
23
+ def __set_plays(self, plays) -> None:
24
+ raise ValueError("A Play or list of Play objects must be passed")
25
+
26
+ @__set_plays.register(list)
27
+ def _(self, plays: List[Play]) -> None:
28
+ if len(plays) == 0 or not isinstance(plays[0], Play):
29
+ raise ValueError("A list of Play objects must be passed")
30
+
31
+ self._plays: List[Play] = plays
32
+
33
+ @__set_plays.register(Play)
34
+ def _(self, plays: Play) -> None:
35
+ self._plays = [plays]
36
+
37
+ @property
38
+ def plays(self) -> List[Play]:
39
+ return self._plays
40
+
41
+ @property
42
+ def clips(self) -> List[Clip]:
43
+ return self._clips
44
+
45
+ @property
46
+ def broadcast_type(self) -> str:
47
+ return self._broadcast_type
48
+
49
+ def download(self, path: str, verbose: bool=False) -> None:
50
+ paths = [f"{path}{index:03d}.mp4" for index in range(len(self._clips))]
51
+ async_run(Clip.download, self._clips, paths, verbose)
@@ -0,0 +1,33 @@
1
+ Team ID,Code,File Code,Abbreviation,Name,Full Name,Brief Name
2
+ 108,ana,ana,LAA,LA Angels,Los Angeles Angels,Angels
3
+ 109,ari,ari,ARI,Arizona,Arizona Diamondbacks,D-backs
4
+ 110,bal,bal,BAL,Baltimore,Baltimore Orioles,Orioles
5
+ 111,bos,bos,BOS,Boston,Boston Red Sox,Red Sox
6
+ 112,chn,chc,CHC,Chi Cubs,Chicago Cubs,Cubs
7
+ 113,cin,cin,CIN,Cincinnati,Cincinnati Reds,Reds
8
+ 114,cle,cle,CLE,Cleveland,Cleveland Guardians,Guardians
9
+ 115,col,col,COL,Colorado,Colorado Rockies,Rockies
10
+ 116,det,det,DET,Detroit,Detroit Tigers,Tigers
11
+ 117,hou,hou,HOU,Houston,Houston Astros,Astros
12
+ 118,kca,kc,KC,Kansas City,Kansas City Royals,Royals
13
+ 119,lan,la,LAD,LA Dodgers,Los Angeles Dodgers,Dodgers
14
+ 120,was,was,WSH,Washington,Washington Nationals,Nationals
15
+ 121,nyn,nym,NYM,NY Mets,New York Mets,Mets
16
+ 133,oak,oak,OAK,Oakland,Oakland Athletics,Athletics
17
+ 134,pit,pit,PIT,Pittsburgh,Pittsburgh Pirates,Pirates
18
+ 135,sdn,sd,SD,San Diego,San Diego Padres,Padres
19
+ 136,sea,sea,SEA,Seattle,Seattle Mariners,Mariners
20
+ 137,sfn,sf,SF,San Francisco,San Francisco Giants,Giants
21
+ 138,sln,stl,STL,St. Louis,St. Louis Cardinals,Cardinals
22
+ 139,tba,tb,TB,Tampa Bay,Tampa Bay Rays,Rays
23
+ 140,tex,tex,TEX,Texas,Texas Rangers,Rangers
24
+ 141,tor,tor,TOR,Toronto,Toronto Blue Jays,Blue Jays
25
+ 142,min,min,MIN,Minnesota,Minnesota Twins,Twins
26
+ 143,phi,phi,PHI,Philadelphia,Philadelphia Phillies,Phillies
27
+ 144,atl,atl,ATL,Atlanta,Atlanta Braves,Braves
28
+ 145,cha,cws,CWS,Chi White Sox,Chicago White Sox,White Sox
29
+ 146,mia,mia,MIA,Miami,Miami Marlins,Marlins
30
+ 147,nya,nyy,NYY,NY Yankees,New York Yankees,Yankees
31
+ 158,mil,mil,MIL,Milwaukee,Milwaukee Brewers,Brewers
32
+ 159,aas,al,AL,AL All-Stars,American League All-Stars,AL All-Stars
33
+ 160,nas,nl,NL,NL All-Stars,National League All-Stars,NL All-Stars
mlbrecaps/date.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import total_ordering, singledispatchmethod
4
+ from datetime import datetime, timedelta
5
+
6
+ from typing import Optional
7
+
8
+ @total_ordering
9
+ class Date():
10
+
11
+ def __init__(self, date: datetime | str | int, day: Optional[int]=None, year: Optional[int]=None):
12
+ if day and year:
13
+ self.__set_date(date, day, year)
14
+ else:
15
+ self.__set_date(date)
16
+
17
+ @singledispatchmethod
18
+ def __set_date(self, date: datetime | str | int, day: Optional[int]=None, year: Optional[int]=None):
19
+ raise ValueError("Invalid Date")
20
+
21
+ @__set_date.register(datetime)
22
+ def _(self, date: datetime):
23
+ self._date = date
24
+
25
+ @__set_date.register(str)
26
+ def _(self, date: str):
27
+ self._date = datetime.strptime(date, "%m/%d/%Y")
28
+
29
+ @__set_date.register(int)
30
+ def _(self, date: int, day: int, year: int):
31
+ self._date: datetime = datetime(year, date, day)
32
+
33
+ @property
34
+ def date(self) -> datetime:
35
+ return self._date
36
+
37
+ def to_formatted_string(self) -> str:
38
+ return self._date.strftime("%Y-%m-%d")
39
+
40
+ @property
41
+ def year(self) -> int:
42
+ return self._date.year
43
+
44
+ @property
45
+ def month(self) -> int:
46
+ return self._date.month
47
+
48
+ @property
49
+ def day(self) -> int:
50
+ return self._date.day
51
+
52
+ def next(self, increment: float=1) -> None:
53
+ self._date += timedelta(days=increment)
54
+
55
+ def prev(self, increment: float=1) -> None:
56
+ self._date -= timedelta(days=increment)
57
+
58
+ def copy(self) -> Date:
59
+ month = self._date.month
60
+ day = self._date.day
61
+ year = self._date.year
62
+
63
+ return type(self)(datetime(year, month, day))
64
+
65
+ def __eq__(self, other: object) -> bool:
66
+ if not isinstance(other, Date):
67
+ return False
68
+
69
+ return self._date == other.date
70
+
71
+ def __lt__(self, other: object) -> bool:
72
+ if not isinstance(other, Date):
73
+ return False
74
+
75
+ return self._date < other.date
76
+
77
+ def __str__(self) -> str:
78
+ return self._date.strftime("%Y-%m-%d")
@@ -0,0 +1,53 @@
1
+ from datetime import datetime
2
+ from copy import copy
3
+
4
+ from .date import Date
5
+ from .date_range import DateRange
6
+
7
+ class DateGenerator():
8
+
9
+ @classmethod
10
+ def today(cls) -> Date:
11
+ obj = Date(datetime.now())
12
+
13
+ return obj
14
+
15
+ @classmethod
16
+ def yesterday(cls) -> Date:
17
+ obj = cls.today()
18
+ obj.prev()
19
+
20
+ return obj
21
+
22
+ @classmethod
23
+ def tomorrow(cls) -> Date:
24
+ obj = cls.today()
25
+ obj.next()
26
+
27
+ return obj
28
+
29
+ @classmethod
30
+ def week(cls, month: int, day: int, year: int) -> DateRange:
31
+ start_dt = Date(month, day, year)
32
+
33
+ # set the end date to the end of the week
34
+ end_dt = copy(start_dt)
35
+ end_dt.next(6)
36
+
37
+ return DateRange(start_dt, end_dt)
38
+
39
+ @classmethod
40
+ def month(cls, month: int, year: int) -> DateRange:
41
+ start_dt = Date(month, 1, year)
42
+
43
+ # set the end date to the end of the month
44
+ end_dt = copy(start_dt)
45
+ end_dt.next(28)
46
+
47
+ # check if it is still in the month then go one back
48
+ while end_dt._date.month == month:
49
+ end_dt.next()
50
+
51
+ end_dt.prev()
52
+
53
+ return DateRange(start_dt, end_dt)
@@ -0,0 +1,21 @@
1
+ from typing import Tuple
2
+
3
+ from .date import Date
4
+
5
+ class DateRange():
6
+ def __init__(self, start_dt: Date, end_dt: Date):
7
+ self.set_dates(start_dt, end_dt)
8
+
9
+ def set_start_date(self, start_dt: Date) -> None:
10
+ self.start_dt: Date = start_dt
11
+
12
+ def set_end_date(self, end_dt: Date) -> None:
13
+ self.end_dt: Date = end_dt
14
+
15
+ def set_dates(self, start_dt: Date, end_dt: Date) -> None:
16
+ self.set_start_date(start_dt)
17
+ self.set_end_date(end_dt)
18
+
19
+ def get_dates(self) -> tuple[Date, Date]:
20
+ return self.start_dt, self.end_dt
21
+
mlbrecaps/game.py ADDED
@@ -0,0 +1,169 @@
1
+ import pandas as pd
2
+ import io
3
+ import requests
4
+ import json
5
+
6
+ from functools import lru_cache, singledispatchmethod
7
+ from typing import List, Optional
8
+
9
+ from .team import Team
10
+ from .date import Date
11
+ from .player import Player
12
+ from .play import Play
13
+ from .utils import async_run, dataframe_from_url
14
+
15
+ class Game():
16
+
17
+ @lru_cache(maxsize=3)
18
+ def __new__(cls, game_pk: int):
19
+ return super().__new__(cls)
20
+
21
+ def __init__(self, game_pk: int):
22
+ self._game_pk: int = game_pk
23
+ self._game_data: pd.DataFrame | None = None
24
+
25
+ # finds the url of the game based on the game_pk information stored in the at-bat data
26
+ game_url = f"https://baseballsavant.mlb.com/gf?game_pk={self._game_pk}"
27
+ game = requests.get(game_url)
28
+
29
+ # load the given game's json file
30
+ self._game_json = json.loads(game.text)
31
+ self._away_json = self._game_json["team_away"]
32
+ self._home_json = self._game_json["team_home"]
33
+
34
+ # Get home/away and date
35
+ self._home: Team = Team(self._game_json["home_team_data"]["abbreviation"])
36
+ self._away: Team = Team(self._game_json["away_team_data"]["abbreviation"])
37
+ self._date: Date = Date(self._game_json["gameDate"])
38
+
39
+ # Get both lineups
40
+ self._home_lineup: List[int] = self._game_json["home_lineup"]
41
+ self._away_lineup: List[int] = self._game_json["away_lineup"]
42
+
43
+ # Get the final scores
44
+ self._home_score: int = self._game_json["scoreboard"]["linescore"]["teams"]["home"]["runs"]
45
+ self._away_score: int = self._game_json["scoreboard"]["linescore"]["teams"]["away"]["runs"]
46
+
47
+ @singledispatchmethod
48
+ def road_status(self, team: Team) -> str:
49
+ raise ValueError("team must be of type Team")
50
+
51
+ @road_status.register(Team)
52
+ def _(self, team: Team) -> str:
53
+ if self._away == team:
54
+ return "AWAY"
55
+
56
+ return "HOME"
57
+
58
+ @property
59
+ def home(self) -> Team:
60
+ return self._home
61
+
62
+ @property
63
+ def away(self) -> Team:
64
+ return self._away
65
+
66
+ @property
67
+ def home_score(self) -> int:
68
+ return self._home_score
69
+
70
+ @property
71
+ def away_score(self) -> int:
72
+ return self._away_score
73
+
74
+ @property
75
+ def home_lineup(self) -> List[int]:
76
+ return self._home_lineup.copy()
77
+
78
+ @property
79
+ def away_lineup(self) -> List[int]:
80
+ return self._away_lineup.copy()
81
+
82
+ def get_lineup(self, team) -> List[int]:
83
+ if team == self._away:
84
+ return self.away_lineup()
85
+
86
+ return self.home_lineup
87
+
88
+ @property
89
+ def game_pk(self) -> int:
90
+ return self._game_pk
91
+
92
+ @property
93
+ def date(self) -> Date:
94
+ return self._date.copy()
95
+
96
+ @dataframe_from_url
97
+ def __get_data(self) -> pd.DataFrame:
98
+ return f"https://baseballsavant.mlb.com/statcast_search/csv?all=true&type=details&game_pk={self._game_pk}"
99
+
100
+ @property
101
+ def data(self) -> pd.DataFrame:
102
+ return self.__get_data().copy()
103
+
104
+ @property
105
+ def game_json(self):
106
+ return self._game_json.copy()
107
+
108
+ @property
109
+ def away_json(self):
110
+ return self._away_json.copy()
111
+
112
+ @property
113
+ def home_json(self):
114
+ return self._home_json.copy()
115
+
116
+ def get_highlights(self, plays:int =10, team: Optional[str]=None):
117
+ df = self.__get_data()
118
+
119
+ if plays <= 0:
120
+ raise ValueError("Plays must be greater than 0")
121
+
122
+ if team is not None and team.upper() not in ["HOME", "AWAY"]:
123
+ raise ValueError("Team must be None, \"HOME\", or \"AWAY\"")
124
+
125
+ # extra logic for when a home or away team is specified
126
+ key = None if team else abs
127
+ ascending = False if not team else (True, False)[team.lower() == "home"]
128
+
129
+ # removes all non-events (balls, strikes, etc.)
130
+ # then sorts for highest win expectancy for either team
131
+ # then only keeps the top plays
132
+ # also make sure the plays are in chronological order of the game
133
+ df = df[df.events.notnull()]
134
+ df = df.sort_values(by="delta_home_win_exp", key=key, ascending=ascending)
135
+ df = df.head(plays)
136
+ df = df.sort_values(by="pitch_number", ascending=True)
137
+ df = df.sort_values(by="at_bat_number", ascending=True)
138
+
139
+ rows = [row for index, row in df.iterrows()]
140
+ return async_run(Play, self, rows)
141
+
142
+ def get_home_highlights(self, plays=10):
143
+ return self.get_highlights(plays, "home")
144
+
145
+ def get_away_highlights(self, plays=10):
146
+ return self.get_highlights(plays, "away")
147
+
148
+ def get_player_highlights(self, player: Player, plays: int):
149
+ df = self.__get_data()
150
+
151
+ is_home_player = player.player_id in self.home_lineup
152
+
153
+ df = df[df.events.notnull() & ((df.batter == player.player_id) | (df.pitcher == player.player_id))]
154
+ df = df.sort_values(by="delta_home_win_exp", key=None, ascending=is_home_player)
155
+ df = df.sort_values(by="at_bat_number", ascending=True)
156
+
157
+ if is_home_player:
158
+ df = df[df["delta_home_win_exp"] >= 0]
159
+ else:
160
+ df = df[df["delta_home_win_exp"] <= 0]
161
+
162
+ df = df.head(plays)
163
+
164
+ rows = [row for index, row in df.iterrows()]
165
+ return async_run(Play, self, rows)
166
+
167
+ def __str__(self) -> str:
168
+ return f"{self._away.abbreviation} - {self._home.abbreviation}, Final: {self._away_score}-{self._home_score}, Date: {self._date}, GamePK: {self._game_pk}"
169
+
@@ -0,0 +1,93 @@
1
+ import requests
2
+ import json
3
+ import pandas as pd
4
+
5
+ # from multipledispatch import dispatch
6
+ from functools import singledispatchmethod, cache
7
+ from typing import List, Set, Tuple
8
+
9
+ from .team import Team
10
+ from .date import Date
11
+ from .date_range import DateRange
12
+ from .game import Game
13
+ from .utils import async_run
14
+
15
+ class GameGenerator():
16
+
17
+ def __init__(self, teams: Team | List[Team], date: Date | DateRange | str | Tuple[int, int, int]):
18
+ self.__set_teams(teams)
19
+ self.__set_date(date)
20
+
21
+ self._ids: Set[int] = self.__from_dates()
22
+
23
+ @singledispatchmethod
24
+ def __set_teams(self, teams: Team | List[Team]) -> None:
25
+ raise ValueError("Teams must be a Team or list of Team object")
26
+
27
+ @__set_teams.register(list)
28
+ def _(self, teams: List[Team]) -> None:
29
+ if len(teams) == 0 or not all(isinstance(team, Team) for team in teams):
30
+ raise ValueError("A list of Team objects must be passed")
31
+
32
+ self._teams: List[Team] = teams
33
+
34
+ @__set_teams.register(Team)
35
+ def _(self, teams: Team) -> None:
36
+ self._teams = [teams]
37
+
38
+ @singledispatchmethod
39
+ def __set_date(self, date) -> None:
40
+ raise ValueError("Invalid date must be of type Date, DateRange, str, or Tuple[int, int, int]")
41
+
42
+ @__set_date.register(str)
43
+ def _(self, date: str) -> None:
44
+ try:
45
+ d = Date(date)
46
+ except:
47
+ raise ValueError("A valid date string of format %m/%d/%Y must be passed")
48
+
49
+ self._date = DateRange(d, d)
50
+
51
+ @__set_date.register(tuple)
52
+ def _(self, date: Tuple[int, int, int]) -> None:
53
+ try:
54
+ d = Date(*date)
55
+ except:
56
+ raise ValueError("A valid date tuple of format (month, day, year) must be passed")
57
+
58
+ self._date = DateRange(d, d)
59
+
60
+ @__set_date.register(Date)
61
+ def _(self, date: Date) -> None:
62
+ self._date: DateRange = DateRange(date, date)
63
+
64
+ @__set_date.register(DateRange)
65
+ def _(self, date: DateRange) -> None:
66
+ self._date = date
67
+
68
+ @property
69
+ @cache
70
+ def games(self) -> List[Game]:
71
+ # Generate every Game object from ids
72
+ games: List[Game] = async_run(Game, list(self._ids))
73
+ games.sort(key=lambda game: game.date)
74
+
75
+ return games
76
+
77
+ def __from_dates(self) -> Set[int]:
78
+ start_dt, end_dt = self._date.get_dates()
79
+
80
+ games_url = [f"https://statsapi.mlb.com/api/v1/schedule?startDate={start_dt.to_formatted_string()}&endDate={end_dt.to_formatted_string()}&sportId=1&teamId={team.team_id}" for team in self._teams]
81
+ date_jsons = [json.loads(requests.get(game_url).text) for game_url in games_url]
82
+
83
+ # Put all game_pks into a set
84
+ return {int(game["gamePk"]) for date_json in date_jsons for date in date_json["dates"] for game in date["games"]}
85
+
86
+ @property
87
+ def ids(self) -> List[int]:
88
+ # Only finds the unique game_pks for a given team (needed in case of a double header)
89
+ return list(self._ids)
90
+
91
+ def __len__(self) -> int:
92
+ return len(self._ids)
93
+
mlbrecaps/play.py ADDED
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import pandas as pd
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .game import Game
8
+
9
+ class Play():
10
+
11
+ def __init__(self, game: Game, row):
12
+ self._game: Game = game
13
+ self._at_bat: int = row.at_bat_number
14
+ self._play: pd.DataFrame = row
15
+ self._pitch_number: int = row.pitch_number
16
+ self._batter: str = row.player_name
17
+ self._event: str = row.description
18
+ self._description: str = row.des
19
+ self._inning_topbot: str = row.inning_topbot
20
+
21
+ if self._inning_topbot == "Top":
22
+ team = self._game.home_json
23
+ else:
24
+ team = self._game.away_json
25
+
26
+ # filter the json file to find the at bat, this will help find the play id
27
+ team = [x for x in team if x["ab_number"] == self._at_bat]
28
+
29
+ # sorts the at bat by pitch number, highest number is the last pitch of the at bat
30
+ team.sort(key=lambda item: item["pitch_number"], reverse=True)
31
+ self._play_id: str = team[0]["play_id"]
32
+
33
+ @property
34
+ def game(self) -> Game:
35
+ return self._game
36
+
37
+ @property
38
+ def at_bat(self) -> int:
39
+ return self._at_bat
40
+
41
+ @property
42
+ def play_data(self) -> pd.DataFrame:
43
+ return self._play.copy()
44
+
45
+ @property
46
+ def inning_topbot(self) -> str:
47
+ return self._inning_topbot
48
+
49
+ @property
50
+ def batter(self) -> str:
51
+ return self._batter
52
+
53
+ @property
54
+ def event(self) -> str:
55
+ return self._event
56
+
57
+ @property
58
+ def description(self) -> str:
59
+ return self._description
60
+
61
+ @property
62
+ def play_id(self) -> str:
63
+ return self._play_id
64
+
65
+ def __str__(self):
66
+ return f"{self.__class__}@Game={self._game}:atBat={self._at_bat}:Batter={self._batter}:Description={self._description}:topBot={self._inning_topbot}"
mlbrecaps/player.py ADDED
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import pandas as pd
4
+ import io
5
+ import requests
6
+ import json
7
+
8
+ from typing import List, Dict, Optional, TYPE_CHECKING
9
+ if TYPE_CHECKING:
10
+ from .game import Play
11
+
12
+ from .date_generator import DateGenerator
13
+ from .utils import async_run, dataframe_copy, dataframe_from_url
14
+
15
+ class Player():
16
+
17
+ def __init__(self, player_id: int):
18
+ self._player_id: int = player_id
19
+
20
+ # Get player JSON information
21
+ playerURL: str = f"https://statsapi.mlb.com/api/v1/people/{player_id}"
22
+ playerContent = requests.get(playerURL)
23
+ player_json = json.loads(playerContent.text)["people"][0]
24
+
25
+ # Player's name
26
+ self._first_name: str = player_json["firstName"]
27
+ self._last_name: str = player_json["lastName"]
28
+
29
+ # Player's position
30
+ self._primary_position: str = player_json["primaryPosition"]["name"]
31
+ self._primary_position_abbr: str = player_json["primaryPosition"]["abbreviation"]
32
+
33
+ def is_pitcher(self) -> bool:
34
+ return self._primary_position_abbr == "P"
35
+
36
+ def is_batter(self) -> bool:
37
+ return not self.is_pitcher()
38
+
39
+ @property
40
+ def position(self) -> str:
41
+ return self._primary_position
42
+
43
+ @property
44
+ def position_abbr(self) -> str:
45
+ return self._primary_position_abbr
46
+
47
+ @property
48
+ def player_id(self) -> int:
49
+ return self._player_id
50
+
51
+ @property
52
+ def season(self) -> int:
53
+ return self._season
54
+
55
+ @property
56
+ def first_name(self) -> str:
57
+ return self._first_name
58
+
59
+ @property
60
+ def last_name(self) -> str:
61
+ return self._last_name
62
+
63
+ @property
64
+ def full_name(self) -> str:
65
+ return f"{self._first_name} {self._last_name}"
66
+
67
+ # Lazy generate and save each season on call
68
+ @dataframe_from_url
69
+ def __get_homerun_data(self, season: int) -> pd.DataFrame:
70
+ """
71
+ Gets pandas dataframe information on all their homerun plays
72
+ """
73
+ return f"https://baseballsavant.mlb.com/statcast_search/csv?hfPT=&hfAB=home%5C.%5C.run%7C&hfGT=R%7C&hfPR=hit%5C.%5C.into%5C.%5C.play%7C&hfZ=&hfStadium=&hfBBL=&hfNewZones=&hfPull=&hfC=&hfSea={season}%7C&hfSit=&player_type=batter&hfOuts=&hfOpponent=&pitcher_throws=&batter_stands=&hfSA=&game_date_gt=&game_date_lt=&hfMo=&hfTeam=&home_road=&hfRO=&position=&hfInfield=&hfOutfield=&hfInn=&hfBBT=&batters_lookup%5B%5D={self._player_id}&hfFlag=&metric_1=&group_by=name&min_pitches=0&min_results=0&min_pas=0&sort_col=pitches&player_event_sort=api_p_release_speed&sort_order=desc&type=details&player_id={self._player_id}"
74
+
75
+ # Encapsulates mutable data by making a copy
76
+ @dataframe_copy
77
+ def get_homerun_data(self, season: int) -> pd.DataFrame:
78
+ return self.__get_homerun_data(season)
79
+
80
+ def get_homerun_count(self, season: int) -> int:
81
+ return len(self.__get_homerun_data(season).index)
82
+
83
+ def get_homeruns(self, season: int | Date) -> List[Play]:
84
+ from .game import Play, Game
85
+
86
+ homerun_data = self.__get_homerun_data(season)
87
+ # homerun_data.to_csv("./homeruns.csv")
88
+
89
+ # if date:
90
+ # homerun_data = homerun_data[homerun_data[]]
91
+
92
+ games: List[Game] = async_run(Game, list(homerun_data["game_pk"]))
93
+ rows = [row for index, row in homerun_data.iterrows()]
94
+
95
+ return async_run(Play, games, rows)[::-1]
96
+
97
+ def __eq__(self, o: object) -> bool:
98
+ if not isinstance(o, Player):
99
+ return False
100
+
101
+ return self._player_id == other._player_id
102
+
103
+ def __hash__(self) -> int:
104
+ return self._player_id
105
+
106
+ def __str__(self) -> str:
107
+ return f"{self.__class__}@PlayerID={self._player_id}:FirstName={self._first_name}:LastName={self._last_name}"
mlbrecaps/scripts.py ADDED
@@ -0,0 +1,56 @@
1
+ from typing import List
2
+
3
+ from .team import Team
4
+ from .player import Player
5
+ from .clips import Clips
6
+ from .date import Date
7
+ from .date_range import DateRange
8
+ from .game_generator import GameGenerator
9
+
10
+ def get_highlights(team: Team, dates: Date | DateRange, plays: int=10) -> List[Clips]:
11
+ # Type checking
12
+ if not isinstance(team, Team):
13
+ raise ValueError("team must be of type Team")
14
+
15
+ if not (isinstance(dates, Date) or isinstance(dates, DateRange)):
16
+ raise ValueError("dates must be of type Date or DateRange")
17
+
18
+ if not isinstance(plays, int):
19
+ raise ValueError("plays must be of type int")
20
+
21
+ # get all games on that date
22
+ games = GameGenerator(team, dates).games
23
+ clips = []
24
+
25
+ for game in games:
26
+ # Determine whether the team is home or away
27
+ homeRoad = game.road_status(team)
28
+
29
+ # Get the top x number of plays of the game
30
+ homeHighlights = game.get_highlights(plays, homeRoad)
31
+ clips.append(Clips(homeHighlights, homeRoad)) # Generate clips of the plays from the home broadcast
32
+
33
+ return clips
34
+
35
+ def get_player_highlights(team: Team, player: Player, dates: Date):
36
+ # Type checking
37
+ if not isinstance(team, Team):
38
+ raise ValueError("team must be of type Team")
39
+
40
+ if not isinstance(dates, Date):
41
+ raise ValueError("dates must be of type Date or DateRange")
42
+
43
+ game: Game = GameGenerator(team, dates).games[0]
44
+ player_plays = game.get_player_highlights(player, 5)
45
+
46
+ return Clips(player_plays)
47
+
48
+ def get_player_homeruns(player_id: Player | int, season: int=2023):
49
+ if isinstance(player_id, Player):
50
+ player: Player = player_id
51
+ else:
52
+ player = Player(player_id)
53
+
54
+ homeruns: List[Play] = player.get_homeruns(season)
55
+
56
+ return Clips(homeruns)
mlbrecaps/team.py ADDED
@@ -0,0 +1,41 @@
1
+ import pandas as pd
2
+ import os
3
+
4
+ from functools import cache
5
+ from typing import List, Dict
6
+
7
+ class Team():
8
+ _team_lookup: pd.DataFrame = pd.read_csv(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/team-info.csv'))
9
+
10
+ # Speeds up and saves memory by caching the same type of objects
11
+ @cache
12
+ def __new__(cls, abbr: str):
13
+ return super().__new__(cls)
14
+
15
+ def __init__(self, abbr: str):
16
+ self._abbr: str = abbr.upper()
17
+
18
+ row: pd.DataFrame = Team._team_lookup.loc[Team._team_lookup["Abbreviation"] == self._abbr]
19
+ self._team: str = row["Full Name"].values[0]
20
+ self._team_id: int = row["Team ID"].values[0]
21
+
22
+ @property
23
+ def name(self) -> str:
24
+ return self._team
25
+
26
+ @property
27
+ def abbreviation(self) -> str:
28
+ return self._abbr
29
+
30
+ @property
31
+ def team_id(self) -> int:
32
+ return self._team_id
33
+
34
+ def __eq__(self, other: object) -> bool:
35
+ if not isinstance(other, Team):
36
+ return False
37
+
38
+ return self._abbr == other._abbr
39
+
40
+ def __str__(self) -> str:
41
+ return f"{self.__class__}@Name={self._team}:Abbreviation={self._abbr}:ID={self._team_id}"
mlbrecaps/utils.py ADDED
@@ -0,0 +1,59 @@
1
+ import asyncio
2
+ import requests
3
+ import io
4
+ import pandas as pd
5
+
6
+ import itertools
7
+ import functools
8
+
9
+ from typing import Any, List, Tuple
10
+
11
+ def async_run(func: callable, *args: Any | List[Any]) -> List[Any]:
12
+ # Makes all arguments into lists
13
+ try:
14
+ max_length = max(len(x) for x in args if isinstance(x, list))
15
+ except ValueError:
16
+ raise ValueError("At least one argument must be a list")
17
+
18
+ repeated_args = [arg if isinstance(arg, list) else list(itertools.repeat(arg, max_length)) for arg in args]
19
+
20
+ # Ensure all list arguments are of the same length
21
+ if not all(len(i) == len(repeated_args[0]) for i in repeated_args):
22
+ raise ValueError("Argument arrays must all be of the same length")
23
+
24
+ async def create(*args: List[Any]):
25
+ return func(*args)
26
+
27
+ async def generate() -> List[Any]:
28
+ tasks = [asyncio.create_task(create(*args)) for args in zip(*repeated_args)]
29
+
30
+ await asyncio.gather(*tasks)
31
+ return [task.result() for task in tasks]
32
+
33
+ return asyncio.run(generate())
34
+
35
+ def copy_cache(func):
36
+ func = functools.cache(func)
37
+
38
+ def wrapper(*args, **kwargs):
39
+ return func.copy()
40
+
41
+ return wrapper
42
+
43
+ def dataframe_copy(func):
44
+ def wrapper(*args, **kwargs):
45
+ return func(*args, **kwargs).copy()
46
+
47
+ return wrapper
48
+
49
+ def dataframe_from_url(func, use_cache: bool=True):
50
+ if use_cache:
51
+ func = functools.cache(func)
52
+
53
+ def wrapper(*args, **kwargs):
54
+ url: str = func(*args, **kwargs)
55
+ csv: bytes = requests.get(url).content
56
+
57
+ return pd.read_csv(io.StringIO(csv.decode('utf-8')))
58
+
59
+ return wrapper
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Karsten Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.1
2
+ Name: mlbrecaps
3
+ Version: 0.0.1
4
+ Summary: Package that gathers information on given MLB games
5
+ Home-page: https://github.com/MrRedwing/MLB-Recaps
6
+ Author: Karsten Larson
7
+ Author-email: karsten.larson.1@gmail.com
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE.txt
15
+ Requires-Dist: numpy >=1.13.0
16
+ Requires-Dist: pandas >=1.0.3
17
+ Requires-Dist: beautifulsoup4 >=4.4.0
18
+ Requires-Dist: requests >=2.18.1
19
+ Requires-Dist: lxml >=4.2.1
20
+ Provides-Extra: dev
21
+ Requires-Dist: twine ; extra == 'dev'
22
+
23
+ # mlbrecaps
24
+
25
+ A Python package for downloading Major League Baseball game highlights based on team and date.
26
+
27
+ ### Installation
28
+
29
+ ```
30
+ pip install mlbrecaps
31
+ ```
32
+
33
+ Usage
34
+ The mlbrecaps package provides a simple function highlight_generator to download game highlights for a specified Major League Baseball team and date.
35
+
36
+ ### Basic Python Example:
37
+
38
+ ```
39
+ from mlbrecaps import scripts, Team, Date
40
+
41
+ # Download highlights for the Minnesota Twins game on October 3rd, 2023
42
+
43
+ highlights = scripts.get_highlights(Team("MIN"), Date(10, 3, 2023))
44
+
45
+ # Handle all the highlights to a folder
46
+ highlights.download("/path/to/folder/", verbose=True)
47
+ ```
48
+
49
+ ### Run directly from the terminal
50
+
51
+ If you have the mlbrecaps package installed, you can use the following command to download highlights directly from the terminal:
52
+
53
+ ```
54
+ python -m mlbrecaps
55
+ ```
56
+
57
+ This command will prompt you to enter the team three-letter abbreviation, game date, and path to a videos download directory for the desired highlights.
58
+
59
+ ### Examples
60
+
61
+ More examples are avaliable on [Github](https://github.com/MrRedwing/MLB-Recaps).
62
+
63
+ ### Development
64
+
65
+ This package is under active development. Feel free to contribute by submitting pull requests!
66
+
67
+ ### Contributing
68
+
69
+ All contributions are welcomed to improve the mlbrecaps package. To contribute simply submit a pull request.
70
+
71
+ ### License
72
+
73
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,20 @@
1
+ mlbrecaps/__init__.py,sha256=rkJ4dEuvgWVrQYpOzKG_0xjJ1LpiHjRih9ePsKCgTyc,449
2
+ mlbrecaps/__main__.py,sha256=Rdt3X9Xkz0T7Y2yGC0mJlzaU5mSFgKoC3LQq_GhYYnY,589
3
+ mlbrecaps/clip.py,sha256=73c4E6R_JbeT1J24TY74IQfS2eBfAvoERHcLek32zd0,3477
4
+ mlbrecaps/clips.py,sha256=-qEThGxihDewbDP4S3K_owAJZFnixjqYwLFtL9SJBNA,1670
5
+ mlbrecaps/date.py,sha256=d2dSeCpK4z6CtjLBACe3n-PlCdmoYO-jpxH9kY8hazQ,1834
6
+ mlbrecaps/date_generator.py,sha256=hLFbw88EbdYkHofxi-Li1YuypZfSzSMt6ooanOawHN4,1158
7
+ mlbrecaps/date_range.py,sha256=m2lyCOK4zg7MLA4WMlVZEeet7kR1XoPF3L9OkPxZd3o,573
8
+ mlbrecaps/game.py,sha256=hLcxa-uCGxLRFg7x7VFETQa2VkCrovcob95F2UFnhvc,5397
9
+ mlbrecaps/game_generator.py,sha256=4XN4oaMAIOKHDyyXIashaUO8lEJgPTSf0Ih9m2ZTHmM,3027
10
+ mlbrecaps/play.py,sha256=rLsqcPrwZs5M-4tUQZAx6zdjD5DGsJNwfVEpSpHlh3Q,1834
11
+ mlbrecaps/player.py,sha256=iIyhNuhXcbUIGyFus5_XbwneL6o-dVt08hOFyzKjLtg,3696
12
+ mlbrecaps/scripts.py,sha256=aSiWoO1YOM47uiEL7hScipKAL2douTRYdePswPRrol4,1791
13
+ mlbrecaps/team.py,sha256=GDd2C97kxqiQpr7LfpJh5NlWg1C_jEsU6PqAQqvQQnE,1161
14
+ mlbrecaps/utils.py,sha256=s6f875DWv-5uSh7uWDaswoUzKwGbT0zhpCKZvPYAWyE,1613
15
+ mlbrecaps/data/team-info.csv,sha256=FuJQjMoRMO7nKlfXJaBOPW6DNygFze9VbgJ2rGhtMoY,1708
16
+ mlbrecaps-0.0.1.dist-info/LICENSE.txt,sha256=dZUFPJprmKmTUFjocGDr8Az4zrrynjaRwhWVZYA7J8s,1070
17
+ mlbrecaps-0.0.1.dist-info/METADATA,sha256=gGbNI0WWeswgn9baeZaADLLtU1vItB5YcVijJtu_SFY,2113
18
+ mlbrecaps-0.0.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
19
+ mlbrecaps-0.0.1.dist-info/top_level.txt,sha256=FR_TA1KjJjFZJJCdGLXW9s1mNH22ZG85hizGPokUA44,10
20
+ mlbrecaps-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mlbrecaps