baseballcommand 0.0.2__tar.gz → 0.0.4__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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baseballcommand
3
- Version: 0.0.2
4
- Summary: This is a tool that will retrieve Major League Baseball scores and stats.
3
+ Version: 0.0.4
4
+ Summary: This is a tool that will retrieve Major League Baseball scores and stats in your command line.
5
5
  License-Expression: MIT
6
6
  Author: Owen Powell
7
7
  Author-email: opowell2899@gmail.com
@@ -10,5 +10,6 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Python :: 3.15
13
14
  Requires-Dist: argparse (>=1.4.0,<2.0.0)
14
15
  Requires-Dist: requests (>=2.34.2,<3.0.0)
@@ -1,7 +1,11 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
1
5
  [project]
2
6
  name = "baseballcommand"
3
- version = "0.0.2"
4
- description = "This is a tool that will retrieve Major League Baseball scores and stats."
7
+ version = "0.0.4"
8
+ description = "This is a tool that will retrieve Major League Baseball scores and stats in your command line."
5
9
  authors = [
6
10
  {name = "Owen Powell",email = "opowell2899@gmail.com"}
7
11
  ]
@@ -15,10 +19,6 @@ dependencies = [
15
19
  [tool.poetry.scripts]
16
20
  base = "baseballcommand:cli" # CLI entry point
17
21
 
18
- [build-system]
19
- requires = ["poetry-core>=2.0.0,<3.0.0"]
20
- build-backend = "poetry.core.masonry.api"
21
-
22
22
  [dependency-groups]
23
23
  dev = [
24
24
  "ruff (>=0.16.5,<0.17.0)"
@@ -0,0 +1,24 @@
1
+ import argparse
2
+
3
+ from .commands import players, schedule
4
+
5
+
6
+ def cli() -> None:
7
+ parser = argparse.ArgumentParser(prog='BaseballCommand')
8
+ subparsers = parser.add_subparsers(dest='command', required=True)
9
+
10
+ subparsers.add_parser('leagues', help='Return a list of MLB leagues.')
11
+ subparsers.add_parser('schedule', help='Return a list of todays games.')
12
+
13
+ player_parser = subparsers.add_parser('player', help='Return stats for the specified player for the current season.')
14
+ player_parser.add_argument('first_name')
15
+ player_parser.add_argument('last_name')
16
+
17
+ args = parser.parse_args()
18
+
19
+ if args.command == 'player':
20
+ players.player_search(f'{args.first_name}', f'{args.last_name}')
21
+ elif args.command == 'leagues':
22
+ schedule.get_leagues()
23
+ elif args.command == 'schedule':
24
+ schedule.get_todays_schedule()
@@ -0,0 +1,133 @@
1
+ import difflib
2
+ import re
3
+ import unicodedata
4
+ from datetime import datetime
5
+ from zoneinfo import ZoneInfo
6
+
7
+ import requests
8
+
9
+ BASE_URL = 'https://statsapi.mlb.com/api/v1'
10
+
11
+ HITTING_COLUMNS = [
12
+ ('G', 'gamesPlayed'), ('AB', 'atBats'), ('R', 'runs'), ('H', 'hits'), ('2B', 'doubles'),
13
+ ('3B', 'triples'), ('HR', 'homeRuns'), ('RBI', 'rbi'), ('SB', 'stolenBases'), ('BB', 'baseOnBalls'),
14
+ ('SO', 'strikeOuts'), ('AVG', 'avg'), ('OBP', 'obp'), ('SLG', 'slg'), ('OPS', 'ops'),
15
+ ]
16
+ PITCHING_COLUMNS = [
17
+ ('G', 'gamesPitched'), ('GS', 'gamesStarted'), ('W', 'wins'), ('L', 'losses'), ('SV', 'saves'),
18
+ ('IP', 'inningsPitched'), ('H', 'hits'), ('ER', 'earnedRuns'), ('BB', 'baseOnBalls'),
19
+ ('SO', 'strikeOuts'), ('HR', 'homeRuns'), ('ERA', 'era'), ('WHIP', 'whip'),
20
+ ]
21
+
22
+
23
+ def normalize_name(name):
24
+ # Strip accents (Acuña -> acuna), punctuation, and suffixes so user input matches API names
25
+ name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode()
26
+ name = re.sub(r'[^a-z ]', '', name.lower().replace('-', ' '))
27
+ words = [word for word in name.split() if word not in ('jr', 'sr', 'ii', 'iii', 'iv')]
28
+ return ' '.join(words)
29
+
30
+
31
+ def get_active_players(season):
32
+ response = requests.get(f'{BASE_URL}/sports/1/players?season={season}')
33
+ response.raise_for_status()
34
+ return response.json().get('people', [])
35
+
36
+
37
+ def find_players(name, players):
38
+ target = normalize_name(name)
39
+ names = {}
40
+ for player in players:
41
+ # Match on both the full name and the name the player goes by (e.g. "Mike" vs "Michael")
42
+ keys = {normalize_name(player['fullName']),
43
+ normalize_name(f'{player.get("useName", "")} {player.get("lastName", "")}')}
44
+ for key in keys:
45
+ names.setdefault(key, []).append(player)
46
+
47
+ if target in names:
48
+ return names[target]
49
+
50
+ close = difflib.get_close_matches(target, names.keys(), n=5, cutoff=0.75)
51
+ matches = []
52
+ for key in close:
53
+ for player in names[key]:
54
+ if player not in matches:
55
+ matches.append(player)
56
+ return matches
57
+
58
+
59
+ def choose_player(matches):
60
+ print('Multiple players found:')
61
+ for i, player in enumerate(matches, start=1):
62
+ print(f' {i}. {player["fullName"]} ({player["primaryPosition"]["abbreviation"]})')
63
+
64
+ try:
65
+ choice = int(input('Select a player: '))
66
+ except (ValueError, EOFError):
67
+ return None
68
+ return matches[choice - 1] if 1 <= choice <= len(matches) else None
69
+
70
+
71
+ def get_season_stats(player_id, group, season):
72
+ response = requests.get(f'{BASE_URL}/people/{player_id}/stats?stats=season&group={group}&season={season}')
73
+ response.raise_for_status()
74
+ stats = response.json().get('stats', [])
75
+ if not stats or not stats[0].get('splits'):
76
+ return None
77
+ # Players traded mid-season have one split per team plus a combined split without a team
78
+ splits = stats[0]['splits']
79
+ combined = [split for split in splits if 'team' not in split]
80
+ return (combined or splits)[-1]['stat']
81
+
82
+
83
+ def print_stats(stat, columns):
84
+ widths = [max(len(label), len(str(stat.get(key, '-')))) for label, key in columns]
85
+ print(' | '.join(label.rjust(width) for (label, _), width in zip(columns, widths)))
86
+ print('-+-'.join('-' * width for width in widths))
87
+ print(' | '.join(str(stat.get(key, '-')).rjust(width) for (_, key), width in zip(columns, widths)))
88
+
89
+
90
+ def player_search(first_name, last_name):
91
+ season = datetime.now(ZoneInfo('America/New_York')).year
92
+ name = f'{first_name} {last_name}'
93
+
94
+ try:
95
+ players = get_active_players(season)
96
+ matches = find_players(name, players)
97
+ except requests.RequestException as e:
98
+ print(f'Error searching for player: {e}')
99
+ return
100
+
101
+ if not matches:
102
+ print(f'No active MLB player found matching "{name}".')
103
+ return
104
+
105
+ player = matches[0] if len(matches) == 1 else choose_player(matches)
106
+ if player is None:
107
+ print('No player selected.')
108
+ return
109
+
110
+ if normalize_name(player['fullName']) != normalize_name(name):
111
+ print(f'Showing results for {player["fullName"]}')
112
+
113
+ position = player['primaryPosition']
114
+ if position['abbreviation'] == 'TWP':
115
+ groups = ['hitting', 'pitching']
116
+ elif position['type'] == 'Pitcher':
117
+ groups = ['pitching']
118
+ else:
119
+ groups = ['hitting']
120
+
121
+ print(f'\n{player["fullName"]} ({position["abbreviation"]}) - {season} Season')
122
+ for group in groups:
123
+ try:
124
+ stat = get_season_stats(player['id'], group, season)
125
+ except requests.RequestException as e:
126
+ print(f'Error retrieving stats: {e}')
127
+ return
128
+
129
+ print(f'\n{group.capitalize()}')
130
+ if stat is None:
131
+ print(f'No {group} stats for {season}.')
132
+ else:
133
+ print_stats(stat, HITTING_COLUMNS if group == 'hitting' else PITCHING_COLUMNS)
@@ -3,7 +3,7 @@ from zoneinfo import ZoneInfo
3
3
 
4
4
  import requests
5
5
 
6
- from . import utils
6
+ from baseballcommand import utils
7
7
 
8
8
 
9
9
  def get_leagues():
@@ -22,7 +22,7 @@ def get_leagues():
22
22
  print(f'League id: {league["id"]} | League name: {league["name"]}')
23
23
  return
24
24
  else:
25
- print(f'{response.status_code}')
25
+ print(f'Response: {response.status_code}')
26
26
  return
27
27
 
28
28
  def get_todays_schedule():
@@ -1,23 +0,0 @@
1
- import argparse
2
-
3
- from . import commands
4
-
5
-
6
- def cli() -> None:
7
- # List all possible functions here
8
- FUNCTION_MAP = {
9
- 'leagues': commands.get_leagues,
10
- 'schedule': commands.get_todays_schedule
11
- }
12
-
13
- parser = argparse.ArgumentParser(
14
- prog='BaseballCommand',
15
- description="Get scores and stats from around the MLB."
16
- )
17
-
18
- parser.add_argument('command', choices=FUNCTION_MAP.keys())
19
-
20
- args = parser.parse_args()
21
-
22
- func = FUNCTION_MAP[args.command]
23
- func()