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.
Files changed (42) hide show
  1. mlbrecaps-0.0.2/LICENSE.txt → mlbrecaps-0.1.0/LICENSE.md +2 -2
  2. mlbrecaps-0.1.0/PKG-INFO +78 -0
  3. mlbrecaps-0.1.0/README.md +62 -0
  4. mlbrecaps-0.1.0/mlbrecaps/__init__.py +21 -0
  5. mlbrecaps-0.1.0/mlbrecaps/broadcast.py +9 -0
  6. mlbrecaps-0.1.0/mlbrecaps/clip.py +112 -0
  7. mlbrecaps-0.1.0/mlbrecaps/date.py +33 -0
  8. mlbrecaps-0.1.0/mlbrecaps/game.py +108 -0
  9. mlbrecaps-0.1.0/mlbrecaps/game_play_ids.py +33 -0
  10. mlbrecaps-0.1.0/mlbrecaps/games.py +98 -0
  11. mlbrecaps-0.1.0/mlbrecaps/play.py +202 -0
  12. mlbrecaps-0.1.0/mlbrecaps/player.py +25 -0
  13. mlbrecaps-0.1.0/mlbrecaps/plays.py +201 -0
  14. mlbrecaps-0.1.0/mlbrecaps/team.py +46 -0
  15. mlbrecaps-0.1.0/mlbrecaps/utils.py +79 -0
  16. mlbrecaps-0.1.0/mlbrecaps.egg-info/PKG-INFO +78 -0
  17. {mlbrecaps-0.0.2 → mlbrecaps-0.1.0}/mlbrecaps.egg-info/SOURCES.txt +7 -10
  18. mlbrecaps-0.1.0/mlbrecaps.egg-info/requires.txt +7 -0
  19. mlbrecaps-0.1.0/pyproject.toml +15 -0
  20. mlbrecaps-0.0.2/PKG-INFO +0 -70
  21. mlbrecaps-0.0.2/README.md +0 -51
  22. mlbrecaps-0.0.2/mlbrecaps/__init__.py +0 -13
  23. mlbrecaps-0.0.2/mlbrecaps/__main__.py +0 -25
  24. mlbrecaps-0.0.2/mlbrecaps/clip.py +0 -108
  25. mlbrecaps-0.0.2/mlbrecaps/clips.py +0 -70
  26. mlbrecaps-0.0.2/mlbrecaps/data/team-info.csv +0 -33
  27. mlbrecaps-0.0.2/mlbrecaps/date.py +0 -78
  28. mlbrecaps-0.0.2/mlbrecaps/date_generator.py +0 -55
  29. mlbrecaps-0.0.2/mlbrecaps/date_range.py +0 -21
  30. mlbrecaps-0.0.2/mlbrecaps/game.py +0 -181
  31. mlbrecaps-0.0.2/mlbrecaps/game_generator.py +0 -93
  32. mlbrecaps-0.0.2/mlbrecaps/play.py +0 -66
  33. mlbrecaps-0.0.2/mlbrecaps/player.py +0 -114
  34. mlbrecaps-0.0.2/mlbrecaps/scripts.py +0 -56
  35. mlbrecaps-0.0.2/mlbrecaps/team.py +0 -45
  36. mlbrecaps-0.0.2/mlbrecaps/utils.py +0 -65
  37. mlbrecaps-0.0.2/mlbrecaps.egg-info/PKG-INFO +0 -70
  38. mlbrecaps-0.0.2/mlbrecaps.egg-info/requires.txt +0 -8
  39. mlbrecaps-0.0.2/setup.py +0 -33
  40. {mlbrecaps-0.0.2 → mlbrecaps-0.1.0}/mlbrecaps.egg-info/dependency_links.txt +0 -0
  41. {mlbrecaps-0.0.2 → mlbrecaps-0.1.0}/mlbrecaps.egg-info/top_level.txt +0 -0
  42. {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) 2024 Karsten Larson
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.
@@ -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,9 @@
1
+ from enum import Enum, auto
2
+
3
+ class BroadcastType(Enum):
4
+ """
5
+ Enum representing different types of broadcasts.
6
+ """
7
+ HOME = auto()
8
+ AWAY = auto()
9
+ NETWORK = auto()
@@ -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)