mlbrecaps 0.0.1__tar.gz → 0.0.2__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 → mlbrecaps-0.0.2}/PKG-INFO +4 -1
- mlbrecaps-0.0.2/mlbrecaps/__main__.py +25 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/clip.py +32 -27
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/clips.py +26 -7
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/data/team-info.csv +1 -1
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/date_generator.py +3 -1
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/game.py +32 -20
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/player.py +20 -13
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/team.py +8 -4
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/utils.py +10 -4
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps.egg-info/PKG-INFO +4 -1
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps.egg-info/SOURCES.txt +0 -5
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/setup.py +2 -2
- mlbrecaps-0.0.1/mlbrecaps/__main__.py +0 -21
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/LICENSE.txt +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/README.md +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/__init__.py +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/date.py +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/date_range.py +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/game_generator.py +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/play.py +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps/scripts.py +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps.egg-info/dependency_links.txt +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps.egg-info/requires.txt +2 -2
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/mlbrecaps.egg-info/top_level.txt +0 -0
- {mlbrecaps-0.0.1 → mlbrecaps-0.0.2}/setup.cfg +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mlbrecaps
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Package that gathers information on given MLB games
|
|
5
5
|
Home-page: https://github.com/MrRedwing/MLB-Recaps
|
|
6
6
|
Author: Karsten Larson
|
|
7
7
|
Author-email: karsten.larson.1@gmail.com
|
|
8
8
|
License: MIT
|
|
9
|
+
Platform: UNKNOWN
|
|
9
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
@@ -65,3 +66,5 @@ All contributions are welcomed to improve the mlbrecaps package. To contribute s
|
|
|
65
66
|
### License
|
|
66
67
|
|
|
67
68
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
69
|
+
|
|
70
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .scripts import get_highlights
|
|
5
|
+
|
|
6
|
+
from .date import Date
|
|
7
|
+
from .team import Team
|
|
8
|
+
from .clips import Clips
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
# Get teams for the search
|
|
12
|
+
team: Team = Team(input("Input team name: "))
|
|
13
|
+
date: Date = Date(input("Date MM/DD/YYYY: "))
|
|
14
|
+
file_path: Path = Path(input("Downloads filepath: "))
|
|
15
|
+
|
|
16
|
+
game_clips: List[Clips] = get_highlights(team, date)
|
|
17
|
+
|
|
18
|
+
for index, clips in enumerate(game_clips):
|
|
19
|
+
# Download all highlight clips
|
|
20
|
+
print(f"Game {index + 1}: {clips.plays[0].game}")
|
|
21
|
+
|
|
22
|
+
dir_path: Path = file_path / f"{index:03d}"
|
|
23
|
+
dir_path.mkdir(exist_ok=True)
|
|
24
|
+
|
|
25
|
+
clips.download(dir_path, verbose=True)
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
from bs4 import BeautifulSoup
|
|
2
2
|
|
|
3
3
|
import requests
|
|
4
|
-
import
|
|
5
|
-
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
6
5
|
|
|
7
6
|
from .play import Play
|
|
8
7
|
|
|
8
|
+
|
|
9
9
|
class Clip():
|
|
10
|
+
"""A wrapper class for Play that allows for plays to be downloaded"""
|
|
10
11
|
|
|
11
|
-
def __init__(self, play: Play, broadcast_type: str | None=None):
|
|
12
|
+
def __init__(self, play: Play, broadcast_type: str | None = None):
|
|
12
13
|
if not isinstance(play, Play):
|
|
13
14
|
raise ValueError("Play must be a Play object")
|
|
14
15
|
|
|
15
16
|
self._play: Play = play
|
|
16
17
|
|
|
17
|
-
match broadcast_type:
|
|
18
|
+
match broadcast_type: # Enforce broad_type types
|
|
18
19
|
case "HOME" | "AWAY" | None:
|
|
19
20
|
self.broadcast_type: str | None = broadcast_type
|
|
20
21
|
case _:
|
|
21
|
-
raise ValueError(
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"BroadcastType must be None, \"HOME\", or \"AWAY\"")
|
|
22
24
|
|
|
23
25
|
self._clip_url: str = self.__generate()
|
|
24
26
|
|
|
@@ -33,13 +35,15 @@ class Clip():
|
|
|
33
35
|
def play(self) -> Play:
|
|
34
36
|
return self._play
|
|
35
37
|
|
|
36
|
-
# gets the url of the clip to be downloaded from the savant clip
|
|
37
38
|
def __get_url(self, site_url: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Gets the url of the clip to be downloaded from the savant clip
|
|
41
|
+
"""
|
|
38
42
|
# Get the savant site
|
|
39
43
|
site: requests.Response = requests.get(site_url)
|
|
40
44
|
|
|
41
45
|
# Find the video element of the savant clip, find the source url of the clip
|
|
42
|
-
soup= BeautifulSoup(site.text, features="lxml")
|
|
46
|
+
soup = BeautifulSoup(site.text, features="lxml")
|
|
43
47
|
video_obj = soup.find("video", id="sporty")
|
|
44
48
|
|
|
45
49
|
if not video_obj:
|
|
@@ -51,12 +55,12 @@ class Clip():
|
|
|
51
55
|
# Return the source url of the clip so it can be downloaded later
|
|
52
56
|
return clip_url
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
# finds the savant clip based on the given at-bat information
|
|
56
|
-
# row must be a pandas dataframe row
|
|
57
58
|
def __generate(self) -> str:
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
"""
|
|
60
|
+
Generates a savant clip based on the given at-bat information
|
|
61
|
+
|
|
62
|
+
Row must be a pandas dataframe row.
|
|
63
|
+
"""
|
|
60
64
|
|
|
61
65
|
# find the broadcast type so it's always corresponding
|
|
62
66
|
# to the given batter's home team's broadcast
|
|
@@ -74,7 +78,7 @@ class Clip():
|
|
|
74
78
|
# if the clip is alright return it
|
|
75
79
|
if clip_url != "":
|
|
76
80
|
return clip_url
|
|
77
|
-
|
|
81
|
+
|
|
78
82
|
# if the clip is screwed up then it was a national tv game
|
|
79
83
|
# return the correct national tv clip url
|
|
80
84
|
site_url = f"https://baseballsavant.mlb.com/sporty-videos?playId={self._play.play_id}&videoType=NETWORK"
|
|
@@ -82,22 +86,23 @@ class Clip():
|
|
|
82
86
|
|
|
83
87
|
return clip_url
|
|
84
88
|
|
|
85
|
-
def download(self, path: str, verbose: bool =False) ->
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
print(f'Timeout has been raised. Link: {self._clip_url}')
|
|
89
|
+
def download(self, path: str | Path, verbose: bool = False) -> Path:
|
|
90
|
+
path = path if isinstance(path, Path) else Path(path)
|
|
91
|
+
|
|
92
|
+
# create response object
|
|
93
|
+
try:
|
|
94
|
+
r = requests.get(self._clip_url, stream=True, timeout=60)
|
|
95
|
+
except requests.exceptions.Timeout:
|
|
96
|
+
print(f'Timeout has been raised. Link: {self._clip_url}')
|
|
94
97
|
|
|
95
98
|
# download the file to the specific location
|
|
96
99
|
# 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
|
|
99
|
-
if chunk:
|
|
100
|
-
f.write(chunk)
|
|
100
|
+
with open(path, 'wb') as f:
|
|
101
|
+
for chunk in r.iter_content(chunk_size=1024*1024):
|
|
102
|
+
if chunk:
|
|
103
|
+
f.write(chunk)
|
|
101
104
|
|
|
102
105
|
if verbose:
|
|
103
|
-
print(f"Successfully downloaded: {path}")
|
|
106
|
+
print(f"Successfully downloaded: {path.absolute()}")
|
|
107
|
+
|
|
108
|
+
return path
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
from typing import List, Literal
|
|
2
2
|
from functools import singledispatchmethod
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from .play import Play
|
|
5
6
|
from .game import Game
|
|
6
7
|
from .clip import Clip
|
|
7
8
|
from .utils import async_run
|
|
8
9
|
|
|
10
|
+
|
|
9
11
|
class Clips():
|
|
10
|
-
|
|
12
|
+
"""Container class for working with and generating multiple clips from a list of plays"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, plays: List[Play] | Play, broadcast_type: Literal["HOME", "AWAY"] | None = None):
|
|
11
15
|
self.__set_plays(plays)
|
|
12
16
|
|
|
13
|
-
match broadcast_type:
|
|
17
|
+
match broadcast_type: # Enforce broad_type types
|
|
14
18
|
case "HOME" | "AWAY" | None:
|
|
15
19
|
self._broadcast_type: str | None = broadcast_type
|
|
16
20
|
case _:
|
|
17
|
-
raise ValueError(
|
|
21
|
+
raise ValueError(
|
|
22
|
+
"BroadcastType must be None, \"HOME\", or \"AWAY\"")
|
|
18
23
|
|
|
19
24
|
# Generate clip objects for each play passed
|
|
20
25
|
self._clips = async_run(Clip, self._plays, self._broadcast_type)
|
|
@@ -22,7 +27,7 @@ class Clips():
|
|
|
22
27
|
@singledispatchmethod
|
|
23
28
|
def __set_plays(self, plays) -> None:
|
|
24
29
|
raise ValueError("A Play or list of Play objects must be passed")
|
|
25
|
-
|
|
30
|
+
|
|
26
31
|
@__set_plays.register(list)
|
|
27
32
|
def _(self, plays: List[Play]) -> None:
|
|
28
33
|
if len(plays) == 0 or not isinstance(plays[0], Play):
|
|
@@ -46,6 +51,20 @@ class Clips():
|
|
|
46
51
|
def broadcast_type(self) -> str:
|
|
47
52
|
return self._broadcast_type
|
|
48
53
|
|
|
49
|
-
def download(self,
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
def download(self, dir_path: str | Path, verbose: bool = False) -> List[Path]:
|
|
55
|
+
"""Download all clips into a single directory"""
|
|
56
|
+
# Convert path string to Path object
|
|
57
|
+
dir_path: Path = dir_path if isinstance(
|
|
58
|
+
dir_path, Path) else Path(dir_path)
|
|
59
|
+
dir_path.mkdir(exist_ok=True, parents=True)
|
|
60
|
+
|
|
61
|
+
# Get all paths for the mp4s
|
|
62
|
+
paths: List[Path] = [
|
|
63
|
+
dir_path / f"{index:04d}.mp4" for index in range(len(self._clips))]
|
|
64
|
+
|
|
65
|
+
# Create all paths in memory if they don't exist
|
|
66
|
+
for p in paths:
|
|
67
|
+
p.touch()
|
|
68
|
+
|
|
69
|
+
# Download all clips and return the download paths
|
|
70
|
+
return async_run(Clip.download, self._clips, paths, verbose)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Team ID,Code,File Code,Abbreviation,Name,Full Name,Brief Name
|
|
2
2
|
108,ana,ana,LAA,LA Angels,Los Angeles Angels,Angels
|
|
3
|
-
109,ari,ari,
|
|
3
|
+
109,ari,ari,AZ,Arizona,Arizona Diamondbacks,D-backs
|
|
4
4
|
110,bal,bal,BAL,Baltimore,Baltimore Orioles,Orioles
|
|
5
5
|
111,bos,bos,BOS,Boston,Boston Red Sox,Red Sox
|
|
6
6
|
112,chn,chc,CHC,Chi Cubs,Chicago Cubs,Cubs
|
|
@@ -4,7 +4,9 @@ from copy import copy
|
|
|
4
4
|
from .date import Date
|
|
5
5
|
from .date_range import DateRange
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class DateGenerator():
|
|
9
|
+
"""Static library for generating date objects"""
|
|
8
10
|
|
|
9
11
|
@classmethod
|
|
10
12
|
def today(cls) -> Date:
|
|
@@ -50,4 +52,4 @@ class DateGenerator():
|
|
|
50
52
|
|
|
51
53
|
end_dt.prev()
|
|
52
54
|
|
|
53
|
-
return DateRange(start_dt, end_dt)
|
|
55
|
+
return DateRange(start_dt, end_dt)
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
|
-
import io
|
|
3
2
|
import requests
|
|
4
3
|
import json
|
|
5
4
|
|
|
6
5
|
from functools import lru_cache, singledispatchmethod
|
|
7
|
-
from typing import List, Optional
|
|
6
|
+
from typing import List, Optional, Dict
|
|
8
7
|
|
|
9
8
|
from .team import Team
|
|
10
9
|
from .date import Date
|
|
@@ -12,6 +11,7 @@ from .player import Player
|
|
|
12
11
|
from .play import Play
|
|
13
12
|
from .utils import async_run, dataframe_from_url
|
|
14
13
|
|
|
14
|
+
|
|
15
15
|
class Game():
|
|
16
16
|
|
|
17
17
|
@lru_cache(maxsize=3)
|
|
@@ -19,8 +19,7 @@ class Game():
|
|
|
19
19
|
return super().__new__(cls)
|
|
20
20
|
|
|
21
21
|
def __init__(self, game_pk: int):
|
|
22
|
-
self._game_pk: int
|
|
23
|
-
self._game_data: pd.DataFrame | None = None
|
|
22
|
+
self._game_pk: int = game_pk
|
|
24
23
|
|
|
25
24
|
# finds the url of the game based on the game_pk information stored in the at-bat data
|
|
26
25
|
game_url = f"https://baseballsavant.mlb.com/gf?game_pk={self._game_pk}"
|
|
@@ -32,18 +31,20 @@ class Game():
|
|
|
32
31
|
self._home_json = self._game_json["team_home"]
|
|
33
32
|
|
|
34
33
|
# Get home/away and date
|
|
35
|
-
self._home: Team = Team(
|
|
36
|
-
|
|
34
|
+
self._home: Team = Team(
|
|
35
|
+
self._game_json["home_team_data"]["abbreviation"])
|
|
36
|
+
self._away: Team = Team(
|
|
37
|
+
self._game_json["away_team_data"]["abbreviation"])
|
|
37
38
|
self._date: Date = Date(self._game_json["gameDate"])
|
|
38
39
|
|
|
39
40
|
# Get both lineups
|
|
40
|
-
self.
|
|
41
|
-
self.
|
|
41
|
+
self._home_lineup_ids: List[int] = self._game_json["home_lineup"]
|
|
42
|
+
self._away_lineup_ids: List[int] = self._game_json["away_lineup"]
|
|
42
43
|
|
|
43
44
|
# Get the final scores
|
|
44
45
|
self._home_score: int = self._game_json["scoreboard"]["linescore"]["teams"]["home"]["runs"]
|
|
45
46
|
self._away_score: int = self._game_json["scoreboard"]["linescore"]["teams"]["away"]["runs"]
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
@singledispatchmethod
|
|
48
49
|
def road_status(self, team: Team) -> str:
|
|
49
50
|
raise ValueError("team must be of type Team")
|
|
@@ -73,16 +74,16 @@ class Game():
|
|
|
73
74
|
|
|
74
75
|
@property
|
|
75
76
|
def home_lineup(self) -> List[int]:
|
|
76
|
-
return self.
|
|
77
|
+
return self._home_lineup_ids.copy()
|
|
77
78
|
|
|
78
79
|
@property
|
|
79
80
|
def away_lineup(self) -> List[int]:
|
|
80
|
-
return self.
|
|
81
|
+
return self._away_lineup_ids.copy()
|
|
81
82
|
|
|
82
83
|
def get_lineup(self, team) -> List[int]:
|
|
83
84
|
if team == self._away:
|
|
84
|
-
return self.away_lineup
|
|
85
|
-
|
|
85
|
+
return self.away_lineup
|
|
86
|
+
|
|
86
87
|
return self.home_lineup
|
|
87
88
|
|
|
88
89
|
@property
|
|
@@ -113,8 +114,9 @@ class Game():
|
|
|
113
114
|
def home_json(self):
|
|
114
115
|
return self._home_json.copy()
|
|
115
116
|
|
|
116
|
-
def get_highlights(self, plays:int =10, team: Optional[str]=None):
|
|
117
|
-
|
|
117
|
+
def get_highlights(self, plays: int = 10, team: Optional[str] = None) -> List[Play]:
|
|
118
|
+
"""Gets the highlights of both, away, or home teams"""
|
|
119
|
+
df: pd.DataFrame = self.__get_data()
|
|
118
120
|
|
|
119
121
|
if plays <= 0:
|
|
120
122
|
raise ValueError("Plays must be greater than 0")
|
|
@@ -124,14 +126,16 @@ class Game():
|
|
|
124
126
|
|
|
125
127
|
# extra logic for when a home or away team is specified
|
|
126
128
|
key = None if team else abs
|
|
127
|
-
ascending = False if not team else (
|
|
129
|
+
ascending = False if not team else (
|
|
130
|
+
True, False)[team.lower() == "home"]
|
|
128
131
|
|
|
129
132
|
# removes all non-events (balls, strikes, etc.)
|
|
130
133
|
# then sorts for highest win expectancy for either team
|
|
131
134
|
# then only keeps the top plays
|
|
132
135
|
# also make sure the plays are in chronological order of the game
|
|
133
136
|
df = df[df.events.notnull()]
|
|
134
|
-
df = df.sort_values(by="delta_home_win_exp",
|
|
137
|
+
df = df.sort_values(by="delta_home_win_exp",
|
|
138
|
+
key=key, ascending=ascending)
|
|
135
139
|
df = df.head(plays)
|
|
136
140
|
df = df.sort_values(by="pitch_number", ascending=True)
|
|
137
141
|
df = df.sort_values(by="at_bat_number", ascending=True)
|
|
@@ -150,8 +154,10 @@ class Game():
|
|
|
150
154
|
|
|
151
155
|
is_home_player = player.player_id in self.home_lineup
|
|
152
156
|
|
|
153
|
-
df = df[df.events.notnull() & ((df.batter == player.player_id)
|
|
154
|
-
|
|
157
|
+
df = df[df.events.notnull() & ((df.batter == player.player_id)
|
|
158
|
+
| (df.pitcher == player.player_id))]
|
|
159
|
+
df = df.sort_values(by="delta_home_win_exp",
|
|
160
|
+
key=None, ascending=is_home_player)
|
|
155
161
|
df = df.sort_values(by="at_bat_number", ascending=True)
|
|
156
162
|
|
|
157
163
|
if is_home_player:
|
|
@@ -164,6 +170,12 @@ class Game():
|
|
|
164
170
|
rows = [row for index, row in df.iterrows()]
|
|
165
171
|
return async_run(Play, self, rows)
|
|
166
172
|
|
|
173
|
+
@property
|
|
174
|
+
def homers(self) -> Dict[int, int]:
|
|
175
|
+
df: pd.DataFrame = self.__get_data()
|
|
176
|
+
df = df[df.events == "home_run"]
|
|
177
|
+
|
|
178
|
+
return df["batter"].value_counts().to_dict()
|
|
179
|
+
|
|
167
180
|
def __str__(self) -> str:
|
|
168
181
|
return f"{self._away.abbreviation} - {self._home.abbreviation}, Final: {self._away_score}-{self._home_score}, Date: {self._date}, GamePK: {self._game_pk}"
|
|
169
|
-
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
|
-
import io
|
|
5
4
|
import requests
|
|
6
5
|
import json
|
|
6
|
+
from functools import lru_cache
|
|
7
7
|
|
|
8
|
-
from typing import List,
|
|
8
|
+
from typing import List, TYPE_CHECKING
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from .game import Play
|
|
11
11
|
|
|
12
|
-
from .
|
|
12
|
+
from .date import Date
|
|
13
13
|
from .utils import async_run, dataframe_copy, dataframe_from_url
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
class Player():
|
|
16
17
|
|
|
18
|
+
@lru_cache(maxsize=100)
|
|
19
|
+
def __new__(cls, player_id: int):
|
|
20
|
+
return super().__new__(cls)
|
|
21
|
+
|
|
17
22
|
def __init__(self, player_id: int):
|
|
18
23
|
self._player_id: int = player_id
|
|
19
24
|
|
|
@@ -32,7 +37,7 @@ class Player():
|
|
|
32
37
|
|
|
33
38
|
def is_pitcher(self) -> bool:
|
|
34
39
|
return self._primary_position_abbr == "P"
|
|
35
|
-
|
|
40
|
+
|
|
36
41
|
def is_batter(self) -> bool:
|
|
37
42
|
return not self.is_pitcher()
|
|
38
43
|
|
|
@@ -75,33 +80,35 @@ class Player():
|
|
|
75
80
|
# Encapsulates mutable data by making a copy
|
|
76
81
|
@dataframe_copy
|
|
77
82
|
def get_homerun_data(self, season: int) -> pd.DataFrame:
|
|
78
|
-
|
|
83
|
+
df: pd.DataFrame = self.__get_homerun_data(season)
|
|
84
|
+
df = df.sort_values("game_date")
|
|
85
|
+
return df
|
|
79
86
|
|
|
80
87
|
def get_homerun_count(self, season: int) -> int:
|
|
81
88
|
return len(self.__get_homerun_data(season).index)
|
|
82
89
|
|
|
83
|
-
def get_homeruns(self, season: int
|
|
90
|
+
def get_homeruns(self, season: int) -> List[Play]:
|
|
84
91
|
from .game import Play, Game
|
|
85
92
|
|
|
86
93
|
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
94
|
|
|
92
95
|
games: List[Game] = async_run(Game, list(homerun_data["game_pk"]))
|
|
93
96
|
rows = [row for index, row in homerun_data.iterrows()]
|
|
94
97
|
|
|
95
98
|
return async_run(Play, games, rows)[::-1]
|
|
96
99
|
|
|
100
|
+
@classmethod
|
|
101
|
+
def generate_players(cls, player_ids: List[int]) -> List[Player]:
|
|
102
|
+
return async_run(cls, player_ids)
|
|
103
|
+
|
|
97
104
|
def __eq__(self, o: object) -> bool:
|
|
98
105
|
if not isinstance(o, Player):
|
|
99
106
|
return False
|
|
100
107
|
|
|
101
|
-
return self._player_id ==
|
|
108
|
+
return self._player_id == o._player_id
|
|
102
109
|
|
|
103
110
|
def __hash__(self) -> int:
|
|
104
|
-
return self._player_id
|
|
111
|
+
return hash(self._player_id)
|
|
105
112
|
|
|
106
113
|
def __str__(self) -> str:
|
|
107
|
-
return f"{self.__class__}@PlayerID={self._player_id}:FirstName={self._first_name}:LastName={self._last_name}"
|
|
114
|
+
return f"{self.__class__}@PlayerID={self._player_id}:FirstName={self._first_name}:LastName={self._last_name}"
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
|
-
import
|
|
2
|
+
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
from functools import cache
|
|
5
5
|
from typing import List, Dict
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class Team():
|
|
8
|
-
|
|
9
|
+
"""Team data class"""
|
|
10
|
+
_team_lookup: pd.DataFrame = pd.read_csv(
|
|
11
|
+
Path(__file__).parent / "data" / "team-info.csv"
|
|
12
|
+
)
|
|
9
13
|
|
|
10
|
-
# Speeds up and saves memory by caching the same type of objects
|
|
11
14
|
@cache
|
|
12
15
|
def __new__(cls, abbr: str):
|
|
16
|
+
"""Speeds up and saves memory by caching objects of the same team"""
|
|
13
17
|
return super().__new__(cls)
|
|
14
18
|
|
|
15
19
|
def __init__(self, abbr: str):
|
|
@@ -38,4 +42,4 @@ class Team():
|
|
|
38
42
|
return self._abbr == other._abbr
|
|
39
43
|
|
|
40
44
|
def __str__(self) -> str:
|
|
41
|
-
return f"{self.__class__}@Name={self._team}:Abbreviation={self._abbr}:ID={self._team_id}"
|
|
45
|
+
return f"{self.__class__}@Name={self._team}:Abbreviation={self._abbr}:ID={self._team_id}"
|
|
@@ -8,6 +8,7 @@ import functools
|
|
|
8
8
|
|
|
9
9
|
from typing import Any, List, Tuple
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
def async_run(func: callable, *args: Any | List[Any]) -> List[Any]:
|
|
12
13
|
# Makes all arguments into lists
|
|
13
14
|
try:
|
|
@@ -15,7 +16,8 @@ def async_run(func: callable, *args: Any | List[Any]) -> List[Any]:
|
|
|
15
16
|
except ValueError:
|
|
16
17
|
raise ValueError("At least one argument must be a list")
|
|
17
18
|
|
|
18
|
-
repeated_args = [arg if isinstance(arg, list) else list(
|
|
19
|
+
repeated_args = [arg if isinstance(arg, list) else list(
|
|
20
|
+
itertools.repeat(arg, max_length)) for arg in args]
|
|
19
21
|
|
|
20
22
|
# Ensure all list arguments are of the same length
|
|
21
23
|
if not all(len(i) == len(repeated_args[0]) for i in repeated_args):
|
|
@@ -25,13 +27,15 @@ def async_run(func: callable, *args: Any | List[Any]) -> List[Any]:
|
|
|
25
27
|
return func(*args)
|
|
26
28
|
|
|
27
29
|
async def generate() -> List[Any]:
|
|
28
|
-
tasks = [asyncio.create_task(create(*args))
|
|
30
|
+
tasks = [asyncio.create_task(create(*args))
|
|
31
|
+
for args in zip(*repeated_args)]
|
|
29
32
|
|
|
30
33
|
await asyncio.gather(*tasks)
|
|
31
34
|
return [task.result() for task in tasks]
|
|
32
35
|
|
|
33
36
|
return asyncio.run(generate())
|
|
34
37
|
|
|
38
|
+
|
|
35
39
|
def copy_cache(func):
|
|
36
40
|
func = functools.cache(func)
|
|
37
41
|
|
|
@@ -40,13 +44,15 @@ def copy_cache(func):
|
|
|
40
44
|
|
|
41
45
|
return wrapper
|
|
42
46
|
|
|
47
|
+
|
|
43
48
|
def dataframe_copy(func):
|
|
44
49
|
def wrapper(*args, **kwargs):
|
|
45
50
|
return func(*args, **kwargs).copy()
|
|
46
51
|
|
|
47
52
|
return wrapper
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
|
|
55
|
+
def dataframe_from_url(func, use_cache: bool = True):
|
|
50
56
|
if use_cache:
|
|
51
57
|
func = functools.cache(func)
|
|
52
58
|
|
|
@@ -56,4 +62,4 @@ def dataframe_from_url(func, use_cache: bool=True):
|
|
|
56
62
|
|
|
57
63
|
return pd.read_csv(io.StringIO(csv.decode('utf-8')))
|
|
58
64
|
|
|
59
|
-
return wrapper
|
|
65
|
+
return wrapper
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mlbrecaps
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Package that gathers information on given MLB games
|
|
5
5
|
Home-page: https://github.com/MrRedwing/MLB-Recaps
|
|
6
6
|
Author: Karsten Larson
|
|
7
7
|
Author-email: karsten.larson.1@gmail.com
|
|
8
8
|
License: MIT
|
|
9
|
+
Platform: UNKNOWN
|
|
9
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
@@ -65,3 +66,5 @@ All contributions are welcomed to improve the mlbrecaps package. To contribute s
|
|
|
65
66
|
### License
|
|
66
67
|
|
|
67
68
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
69
|
+
|
|
70
|
+
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
LICENSE.txt
|
|
2
2
|
README.md
|
|
3
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
4
|
mlbrecaps/__init__.py
|
|
10
5
|
mlbrecaps/__main__.py
|
|
11
6
|
mlbrecaps/clip.py
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from setuptools import find_packages, setup
|
|
2
2
|
|
|
3
3
|
with open("README.md", "r") as f:
|
|
4
|
-
|
|
4
|
+
long_description = f.read()
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="mlbrecaps",
|
|
8
|
-
version="0.0.
|
|
8
|
+
version="0.0.2",
|
|
9
9
|
description="Package that gathers information on given MLB games",
|
|
10
10
|
packages=find_packages(include=["mlbrecaps"]),
|
|
11
11
|
long_description=long_description,
|
|
@@ -1,21 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|