mlbrecaps 0.0.1__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.
- mlbrecaps-0.0.1/LICENSE.txt +21 -0
- mlbrecaps-0.0.1/PKG-INFO +67 -0
- mlbrecaps-0.0.1/README.md +51 -0
- mlbrecaps-0.0.1/mlbrecaps/__init__.py +13 -0
- mlbrecaps-0.0.1/mlbrecaps/__main__.py +21 -0
- mlbrecaps-0.0.1/mlbrecaps/clip.py +103 -0
- mlbrecaps-0.0.1/mlbrecaps/clips.py +51 -0
- mlbrecaps-0.0.1/mlbrecaps/data/team-info.csv +33 -0
- mlbrecaps-0.0.1/mlbrecaps/date.py +78 -0
- mlbrecaps-0.0.1/mlbrecaps/date_generator.py +53 -0
- mlbrecaps-0.0.1/mlbrecaps/date_range.py +21 -0
- mlbrecaps-0.0.1/mlbrecaps/game.py +169 -0
- mlbrecaps-0.0.1/mlbrecaps/game_generator.py +93 -0
- mlbrecaps-0.0.1/mlbrecaps/play.py +66 -0
- mlbrecaps-0.0.1/mlbrecaps/player.py +107 -0
- mlbrecaps-0.0.1/mlbrecaps/scripts.py +56 -0
- mlbrecaps-0.0.1/mlbrecaps/team.py +41 -0
- mlbrecaps-0.0.1/mlbrecaps/utils.py +59 -0
- mlbrecaps-0.0.1/mlbrecaps.egg-info/PKG-INFO +67 -0
- mlbrecaps-0.0.1/mlbrecaps.egg-info/SOURCES.txt +28 -0
- mlbrecaps-0.0.1/mlbrecaps.egg-info/dependency_links.txt +1 -0
- mlbrecaps-0.0.1/mlbrecaps.egg-info/requires.txt +8 -0
- mlbrecaps-0.0.1/mlbrecaps.egg-info/top_level.txt +1 -0
- mlbrecaps-0.0.1/setup.cfg +4 -0
- mlbrecaps-0.0.1/setup.py +33 -0
|
@@ -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.
|
mlbrecaps-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
Provides-Extra: dev
|
|
15
|
+
License-File: LICENSE.txt
|
|
16
|
+
|
|
17
|
+
# mlbrecaps
|
|
18
|
+
|
|
19
|
+
A Python package for downloading Major League Baseball game highlights based on team and date.
|
|
20
|
+
|
|
21
|
+
### Installation
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install mlbrecaps
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Usage
|
|
28
|
+
The mlbrecaps package provides a simple function highlight_generator to download game highlights for a specified Major League Baseball team and date.
|
|
29
|
+
|
|
30
|
+
### Basic Python Example:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
from mlbrecaps import scripts, Team, Date
|
|
34
|
+
|
|
35
|
+
# Download highlights for the Minnesota Twins game on October 3rd, 2023
|
|
36
|
+
|
|
37
|
+
highlights = scripts.get_highlights(Team("MIN"), Date(10, 3, 2023))
|
|
38
|
+
|
|
39
|
+
# Handle all the highlights to a folder
|
|
40
|
+
highlights.download("/path/to/folder/", verbose=True)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Run directly from the terminal
|
|
44
|
+
|
|
45
|
+
If you have the mlbrecaps package installed, you can use the following command to download highlights directly from the terminal:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
python -m mlbrecaps
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
### Examples
|
|
54
|
+
|
|
55
|
+
More examples are avaliable on [Github](https://github.com/MrRedwing/MLB-Recaps).
|
|
56
|
+
|
|
57
|
+
### Development
|
|
58
|
+
|
|
59
|
+
This package is under active development. Feel free to contribute by submitting pull requests!
|
|
60
|
+
|
|
61
|
+
### Contributing
|
|
62
|
+
|
|
63
|
+
All contributions are welcomed to improve the mlbrecaps package. To contribute simply submit a pull request.
|
|
64
|
+
|
|
65
|
+
### License
|
|
66
|
+
|
|
67
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# mlbrecaps
|
|
2
|
+
|
|
3
|
+
A Python package for downloading Major League Baseball game highlights based on team and date.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install mlbrecaps
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Usage
|
|
12
|
+
The mlbrecaps package provides a simple function highlight_generator to download game highlights for a specified Major League Baseball team and date.
|
|
13
|
+
|
|
14
|
+
### Basic Python Example:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
from mlbrecaps import scripts, Team, Date
|
|
18
|
+
|
|
19
|
+
# Download highlights for the Minnesota Twins game on October 3rd, 2023
|
|
20
|
+
|
|
21
|
+
highlights = scripts.get_highlights(Team("MIN"), Date(10, 3, 2023))
|
|
22
|
+
|
|
23
|
+
# Handle all the highlights to a folder
|
|
24
|
+
highlights.download("/path/to/folder/", verbose=True)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Run directly from the terminal
|
|
28
|
+
|
|
29
|
+
If you have the mlbrecaps package installed, you can use the following command to download highlights directly from the terminal:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
python -m mlbrecaps
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
### Examples
|
|
38
|
+
|
|
39
|
+
More examples are avaliable on [Github](https://github.com/MrRedwing/MLB-Recaps).
|
|
40
|
+
|
|
41
|
+
### Development
|
|
42
|
+
|
|
43
|
+
This package is under active development. Feel free to contribute by submitting pull requests!
|
|
44
|
+
|
|
45
|
+
### Contributing
|
|
46
|
+
|
|
47
|
+
All contributions are welcomed to improve the mlbrecaps package. To contribute simply submit a pull request.
|
|
48
|
+
|
|
49
|
+
### License
|
|
50
|
+
|
|
51
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -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"]
|
|
@@ -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)
|
|
@@ -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}")
|
|
@@ -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
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|
|
@@ -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}"
|
|
@@ -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}"
|
|
@@ -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)
|
|
@@ -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}"
|
|
@@ -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,67 @@
|
|
|
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
|
+
Provides-Extra: dev
|
|
15
|
+
License-File: LICENSE.txt
|
|
16
|
+
|
|
17
|
+
# mlbrecaps
|
|
18
|
+
|
|
19
|
+
A Python package for downloading Major League Baseball game highlights based on team and date.
|
|
20
|
+
|
|
21
|
+
### Installation
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install mlbrecaps
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Usage
|
|
28
|
+
The mlbrecaps package provides a simple function highlight_generator to download game highlights for a specified Major League Baseball team and date.
|
|
29
|
+
|
|
30
|
+
### Basic Python Example:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
from mlbrecaps import scripts, Team, Date
|
|
34
|
+
|
|
35
|
+
# Download highlights for the Minnesota Twins game on October 3rd, 2023
|
|
36
|
+
|
|
37
|
+
highlights = scripts.get_highlights(Team("MIN"), Date(10, 3, 2023))
|
|
38
|
+
|
|
39
|
+
# Handle all the highlights to a folder
|
|
40
|
+
highlights.download("/path/to/folder/", verbose=True)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Run directly from the terminal
|
|
44
|
+
|
|
45
|
+
If you have the mlbrecaps package installed, you can use the following command to download highlights directly from the terminal:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
python -m mlbrecaps
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
### Examples
|
|
54
|
+
|
|
55
|
+
More examples are avaliable on [Github](https://github.com/MrRedwing/MLB-Recaps).
|
|
56
|
+
|
|
57
|
+
### Development
|
|
58
|
+
|
|
59
|
+
This package is under active development. Feel free to contribute by submitting pull requests!
|
|
60
|
+
|
|
61
|
+
### Contributing
|
|
62
|
+
|
|
63
|
+
All contributions are welcomed to improve the mlbrecaps package. To contribute simply submit a pull request.
|
|
64
|
+
|
|
65
|
+
### License
|
|
66
|
+
|
|
67
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
/home/karsten/coding/python/recaps/mlbrecaps.egg-info/PKG-INFO
|
|
5
|
+
/home/karsten/coding/python/recaps/mlbrecaps.egg-info/SOURCES.txt
|
|
6
|
+
/home/karsten/coding/python/recaps/mlbrecaps.egg-info/dependency_links.txt
|
|
7
|
+
/home/karsten/coding/python/recaps/mlbrecaps.egg-info/requires.txt
|
|
8
|
+
/home/karsten/coding/python/recaps/mlbrecaps.egg-info/top_level.txt
|
|
9
|
+
mlbrecaps/__init__.py
|
|
10
|
+
mlbrecaps/__main__.py
|
|
11
|
+
mlbrecaps/clip.py
|
|
12
|
+
mlbrecaps/clips.py
|
|
13
|
+
mlbrecaps/date.py
|
|
14
|
+
mlbrecaps/date_generator.py
|
|
15
|
+
mlbrecaps/date_range.py
|
|
16
|
+
mlbrecaps/game.py
|
|
17
|
+
mlbrecaps/game_generator.py
|
|
18
|
+
mlbrecaps/play.py
|
|
19
|
+
mlbrecaps/player.py
|
|
20
|
+
mlbrecaps/scripts.py
|
|
21
|
+
mlbrecaps/team.py
|
|
22
|
+
mlbrecaps/utils.py
|
|
23
|
+
mlbrecaps.egg-info/PKG-INFO
|
|
24
|
+
mlbrecaps.egg-info/SOURCES.txt
|
|
25
|
+
mlbrecaps.egg-info/dependency_links.txt
|
|
26
|
+
mlbrecaps.egg-info/requires.txt
|
|
27
|
+
mlbrecaps.egg-info/top_level.txt
|
|
28
|
+
mlbrecaps/data/team-info.csv
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mlbrecaps
|
mlbrecaps-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r") as f:
|
|
4
|
+
long_description = f.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="mlbrecaps",
|
|
8
|
+
version="0.0.1",
|
|
9
|
+
description="Package that gathers information on given MLB games",
|
|
10
|
+
packages=find_packages(include=["mlbrecaps"]),
|
|
11
|
+
long_description=long_description,
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
url="https://github.com/MrRedwing/MLB-Recaps",
|
|
14
|
+
author="Karsten Larson",
|
|
15
|
+
author_email="karsten.larson.1@gmail.com",
|
|
16
|
+
license="MIT",
|
|
17
|
+
classifiers=[
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
],
|
|
22
|
+
install_requires=['numpy>=1.13.0',
|
|
23
|
+
'pandas >= 1.0.3',
|
|
24
|
+
'beautifulsoup4>=4.4.0',
|
|
25
|
+
'requests>=2.18.1',
|
|
26
|
+
'lxml>=4.2.1',],
|
|
27
|
+
extras_require={
|
|
28
|
+
"dev": ["twine"]
|
|
29
|
+
},
|
|
30
|
+
python_requires=">=3.10",
|
|
31
|
+
include_package_data=True,
|
|
32
|
+
package_data={'': ['data/*.csv']},
|
|
33
|
+
)
|