mlbrecaps 0.0.2__tar.gz → 0.1.0__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.2/LICENSE.txt → mlbrecaps-0.1.0/LICENSE.md +2 -2
- mlbrecaps-0.1.0/PKG-INFO +78 -0
- mlbrecaps-0.1.0/README.md +62 -0
- mlbrecaps-0.1.0/mlbrecaps/__init__.py +21 -0
- mlbrecaps-0.1.0/mlbrecaps/broadcast.py +9 -0
- mlbrecaps-0.1.0/mlbrecaps/clip.py +112 -0
- mlbrecaps-0.1.0/mlbrecaps/date.py +33 -0
- mlbrecaps-0.1.0/mlbrecaps/game.py +108 -0
- mlbrecaps-0.1.0/mlbrecaps/game_play_ids.py +33 -0
- mlbrecaps-0.1.0/mlbrecaps/games.py +98 -0
- mlbrecaps-0.1.0/mlbrecaps/play.py +202 -0
- mlbrecaps-0.1.0/mlbrecaps/player.py +25 -0
- mlbrecaps-0.1.0/mlbrecaps/plays.py +201 -0
- mlbrecaps-0.1.0/mlbrecaps/team.py +46 -0
- mlbrecaps-0.1.0/mlbrecaps/utils.py +79 -0
- mlbrecaps-0.1.0/mlbrecaps.egg-info/PKG-INFO +78 -0
- {mlbrecaps-0.0.2 → mlbrecaps-0.1.0}/mlbrecaps.egg-info/SOURCES.txt +7 -10
- mlbrecaps-0.1.0/mlbrecaps.egg-info/requires.txt +7 -0
- mlbrecaps-0.1.0/pyproject.toml +15 -0
- mlbrecaps-0.0.2/PKG-INFO +0 -70
- mlbrecaps-0.0.2/README.md +0 -51
- mlbrecaps-0.0.2/mlbrecaps/__init__.py +0 -13
- mlbrecaps-0.0.2/mlbrecaps/__main__.py +0 -25
- mlbrecaps-0.0.2/mlbrecaps/clip.py +0 -108
- mlbrecaps-0.0.2/mlbrecaps/clips.py +0 -70
- mlbrecaps-0.0.2/mlbrecaps/data/team-info.csv +0 -33
- mlbrecaps-0.0.2/mlbrecaps/date.py +0 -78
- mlbrecaps-0.0.2/mlbrecaps/date_generator.py +0 -55
- mlbrecaps-0.0.2/mlbrecaps/date_range.py +0 -21
- mlbrecaps-0.0.2/mlbrecaps/game.py +0 -181
- mlbrecaps-0.0.2/mlbrecaps/game_generator.py +0 -93
- mlbrecaps-0.0.2/mlbrecaps/play.py +0 -66
- mlbrecaps-0.0.2/mlbrecaps/player.py +0 -114
- mlbrecaps-0.0.2/mlbrecaps/scripts.py +0 -56
- mlbrecaps-0.0.2/mlbrecaps/team.py +0 -45
- mlbrecaps-0.0.2/mlbrecaps/utils.py +0 -65
- mlbrecaps-0.0.2/mlbrecaps.egg-info/PKG-INFO +0 -70
- mlbrecaps-0.0.2/mlbrecaps.egg-info/requires.txt +0 -8
- mlbrecaps-0.0.2/setup.py +0 -33
- {mlbrecaps-0.0.2 → mlbrecaps-0.1.0}/mlbrecaps.egg-info/dependency_links.txt +0 -0
- {mlbrecaps-0.0.2 → mlbrecaps-0.1.0}/mlbrecaps.egg-info/top_level.txt +0 -0
- {mlbrecaps-0.0.2 → mlbrecaps-0.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2025 Karsten Larson
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
mlbrecaps-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mlbrecaps
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Package that gathers information on given MLB games and allows downloading of video clips of plays.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE.md
|
|
8
|
+
Requires-Dist: bs4>=0.0.2
|
|
9
|
+
Requires-Dist: curl-cffi>=0.13.0
|
|
10
|
+
Requires-Dist: fireducks>=1.3.3
|
|
11
|
+
Requires-Dist: lark>=1.2.2
|
|
12
|
+
Requires-Dist: lxml>=6.0.0
|
|
13
|
+
Requires-Dist: pydantic>=2.11.7
|
|
14
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# mlbrecaps
|
|
18
|
+
|
|
19
|
+
mlbrecaps is a Python library for querying and retrieving highlight videos and play information from Major League Baseball (MLB) games. It provides a simple interface to access game recaps, top plays, and player highlights programmatically.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- Query highlight videos for specific MLB games
|
|
24
|
+
- Retrieve top plays for a given day, month, or year
|
|
25
|
+
- Get player-specific highlight clips
|
|
26
|
+
- Easily integrate with your own Python scripts
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
You can install mlbrecaps directly from PyPI using pip:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install mlbrecaps
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Install from Source
|
|
37
|
+
|
|
38
|
+
1. **Clone the repository:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/yourusername/mlbrecaps.git
|
|
42
|
+
cd mlbrecaps
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. **Install dependencies with uv:**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv pip install -e .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This will install the package in editable mode along with all required dependencies.
|
|
52
|
+
|
|
53
|
+
## Example Scripts
|
|
54
|
+
|
|
55
|
+
The `examples/` directory contains ready-to-run scripts:
|
|
56
|
+
|
|
57
|
+
- `examples/top_player_plays.py` — Get top plays for a player
|
|
58
|
+
- `examples/top_plays_of_month.py` — Get top plays for a month
|
|
59
|
+
- `examples/top_plays_of_year.py` — Get top plays for a year
|
|
60
|
+
|
|
61
|
+
Run an example with:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python examples/top_player_plays.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Contributing
|
|
68
|
+
|
|
69
|
+
Contributions are welcome! To contribute:
|
|
70
|
+
|
|
71
|
+
1. Fork the repository and create your branch.
|
|
72
|
+
2. Make your changes and add tests if applicable.
|
|
73
|
+
3. Ensure code style and formatting are consistent.
|
|
74
|
+
4. Submit a pull request with a clear description of your changes.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
This project is open source and available under the MIT License.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# mlbrecaps
|
|
2
|
+
|
|
3
|
+
mlbrecaps is a Python library for querying and retrieving highlight videos and play information from Major League Baseball (MLB) games. It provides a simple interface to access game recaps, top plays, and player highlights programmatically.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Query highlight videos for specific MLB games
|
|
8
|
+
- Retrieve top plays for a given day, month, or year
|
|
9
|
+
- Get player-specific highlight clips
|
|
10
|
+
- Easily integrate with your own Python scripts
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
You can install mlbrecaps directly from PyPI using pip:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install mlbrecaps
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Install from Source
|
|
21
|
+
|
|
22
|
+
1. **Clone the repository:**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/yourusername/mlbrecaps.git
|
|
26
|
+
cd mlbrecaps
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. **Install dependencies with uv:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This will install the package in editable mode along with all required dependencies.
|
|
36
|
+
|
|
37
|
+
## Example Scripts
|
|
38
|
+
|
|
39
|
+
The `examples/` directory contains ready-to-run scripts:
|
|
40
|
+
|
|
41
|
+
- `examples/top_player_plays.py` — Get top plays for a player
|
|
42
|
+
- `examples/top_plays_of_month.py` — Get top plays for a month
|
|
43
|
+
- `examples/top_plays_of_year.py` — Get top plays for a year
|
|
44
|
+
|
|
45
|
+
Run an example with:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python examples/top_player_plays.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Contributing
|
|
52
|
+
|
|
53
|
+
Contributions are welcome! To contribute:
|
|
54
|
+
|
|
55
|
+
1. Fork the repository and create your branch.
|
|
56
|
+
2. Make your changes and add tests if applicable.
|
|
57
|
+
3. Ensure code style and formatting are consistent.
|
|
58
|
+
4. Submit a pull request with a clear description of your changes.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
This project is open source and available under the MIT License.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .date import Date, Season
|
|
2
|
+
from .games import Games
|
|
3
|
+
from .game import Game
|
|
4
|
+
from .plays import Play, PlayField
|
|
5
|
+
from .team import Team
|
|
6
|
+
from .broadcast import BroadcastType
|
|
7
|
+
from .clip import Clip
|
|
8
|
+
from .player import Player
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Date",
|
|
12
|
+
"Season",
|
|
13
|
+
"Games",
|
|
14
|
+
"Game",
|
|
15
|
+
"Play",
|
|
16
|
+
"PlayField",
|
|
17
|
+
"Team",
|
|
18
|
+
"BroadcastType",
|
|
19
|
+
"Clip",
|
|
20
|
+
"Player"
|
|
21
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from bs4 import BeautifulSoup, Tag
|
|
2
|
+
|
|
3
|
+
from curl_cffi.requests.exceptions import Timeout
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .play import Play
|
|
7
|
+
from .utils import fetch_html_from_url, fetch_url
|
|
8
|
+
from .broadcast import BroadcastType
|
|
9
|
+
from .team import Team
|
|
10
|
+
|
|
11
|
+
class Clip():
|
|
12
|
+
"""A wrapper class for Play that allows for plays to be downloaded"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, play: Play, broadcast_type: Team | BroadcastType | None = None):
|
|
15
|
+
self._play: Play = play
|
|
16
|
+
self.broadcast_type: BroadcastType | None = None
|
|
17
|
+
|
|
18
|
+
# Find the broadcast type based on the given team
|
|
19
|
+
if isinstance(broadcast_type, Team):
|
|
20
|
+
if self._play.home_team == broadcast_type.name:
|
|
21
|
+
self.broadcast_type = BroadcastType.HOME
|
|
22
|
+
else:
|
|
23
|
+
self.broadcast_type = BroadcastType.AWAY
|
|
24
|
+
else:
|
|
25
|
+
self.broadcast_type: BroadcastType | None = broadcast_type
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def play(self) -> Play:
|
|
29
|
+
return self._play
|
|
30
|
+
|
|
31
|
+
async def __get_url(self, site_url: str) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Gets the url of the clip to be downloaded from the savant clip
|
|
34
|
+
"""
|
|
35
|
+
# Get the savant site
|
|
36
|
+
site = await fetch_html_from_url(site_url)
|
|
37
|
+
|
|
38
|
+
# Find the video element of the savant clip, find the source url of the clip
|
|
39
|
+
soup = BeautifulSoup(site, features="lxml")
|
|
40
|
+
video_obj = soup.find("video", id="sporty")
|
|
41
|
+
|
|
42
|
+
if not isinstance(video_obj, Tag):
|
|
43
|
+
raise ValueError("Clip url is not found")
|
|
44
|
+
|
|
45
|
+
source = video_obj.find('source')
|
|
46
|
+
|
|
47
|
+
if not isinstance(source, Tag):
|
|
48
|
+
raise ValueError("Clip url is not found")
|
|
49
|
+
|
|
50
|
+
clip_url = source.get('src')
|
|
51
|
+
|
|
52
|
+
if not isinstance(clip_url, str) or clip_url is None:
|
|
53
|
+
raise ValueError("Clip url is not found or not a string")
|
|
54
|
+
|
|
55
|
+
# Return the source url of the clip so it can be downloaded later
|
|
56
|
+
return clip_url
|
|
57
|
+
|
|
58
|
+
async def __generate(self) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Generates a savant clip based on the given at-bat information
|
|
61
|
+
|
|
62
|
+
Row must be a pandas dataframe row.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# find the broadcast type so it's always corresponding
|
|
66
|
+
# to the given batter's home team's broadcast
|
|
67
|
+
if self.broadcast_type:
|
|
68
|
+
broadcast_type = self.broadcast_type
|
|
69
|
+
elif self._play.inning_topbot == "TOP":
|
|
70
|
+
broadcast_type = BroadcastType.AWAY
|
|
71
|
+
else:
|
|
72
|
+
broadcast_type = BroadcastType.HOME
|
|
73
|
+
|
|
74
|
+
# with the play id find the url for the savant clip
|
|
75
|
+
site_url = f"https://baseballsavant.mlb.com/sporty-videos?playId={self._play.play_id}&videoType={broadcast_type.name}"
|
|
76
|
+
clip_url = await self.__get_url(site_url)
|
|
77
|
+
|
|
78
|
+
# if the clip is alright return it
|
|
79
|
+
if clip_url != "":
|
|
80
|
+
return clip_url
|
|
81
|
+
|
|
82
|
+
if broadcast_type == BroadcastType.NETWORK:
|
|
83
|
+
raise ValueError("Clip url is not found or not a string")
|
|
84
|
+
|
|
85
|
+
# if the clip is screwed up then it was a national tv game
|
|
86
|
+
# return the correct national tv clip url
|
|
87
|
+
site_url = f"https://baseballsavant.mlb.com/sporty-videos?playId={self._play.play_id}&videoType=NETWORK"
|
|
88
|
+
clip_url = await self.__get_url(site_url)
|
|
89
|
+
|
|
90
|
+
return clip_url
|
|
91
|
+
|
|
92
|
+
async def download(self, path: str | Path, verbose: bool = False) -> Path:
|
|
93
|
+
path = Path(path)
|
|
94
|
+
|
|
95
|
+
clip_url = await self.__generate()
|
|
96
|
+
|
|
97
|
+
# create response object
|
|
98
|
+
try:
|
|
99
|
+
r = await fetch_url(clip_url)
|
|
100
|
+
except Timeout:
|
|
101
|
+
print(f'Timeout has been raised. Link: {clip_url}')
|
|
102
|
+
raise
|
|
103
|
+
|
|
104
|
+
# Download video
|
|
105
|
+
with path.open("wb") as f:
|
|
106
|
+
f.write(r.content)
|
|
107
|
+
|
|
108
|
+
# State the video was successfully downloaded
|
|
109
|
+
if verbose:
|
|
110
|
+
print(f"Successfully downloaded: {path.absolute()}")
|
|
111
|
+
|
|
112
|
+
return path
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
def parse_date(v: str | date) -> date:
|
|
5
|
+
if isinstance(v, str):
|
|
6
|
+
return date.fromisoformat(v)
|
|
7
|
+
return v
|
|
8
|
+
|
|
9
|
+
class Date():
|
|
10
|
+
def __init__(self, start_date: date | str, end_date: Optional[date | str] = None):
|
|
11
|
+
self._start_date = parse_date(start_date)
|
|
12
|
+
self._end_date = parse_date(end_date) if end_date else self._start_date
|
|
13
|
+
|
|
14
|
+
if self._start_date > self._end_date:
|
|
15
|
+
raise ValueError("start_date must be less than or equal to end_date")
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def start_date(self) -> date:
|
|
19
|
+
return self._start_date
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def end_date(self) -> date:
|
|
23
|
+
return self._end_date
|
|
24
|
+
|
|
25
|
+
class Season(Date):
|
|
26
|
+
"""
|
|
27
|
+
Represents a season in MLB.
|
|
28
|
+
Inherits from Date to provide start and end dates.
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self, year: int):
|
|
31
|
+
start_date = date(year, 1, 1) # Assuming season starts in March
|
|
32
|
+
end_date = date(year, 12, 31) # Assuming season ends in November
|
|
33
|
+
super().__init__(start_date, end_date)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from .plays import Plays
|
|
5
|
+
|
|
6
|
+
class Status(BaseModel):
|
|
7
|
+
model_config = ConfigDict(frozen=True)
|
|
8
|
+
abstractGameState: str
|
|
9
|
+
codedGameState: str
|
|
10
|
+
detailedState: str
|
|
11
|
+
statusCode: str
|
|
12
|
+
startTimeTBD: bool
|
|
13
|
+
abstractGameCode: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LeagueRecord(BaseModel):
|
|
17
|
+
model_config = ConfigDict(frozen=True)
|
|
18
|
+
wins: int
|
|
19
|
+
losses: int
|
|
20
|
+
pct: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Team(BaseModel):
|
|
24
|
+
model_config = ConfigDict(frozen=True)
|
|
25
|
+
id: int
|
|
26
|
+
name: str
|
|
27
|
+
link: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TeamResult(BaseModel):
|
|
31
|
+
model_config = ConfigDict(frozen=True)
|
|
32
|
+
leagueRecord: LeagueRecord
|
|
33
|
+
score: Optional[int] = None
|
|
34
|
+
team: Team
|
|
35
|
+
isWinner: Optional[bool] = None
|
|
36
|
+
splitSquad: Optional[bool] = None
|
|
37
|
+
seriesNumber: Optional[int] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Teams(BaseModel):
|
|
41
|
+
model_config = ConfigDict(frozen=True)
|
|
42
|
+
away: TeamResult
|
|
43
|
+
home: TeamResult
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Venue(BaseModel):
|
|
47
|
+
model_config = ConfigDict(frozen=True)
|
|
48
|
+
id: int
|
|
49
|
+
name: str
|
|
50
|
+
link: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Content(BaseModel):
|
|
54
|
+
model_config = ConfigDict(frozen=True)
|
|
55
|
+
link: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Game(BaseModel):
|
|
59
|
+
model_config = ConfigDict(frozen=True)
|
|
60
|
+
gamePk: int
|
|
61
|
+
gameGuid: str
|
|
62
|
+
link: str
|
|
63
|
+
gameType: str
|
|
64
|
+
season: str
|
|
65
|
+
gameDate: str
|
|
66
|
+
officialDate: str
|
|
67
|
+
status: Status
|
|
68
|
+
teams: Teams
|
|
69
|
+
venue: Venue
|
|
70
|
+
content: Content
|
|
71
|
+
# isTie: Optional[bool] = None
|
|
72
|
+
gameNumber: int
|
|
73
|
+
publicFacing: bool
|
|
74
|
+
doubleHeader: str
|
|
75
|
+
gamedayType: str
|
|
76
|
+
tiebreaker: str
|
|
77
|
+
calendarEventID: str
|
|
78
|
+
seasonDisplay: str
|
|
79
|
+
dayNight: str
|
|
80
|
+
scheduledInnings: int
|
|
81
|
+
reverseHomeAwayStatus: bool
|
|
82
|
+
inningBreakLength: int
|
|
83
|
+
gamesInSeries: Optional[int] = None
|
|
84
|
+
seriesGameNumber: Optional[int] = None
|
|
85
|
+
seriesDescription: Optional[str] = None
|
|
86
|
+
recordSource: str
|
|
87
|
+
ifNecessary: str
|
|
88
|
+
ifNecessaryDescription: str
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def plays(self) -> Plays:
|
|
92
|
+
"""Returns a Plays instance for the game."""
|
|
93
|
+
return Plays([self.gamePk])
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def is_final(self) -> bool:
|
|
97
|
+
"""Returns True if the game is final."""
|
|
98
|
+
return self.status.codedGameState == "F"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_regular_season(self) -> bool:
|
|
102
|
+
"""Returns True if the game is a regular season game."""
|
|
103
|
+
return self.gameType == "R"
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def is_valid_game(self) -> bool:
|
|
107
|
+
"""Returns True if the game is a valid regular season game."""
|
|
108
|
+
return self.is_final and self.is_regular_season
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
class Scoreboard(BaseModel):
|
|
5
|
+
gamePk: int
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlayItem(BaseModel):
|
|
9
|
+
play_id: str
|
|
10
|
+
inning: int
|
|
11
|
+
ab_number: int
|
|
12
|
+
cap_index: int
|
|
13
|
+
outs: int
|
|
14
|
+
batter: int
|
|
15
|
+
pitcher: int
|
|
16
|
+
pitch_number: int
|
|
17
|
+
player_total_pitches: int
|
|
18
|
+
game_total_pitches: int
|
|
19
|
+
rowId: str
|
|
20
|
+
game_pk: int
|
|
21
|
+
|
|
22
|
+
class GamePlayIds(BaseModel):
|
|
23
|
+
game_status_code: str
|
|
24
|
+
game_status: str
|
|
25
|
+
gamedayType: str
|
|
26
|
+
gameDate: str
|
|
27
|
+
scoreboard: Scoreboard
|
|
28
|
+
team_home: list[PlayItem] = []
|
|
29
|
+
team_away: list[PlayItem] = []
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def play_data(self) -> list[PlayItem]:
|
|
33
|
+
return self.team_home + self.team_away
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterator
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
from .date import Date
|
|
8
|
+
from .plays import Plays
|
|
9
|
+
from .game import Game
|
|
10
|
+
from .team import Team
|
|
11
|
+
from .utils import fetch_model_from_url
|
|
12
|
+
|
|
13
|
+
class GameDate(BaseModel):
|
|
14
|
+
model_config = ConfigDict(frozen=True)
|
|
15
|
+
date: str
|
|
16
|
+
totalGames: int
|
|
17
|
+
totalGamesInProgress: int
|
|
18
|
+
games: list[Game]
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def final_games(self) -> list[Game]:
|
|
22
|
+
"""Returns a list of final games for the date."""
|
|
23
|
+
return [game for game in self.games if game.is_valid_game]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Games(BaseModel):
|
|
27
|
+
model_config = ConfigDict(frozen=True)
|
|
28
|
+
totalGames: int
|
|
29
|
+
totalGamesInProgress: int
|
|
30
|
+
dates: list[GameDate]
|
|
31
|
+
|
|
32
|
+
@cached_property
|
|
33
|
+
def game_pks(self) -> set[int]:
|
|
34
|
+
"""Returns a list of game Pks."""
|
|
35
|
+
return {game.gamePk for date in self.dates for game in date.final_games}
|
|
36
|
+
|
|
37
|
+
@cached_property
|
|
38
|
+
def games_by_pk(self) -> dict[int, Game]:
|
|
39
|
+
"""Returns a dictionary mapping game Pks to Game objects."""
|
|
40
|
+
return {game.gamePk: game for date in self.dates for game in date.final_games}
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
43
|
+
def games_by_date(self) -> dict[str, list[Game]]:
|
|
44
|
+
"""Returns a dictionary mapping dates to lists of Game objects."""
|
|
45
|
+
return {date.date: date.final_games for date in self.dates}
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def games(self) -> list[Game]:
|
|
49
|
+
"""Returns a flat list of all Game objects."""
|
|
50
|
+
return [game for date in self.dates for game in date.final_games]
|
|
51
|
+
|
|
52
|
+
@cached_property
|
|
53
|
+
def plays(self) -> Plays:
|
|
54
|
+
return Plays(self.game_pks)
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def games_by_team(self) -> dict[int, list[Game]]:
|
|
58
|
+
"""Returns a dictionary mapping team IDs to lists of Game objects."""
|
|
59
|
+
team_games = {}
|
|
60
|
+
for date in self.dates:
|
|
61
|
+
for game in date.final_games:
|
|
62
|
+
team_id = game.teams.away.team.id
|
|
63
|
+
if team_id not in team_games:
|
|
64
|
+
team_games[team_id] = []
|
|
65
|
+
team_games[team_id].append(game)
|
|
66
|
+
team_id = game.teams.home.team.id
|
|
67
|
+
if team_id not in team_games:
|
|
68
|
+
team_games[team_id] = []
|
|
69
|
+
team_games[team_id].append(game)
|
|
70
|
+
return team_games
|
|
71
|
+
|
|
72
|
+
def iter_games(self) -> Iterator[Game]:
|
|
73
|
+
"""Returns an iterator over all Game objects."""
|
|
74
|
+
return iter(self.games)
|
|
75
|
+
|
|
76
|
+
def __len__(self) -> int:
|
|
77
|
+
"""Returns the total number of games."""
|
|
78
|
+
return len(self.games)
|
|
79
|
+
|
|
80
|
+
def __add__(self, other: Games) -> Games:
|
|
81
|
+
return Games(
|
|
82
|
+
totalGames=self.totalGames + other.totalGames,
|
|
83
|
+
totalGamesInProgress=self.totalGamesInProgress + other.totalGamesInProgress,
|
|
84
|
+
dates=self.dates + other.dates
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
async def get_games(date: Date) -> Games:
|
|
89
|
+
url = f'https://statsapi.mlb.com/api/v1/schedule?startDate={date.start_date}&endDate={date.end_date}&sportId=1'
|
|
90
|
+
|
|
91
|
+
return await fetch_model_from_url(url, Games)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
async def get_games_by_team(team: Team, date: Date) -> Games:
|
|
95
|
+
"""Fetches games for a specific team within a date range."""
|
|
96
|
+
url = f'https://statsapi.mlb.com/api/v1/schedule?startDate={date.start_date}&endDate={date.end_date}&sportId=1&teamId={team.value}'
|
|
97
|
+
|
|
98
|
+
return await fetch_model_from_url(url, Games)
|