baseballcommand 0.0.3__tar.gz → 0.0.5__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.
- {baseballcommand-0.0.3 → baseballcommand-0.0.5}/PKG-INFO +1 -1
- {baseballcommand-0.0.3 → baseballcommand-0.0.5}/pyproject.toml +3 -2
- baseballcommand-0.0.5/src/baseballcommand/commands/players.py +136 -0
- baseballcommand-0.0.3/src/baseballcommand/commands/players.py +0 -41
- {baseballcommand-0.0.3 → baseballcommand-0.0.5}/src/baseballcommand/__init__.py +0 -0
- {baseballcommand-0.0.3 → baseballcommand-0.0.5}/src/baseballcommand/cli.py +0 -0
- {baseballcommand-0.0.3 → baseballcommand-0.0.5}/src/baseballcommand/commands/schedule.py +0 -0
- {baseballcommand-0.0.3 → baseballcommand-0.0.5}/src/baseballcommand/utils.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "baseballcommand"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
8
8
|
description = "This is a tool that will retrieve Major League Baseball scores and stats in your command line."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Owen Powell",email = "opowell2899@gmail.com"}
|
|
@@ -21,5 +21,6 @@ base = "baseballcommand:cli" # CLI entry point
|
|
|
21
21
|
|
|
22
22
|
[dependency-groups]
|
|
23
23
|
dev = [
|
|
24
|
-
"ruff (>=0.16.5,<0.17.0)"
|
|
24
|
+
"ruff (>=0.16.5,<0.17.0)",
|
|
25
|
+
"pytest (>=9.2.0,<10.0.0)"
|
|
25
26
|
]
|
|
@@ -0,0 +1,136 @@
|
|
|
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 player_names(player):
|
|
38
|
+
# Both the full name and the name the player goes by (e.g. "Mike" vs "Michael")
|
|
39
|
+
return {normalize_name(player['fullName']),
|
|
40
|
+
normalize_name(f'{player.get("useName", "")} {player.get("lastName", "")}')}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_players(name, players):
|
|
44
|
+
target = normalize_name(name)
|
|
45
|
+
names = {}
|
|
46
|
+
for player in players:
|
|
47
|
+
for key in player_names(player):
|
|
48
|
+
names.setdefault(key, []).append(player)
|
|
49
|
+
|
|
50
|
+
if target in names:
|
|
51
|
+
return names[target]
|
|
52
|
+
|
|
53
|
+
close = difflib.get_close_matches(target, names.keys(), n=5, cutoff=0.75)
|
|
54
|
+
matches = []
|
|
55
|
+
for key in close:
|
|
56
|
+
for player in names[key]:
|
|
57
|
+
if player not in matches:
|
|
58
|
+
matches.append(player)
|
|
59
|
+
return matches
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def choose_player(matches):
|
|
63
|
+
print('Multiple players found:')
|
|
64
|
+
for i, player in enumerate(matches, start=1):
|
|
65
|
+
print(f' {i}. {player["fullName"]} ({player["primaryPosition"]["abbreviation"]})')
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
choice = int(input('Select a player: '))
|
|
69
|
+
except (ValueError, EOFError):
|
|
70
|
+
return None
|
|
71
|
+
return matches[choice - 1] if 1 <= choice <= len(matches) else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_season_stats(player_id, group, season):
|
|
75
|
+
response = requests.get(f'{BASE_URL}/people/{player_id}/stats?stats=season&group={group}&season={season}')
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
stats = response.json().get('stats', [])
|
|
78
|
+
if not stats or not stats[0].get('splits'):
|
|
79
|
+
return None
|
|
80
|
+
# Players traded mid-season have one split per team plus a combined split without a team
|
|
81
|
+
splits = stats[0]['splits']
|
|
82
|
+
combined = [split for split in splits if 'team' not in split]
|
|
83
|
+
return (combined or splits)[-1]['stat']
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def print_stats(stat, columns):
|
|
87
|
+
widths = [max(len(label), len(str(stat.get(key, '-')))) for label, key in columns]
|
|
88
|
+
print(' | '.join(label.rjust(width) for (label, _), width in zip(columns, widths)))
|
|
89
|
+
print('-+-'.join('-' * width for width in widths))
|
|
90
|
+
print(' | '.join(str(stat.get(key, '-')).rjust(width) for (_, key), width in zip(columns, widths)))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def player_search(first_name, last_name):
|
|
94
|
+
season = datetime.now(ZoneInfo('America/New_York')).year
|
|
95
|
+
name = f'{first_name} {last_name}'
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
players = get_active_players(season)
|
|
99
|
+
matches = find_players(name, players)
|
|
100
|
+
except requests.RequestException as e:
|
|
101
|
+
print(f'Error searching for player: {e}')
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if not matches:
|
|
105
|
+
print(f'No active MLB player found matching "{name}".')
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
player = matches[0] if len(matches) == 1 else choose_player(matches)
|
|
109
|
+
if player is None:
|
|
110
|
+
print('No player selected.')
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if normalize_name(name) not in player_names(player):
|
|
114
|
+
print(f'Showing results for {player["fullName"]}')
|
|
115
|
+
|
|
116
|
+
position = player['primaryPosition']
|
|
117
|
+
if position['abbreviation'] == 'TWP':
|
|
118
|
+
groups = ['hitting', 'pitching']
|
|
119
|
+
elif position['type'] == 'Pitcher':
|
|
120
|
+
groups = ['pitching']
|
|
121
|
+
else:
|
|
122
|
+
groups = ['hitting']
|
|
123
|
+
|
|
124
|
+
print(f'\n{player["fullName"]} ({position["abbreviation"]}) - {season} Season')
|
|
125
|
+
for group in groups:
|
|
126
|
+
try:
|
|
127
|
+
stat = get_season_stats(player['id'], group, season)
|
|
128
|
+
except requests.RequestException as e:
|
|
129
|
+
print(f'Error retrieving stats: {e}')
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
print(f'\n{group.capitalize()}')
|
|
133
|
+
if stat is None:
|
|
134
|
+
print(f'No {group} stats for {season}.')
|
|
135
|
+
else:
|
|
136
|
+
print_stats(stat, HITTING_COLUMNS if group == 'hitting' else PITCHING_COLUMNS)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import requests
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# TODO: improve name matching. Example: searching for Pete Crowe-Armstrong returns Pete Alonso
|
|
6
|
-
# solution: check name match on JSON return?
|
|
7
|
-
def player_search(first_name, last_name):
|
|
8
|
-
url = (f'https://statsapi.mlb.com/api/v1/people/search?names={first_name.lower()}%20'
|
|
9
|
-
f'{last_name.lower()}'
|
|
10
|
-
f'&active=True'
|
|
11
|
-
f'&sportIDs=1'
|
|
12
|
-
f'&limit=1')
|
|
13
|
-
|
|
14
|
-
response = requests.get(url)
|
|
15
|
-
|
|
16
|
-
if response.status_code == 200:
|
|
17
|
-
data = response.json()
|
|
18
|
-
|
|
19
|
-
# print(json.dumps(data, sort_keys=True, indent=4))
|
|
20
|
-
|
|
21
|
-
player = data["people"][0]
|
|
22
|
-
player_id = player["id"]
|
|
23
|
-
|
|
24
|
-
# TODO: change URL if player is a pitcher
|
|
25
|
-
stats_url = f'https://statsapi.mlb.com/api/v1/people/{player_id}/stats?stats=season&group=hitting'
|
|
26
|
-
|
|
27
|
-
stats_response = requests.get(stats_url)
|
|
28
|
-
|
|
29
|
-
if stats_response.status_code == 200:
|
|
30
|
-
# stats_data = stats_response.json()
|
|
31
|
-
|
|
32
|
-
# print(json.dumps(stats_data, sort_keys=True, indent=4))
|
|
33
|
-
|
|
34
|
-
# stat = stats_data["stats"][0]["splits"][0]["stat"]
|
|
35
|
-
print(f'{"Games Played | ":14}{"At Bats | ":9}{"Hits | ":6}{"Runs | ":6}{"RBI | ":5}{"Home Runs | ":11}'
|
|
36
|
-
f'{"Doubles | ":9}{"Triples | ":9}{"AVG | ":5}{"OBP | ":5}{"OPS | ":5}{"SLG | ":5}')
|
|
37
|
-
print('-' * 100)
|
|
38
|
-
|
|
39
|
-
else:
|
|
40
|
-
print(f'Response: {response.status_code}')
|
|
41
|
-
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|