.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MLB-StatsAPI
2 |
3 | Python wrapper for MLB Stats API
4 |
5 | Created by Todd Roberts
6 |
7 | https://pypi.org/project/MLB-StatsAPI/
8 |
9 | Issues: https://github.com/toddrob99/MLB-StatsAPI/issues
10 |
11 | Wiki/Documentation: https://github.com/toddrob99/MLB-StatsAPI/wiki
12 |
13 | ## Copyright Notice
14 |
15 | This package and its author are not affiliated with MLB or any MLB team. This API wrapper interfaces with MLB's Stats API. Use of MLB data is subject to the notice posted at http://gdx.mlb.com/components/copyright.txt.
16 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | MLB-StatsAPI
7 |
8 |
9 | MLB-StatsAPI
10 | Documentation has moved to https://github.com/toddrob99/MLB-StatsAPI/wiki.
You should be redirected within 5 seconds. If not, please click the link to proceed.
11 |
12 |
--------------------------------------------------------------------------------
/generate_endpoint_doc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from statsapi import endpoints
4 | lbb = """
5 | * """
6 | lb = """
7 | """
8 |
9 | for k, v in endpoints.ENDPOINTS.items():
10 | print(f"## Endpoint: `{k}`{lb}")
11 | print(f"### URL: `{v['url']}`{lb}")
12 | rp = [pk for pk, pv in v['path_params'].items() if pv['required'] and pk != 'ver']
13 | # print(f"### Required Path Parameters{lb}{rp}")
14 | rq = [' + '.join(q) for q in v['required_params'] if len(q) > 0]
15 | # print(f"### Required Query Parameters{lb}{rq}")
16 | rp.extend(rq)
17 | print(f"### Required Parameters{lb}* {lbb.join(rp) if len(rp) else '*None*'}{lb}")
18 | ap = list(v['path_params'].keys()) + (v['query_params'] if v['query_params'] != [[]] else [])
19 | print(f"### All Parameters{lb}* {lbb.join(ap)}{lb}")
20 | if v.get("note"):
21 | print(f"### Note{lb}{v['note']}{lb}")
22 |
23 | print(f"-----{lb}")
24 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | responses
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 | from distutils.util import convert_path
3 |
4 | # https://stackoverflow.com/questions/2058802/how-can-i-get-the-version-defined-in-setup-py-setuptools-in-my-package
5 | main_ns = {}
6 | ver_path = convert_path("statsapi/version.py")
7 | with open(ver_path) as ver_file:
8 | exec(ver_file.read(), main_ns)
9 |
10 | with open("README.md", "r") as fh:
11 | long_description = fh.read()
12 |
13 | setuptools.setup(
14 | name="MLB-StatsAPI",
15 | version=main_ns["VERSION"],
16 | author="Todd Roberts",
17 | author_email="todd@toddrob.com",
18 | description="MLB Stats API Wrapper for Python",
19 | long_description=long_description,
20 | long_description_content_type="text/markdown",
21 | url="https://github.com/toddrob99/MLB-StatsAPI",
22 | packages=setuptools.find_packages(),
23 | install_requires=["requests"],
24 | classifiers=[
25 | "Programming Language :: Python",
26 | "Programming Language :: Python :: 3",
27 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
28 | "Operating System :: OS Independent",
29 | ],
30 | )
31 |
--------------------------------------------------------------------------------
/statsapi/__init__.py:
--------------------------------------------------------------------------------
1 | # encoding=utf-8
2 | """# MLB-StatsAPI
3 |
4 | Python wrapper for MLB Stats API
5 |
6 | Created by Todd Roberts
7 |
8 | https://pypi.org/project/MLB-StatsAPI/
9 |
10 | Issues: https://github.com/toddrob99/MLB-StatsAPI/issues
11 |
12 | Wiki/Documentation: https://github.com/toddrob99/MLB-StatsAPI/wiki
13 | """
14 | import sys
15 |
16 | import copy
17 | import logging
18 | import requests
19 | from datetime import datetime
20 |
21 | from . import version
22 | from . import endpoints
23 |
24 | __version__ = version.VERSION
25 | """Installed version of MLB-StatsAPI"""
26 |
27 | BASE_URL = endpoints.BASE_URL
28 | """Base MLB Stats API URL"""
29 | ENDPOINTS = endpoints.ENDPOINTS
30 | """MLB Stats API endpoint configuration"""
31 |
32 | logger = logging.getLogger("statsapi")
33 |
34 | # Python 2 Support Warning
35 | if sys.version_info.major < 3:
36 | logger.warning(
37 | "WARNING: Support for Python 2 has been discontinued. "
38 | "The MLB-StatsAPI module may continue to function, but "
39 | "issues not impacting Python 3 will be closed and support will not be provided."
40 | )
41 |
42 |
43 | def schedule(
44 | date=None,
45 | start_date=None,
46 | end_date=None,
47 | team="",
48 | opponent="",
49 | sportId=1,
50 | game_id=None,
51 | leagueId=None,
52 | season=None,
53 | include_series_status=True,
54 | ):
55 | """Get list of games for a given date/range and/or team/opponent."""
56 | if end_date and not start_date:
57 | date = end_date
58 | end_date = None
59 |
60 | if start_date and not end_date:
61 | date = start_date
62 | start_date = None
63 |
64 | params = {}
65 |
66 | if date:
67 | params.update({"date": date})
68 | elif start_date and end_date:
69 | params.update({"startDate": start_date, "endDate": end_date})
70 |
71 | if team != "":
72 | params.update({"teamId": str(team)})
73 |
74 | if opponent != "":
75 | params.update({"opponentId": str(opponent)})
76 |
77 | if game_id:
78 | params.update({"gamePks": game_id})
79 |
80 | if leagueId:
81 | params.update({"leagueId": leagueId})
82 |
83 | if season:
84 | params.update({"season": season})
85 |
86 | hydrate = (
87 | "decisions,probablePitcher(note),linescore,broadcasts,game(content(media(epg)))"
88 | )
89 | if include_series_status:
90 | if date == "2014-03-11" or (str(start_date) <= "2014-03-11" <= str(end_date)):
91 | # For some reason the seriesStatus hydration throws a server error on 2014-03-11 only (checked back to 2000)
92 | logger.warning(
93 | "Excluding seriesStatus hydration because the MLB API throws an error for 2014-03-11 which is included in the requested date range."
94 | )
95 | else:
96 | hydrate += ",seriesStatus"
97 | params.update(
98 | {
99 | "sportId": str(sportId),
100 | "hydrate": hydrate,
101 | }
102 | )
103 |
104 | r = get("schedule", params)
105 |
106 | games = []
107 | if r.get("totalItems") == 0:
108 | return games # TODO: ValueError('No games to parse from schedule object.') instead?
109 | else:
110 | for date in r.get("dates"):
111 | for game in date.get("games"):
112 | game_info = {
113 | "game_id": game["gamePk"],
114 | "game_datetime": game["gameDate"],
115 | "game_date": date["date"],
116 | "game_type": game["gameType"],
117 | "status": game["status"]["detailedState"],
118 | "away_name": game["teams"]["away"]["team"].get("name", "???"),
119 | "home_name": game["teams"]["home"]["team"].get("name", "???"),
120 | "away_id": game["teams"]["away"]["team"]["id"],
121 | "home_id": game["teams"]["home"]["team"]["id"],
122 | "doubleheader": game["doubleHeader"],
123 | "game_num": game["gameNumber"],
124 | "home_probable_pitcher": game["teams"]["home"]
125 | .get("probablePitcher", {})
126 | .get("fullName", ""),
127 | "away_probable_pitcher": game["teams"]["away"]
128 | .get("probablePitcher", {})
129 | .get("fullName", ""),
130 | "home_pitcher_note": game["teams"]["home"]
131 | .get("probablePitcher", {})
132 | .get("note", ""),
133 | "away_pitcher_note": game["teams"]["away"]
134 | .get("probablePitcher", {})
135 | .get("note", ""),
136 | "away_score": game["teams"]["away"].get("score", "0"),
137 | "home_score": game["teams"]["home"].get("score", "0"),
138 | "current_inning": game.get("linescore", {}).get(
139 | "currentInning", ""
140 | ),
141 | "inning_state": game.get("linescore", {}).get("inningState", ""),
142 | "venue_id": game.get("venue", {}).get("id"),
143 | "venue_name": game.get("venue", {}).get("name"),
144 | "national_broadcasts": list(
145 | set(
146 | broadcast["name"]
147 | for broadcast in game.get("broadcasts", [])
148 | if broadcast.get("isNational", False)
149 | )
150 | ),
151 | "series_status": game.get("seriesStatus", {}).get("result"),
152 | }
153 | if game["content"].get("media", {}).get("freeGame", False):
154 | game_info["national_broadcasts"].append("MLB.tv Free Game")
155 | if game_info["status"] in ["Final", "Game Over"]:
156 | if game.get("isTie"):
157 | game_info.update({"winning_team": "Tie", "losing_Team": "Tie"})
158 | else:
159 | game_info.update(
160 | {
161 | "winning_team": (
162 | game["teams"]["away"]["team"].get("name", "???")
163 | if game["teams"]["away"].get("isWinner")
164 | else game["teams"]["home"]["team"].get(
165 | "name", "???"
166 | )
167 | ),
168 | "losing_team": (
169 | game["teams"]["home"]["team"].get("name", "???")
170 | if game["teams"]["away"].get("isWinner")
171 | else game["teams"]["away"]["team"].get(
172 | "name", "???"
173 | )
174 | ),
175 | "winning_pitcher": game.get("decisions", {})
176 | .get("winner", {})
177 | .get("fullName", ""),
178 | "losing_pitcher": game.get("decisions", {})
179 | .get("loser", {})
180 | .get("fullName", ""),
181 | "save_pitcher": game.get("decisions", {})
182 | .get("save", {})
183 | .get("fullName"),
184 | }
185 | )
186 | summary = (
187 | date["date"]
188 | + " - "
189 | + game["teams"]["away"]["team"].get("name", "???")
190 | + " ("
191 | + str(game["teams"]["away"].get("score", ""))
192 | + ") @ "
193 | + game["teams"]["home"]["team"].get("name", "???")
194 | + " ("
195 | + str(game["teams"]["home"].get("score", ""))
196 | + ") ("
197 | + game["status"]["detailedState"]
198 | + ")"
199 | )
200 | game_info.update({"summary": summary})
201 | elif game_info["status"] == "In Progress":
202 | game_info.update(
203 | {
204 | "summary": date["date"]
205 | + " - "
206 | + game["teams"]["away"]["team"]["name"]
207 | + " ("
208 | + str(game["teams"]["away"].get("score", "0"))
209 | + ") @ "
210 | + game["teams"]["home"]["team"]["name"]
211 | + " ("
212 | + str(game["teams"]["home"].get("score", "0"))
213 | + ") ("
214 | + game["linescore"]["inningState"]
215 | + " of the "
216 | + game["linescore"]["currentInningOrdinal"]
217 | + ")"
218 | }
219 | )
220 | else:
221 | summary = (
222 | date["date"]
223 | + " - "
224 | + game["teams"]["away"]["team"]["name"]
225 | + " @ "
226 | + game["teams"]["home"]["team"]["name"]
227 | + " ("
228 | + game["status"]["detailedState"]
229 | + ")"
230 | )
231 | game_info.update({"summary": summary})
232 |
233 | games.append(game_info)
234 |
235 | return games
236 |
237 |
238 | def boxscore(
239 | gamePk,
240 | battingBox=True,
241 | battingInfo=True,
242 | fieldingInfo=True,
243 | pitchingBox=True,
244 | gameInfo=True,
245 | timecode=None,
246 | ):
247 | """Get a formatted boxscore for a given game."""
248 | boxData = boxscore_data(gamePk, timecode)
249 |
250 | rowLen = 79
251 | """rowLen is the total width of each side of the box score, excluding the " | " separator"""
252 | fullRowLen = rowLen * 2 + 3
253 | """fullRowLen is the full table width"""
254 | boxscore = ""
255 | """boxscore will hold the string to be returned"""
256 |
257 | if battingBox:
258 | # Add column headers
259 | awayBatters = boxData["awayBatters"]
260 | homeBatters = boxData["homeBatters"]
261 |
262 | # Make sure the home and away batter lists are the same length
263 | blankBatter = {
264 | "namefield": "",
265 | "ab": "",
266 | "r": "",
267 | "h": "",
268 | "rbi": "",
269 | "bb": "",
270 | "k": "",
271 | "lob": "",
272 | "avg": "",
273 | "ops": "",
274 | }
275 |
276 | while len(awayBatters) > len(homeBatters):
277 | homeBatters.append(blankBatter)
278 |
279 | while len(awayBatters) < len(homeBatters):
280 | awayBatters.append(blankBatter)
281 |
282 | # Get team totals
283 | awayBatters.append(boxData["awayBattingTotals"])
284 | homeBatters.append(boxData["homeBattingTotals"])
285 |
286 | # Build the batting box!
287 | for i in range(0, len(awayBatters)):
288 | if i == 0 or i == len(awayBatters) - 1:
289 | boxscore += "-" * rowLen + " | " + "-" * rowLen + "\n"
290 |
291 | boxscore += "{namefield:<40} {ab:^3} {r:^3} {h:^3} {rbi:^3} {bb:^3} {k:^3} {lob:^3} {avg:^4} {ops:^5} | ".format(
292 | **awayBatters[i]
293 | )
294 | boxscore += "{namefield:<40} {ab:^3} {r:^3} {h:^3} {rbi:^3} {bb:^3} {k:^3} {lob:^3} {avg:^4} {ops:^5}\n".format(
295 | **homeBatters[i]
296 | )
297 | if i == 0 or i == len(awayBatters) - 1:
298 | boxscore += "-" * rowLen + " | " + "-" * rowLen + "\n"
299 |
300 | # Get batting notes
301 | awayBattingNotes = boxData["awayBattingNotes"]
302 | homeBattingNotes = boxData["homeBattingNotes"]
303 |
304 | while len(awayBattingNotes) > len(homeBattingNotes):
305 | homeBattingNotes.update({len(homeBattingNotes): ""})
306 |
307 | while len(awayBattingNotes) < len(homeBattingNotes):
308 | awayBattingNotes.update({len(awayBattingNotes): ""})
309 |
310 | for i in range(0, len(awayBattingNotes)):
311 | boxscore += "{:<79} | ".format(awayBattingNotes[i])
312 | boxscore += "{:<79}\n".format(homeBattingNotes[i])
313 |
314 | boxscore += " " * rowLen + " | " + " " * rowLen + "\n"
315 |
316 | # Get batting and fielding info
317 | awayBoxInfo = {}
318 | homeBoxInfo = {}
319 | boxInfo = [awayBoxInfo, homeBoxInfo]
320 | sides = ["away", "home"]
321 | for infoType in ["BATTING", "FIELDING"]:
322 | if (infoType == "BATTING" and battingInfo) or (
323 | infoType == "FIELDING" and fieldingInfo
324 | ):
325 | for i in range(0, len(sides)):
326 | for z in (
327 | x for x in boxData[sides[i]]["info"] if x.get("title") == infoType
328 | ):
329 | boxInfo[i].update({len(boxInfo[i]): z["title"]})
330 | for x in z["fieldList"]:
331 | if len(x["label"] + ": " + x.get("value", "")) > rowLen:
332 | words = iter(
333 | (x["label"] + ": " + x.get("value", "")).split()
334 | )
335 | check = ""
336 | lines = []
337 | for word in words:
338 | if len(check) + 1 + len(word) <= rowLen:
339 | if check == "":
340 | check = word
341 | else:
342 | check += " " + word
343 | else:
344 | lines.append(check)
345 | check = " " + word
346 |
347 | if len(check):
348 | lines.append(check)
349 |
350 | for j in range(0, len(lines)):
351 | boxInfo[i].update({len(boxInfo[i]): lines[j]})
352 | else:
353 | boxInfo[i].update(
354 | {
355 | len(boxInfo[i]): x["label"]
356 | + ": "
357 | + x.get("value", "")
358 | }
359 | )
360 |
361 | if infoType == "BATTING":
362 | if len(awayBoxInfo):
363 | awayBoxInfo.update({len(awayBoxInfo): " "})
364 |
365 | if len(homeBoxInfo):
366 | homeBoxInfo.update({len(homeBoxInfo): " "})
367 |
368 | if len(awayBoxInfo) > 0:
369 | while len(awayBoxInfo) > len(homeBoxInfo):
370 | homeBoxInfo.update({len(homeBoxInfo): ""})
371 |
372 | while len(awayBoxInfo) < len(homeBoxInfo):
373 | awayBoxInfo.update({len(awayBoxInfo): ""})
374 |
375 | # Build info box
376 | for i in range(0, len(awayBoxInfo)):
377 | boxscore += ("{:<%s} | " % rowLen).format(awayBoxInfo[i])
378 | boxscore += ("{:<%s}\n" % rowLen).format(homeBoxInfo[i])
379 | if i == len(awayBoxInfo) - 1:
380 | boxscore += "-" * rowLen + " | " + "-" * rowLen + "\n"
381 |
382 | # Get pitching box
383 | if pitchingBox:
384 | awayPitchers = boxData["awayPitchers"]
385 | homePitchers = boxData["homePitchers"]
386 |
387 | # Make sure the home and away pitcher lists are the same length
388 | blankPitcher = {
389 | "namefield": "",
390 | "ip": "",
391 | "h": "",
392 | "r": "",
393 | "er": "",
394 | "bb": "",
395 | "k": "",
396 | "hr": "",
397 | "era": "",
398 | }
399 |
400 | while len(awayPitchers) > len(homePitchers):
401 | homePitchers.append(blankPitcher)
402 |
403 | while len(awayPitchers) < len(homePitchers):
404 | awayPitchers.append(blankPitcher)
405 |
406 | # Get team totals
407 | awayPitchers.append(boxData["awayPitchingTotals"])
408 | homePitchers.append(boxData["homePitchingTotals"])
409 |
410 | # Build the pitching box!
411 | for i in range(0, len(awayPitchers)):
412 | if i == 0 or i == len(awayPitchers) - 1:
413 | boxscore += "-" * rowLen + " | " + "-" * rowLen + "\n"
414 |
415 | boxscore += "{namefield:<43} {ip:^4} {h:^3} {r:^3} {er:^3} {bb:^3} {k:^3} {hr:^3} {era:^6} | ".format(
416 | **awayPitchers[i]
417 | )
418 | boxscore += "{namefield:<43} {ip:^4} {h:^3} {r:^3} {er:^3} {bb:^3} {k:^3} {hr:^3} {era:^6}\n".format(
419 | **homePitchers[i]
420 | )
421 | if i == 0 or i == len(awayPitchers) - 1:
422 | boxscore += "-" * rowLen + " | " + "-" * rowLen + "\n"
423 |
424 | # Get game info
425 | if gameInfo:
426 | z = boxData["gameBoxInfo"]
427 | gameBoxInfo = {}
428 | for x in z:
429 | if (
430 | len(x["label"] + (": " if x.get("value") else "") + x.get("value", ""))
431 | > fullRowLen
432 | ):
433 | words = iter(
434 | (
435 | x["label"]
436 | + (": " if x.get("value") else "")
437 | + x.get("value", "")
438 | ).split()
439 | )
440 | check = ""
441 | lines = []
442 | for word in words:
443 | if len(check) + 1 + len(word) <= fullRowLen:
444 | if check == "":
445 | check = word
446 | else:
447 | check += " " + word
448 | else:
449 | lines.append(check)
450 | check = " " + word
451 |
452 | if len(check):
453 | lines.append(check)
454 |
455 | for i in range(0, len(lines)):
456 | gameBoxInfo.update({len(gameBoxInfo): lines[i]})
457 | else:
458 | gameBoxInfo.update(
459 | {
460 | len(gameBoxInfo): x["label"]
461 | + (": " if x.get("value") else "")
462 | + x.get("value", "")
463 | }
464 | )
465 |
466 | # Build the game info box
467 | for i in range(0, len(gameBoxInfo)):
468 | boxscore += ("{:<%s}" % fullRowLen + "\n").format(gameBoxInfo[i])
469 | if i == len(gameBoxInfo) - 1:
470 | boxscore += "-" * fullRowLen + "\n"
471 |
472 | return boxscore
473 |
474 |
475 | def boxscore_data(gamePk, timecode=None):
476 | """Returns a python dict containing boxscore data for a given game."""
477 |
478 | boxData = {}
479 | """boxData holds the dict to be returned"""
480 |
481 | params = {
482 | "gamePk": gamePk,
483 | "fields": "gameData,game,teams,teamName,shortName,teamStats,batting,atBats,runs,hits,doubles,triples,homeRuns,rbi,stolenBases,strikeOuts,baseOnBalls,leftOnBase,pitching,inningsPitched,earnedRuns,homeRuns,players,boxscoreName,liveData,boxscore,teams,players,id,fullName,allPositions,abbreviation,seasonStats,batting,avg,ops,obp,slg,era,pitchesThrown,numberOfPitches,strikes,battingOrder,info,title,fieldList,note,label,value,wins,losses,holds,blownSaves",
484 | }
485 | if timecode:
486 | params.update({"timecode": timecode})
487 |
488 | r = get("game", params)
489 |
490 | boxData.update({"gameId": r["gameData"]["game"]["id"]})
491 | boxData.update({"teamInfo": r["gameData"]["teams"]})
492 | boxData.update({"playerInfo": r["gameData"]["players"]})
493 | boxData.update({"away": r["liveData"]["boxscore"]["teams"]["away"]})
494 | boxData.update({"home": r["liveData"]["boxscore"]["teams"]["home"]})
495 |
496 | batterColumns = [
497 | {
498 | "namefield": boxData["teamInfo"]["away"]["teamName"] + " Batters",
499 | "ab": "AB",
500 | "r": "R",
501 | "h": "H",
502 | "doubles": "2B",
503 | "triples": "3B",
504 | "hr": "HR",
505 | "rbi": "RBI",
506 | "sb": "SB",
507 | "bb": "BB",
508 | "k": "K",
509 | "lob": "LOB",
510 | "avg": "AVG",
511 | "ops": "OPS",
512 | "personId": 0,
513 | "substitution": False,
514 | "note": "",
515 | "name": boxData["teamInfo"]["away"]["teamName"] + " Batters",
516 | "position": "",
517 | "obp": "OBP",
518 | "slg": "SLG",
519 | "battingOrder": "",
520 | }
521 | ]
522 | # Add away and home column headers
523 | sides = ["away", "home"]
524 | awayBatters = copy.deepcopy(batterColumns)
525 | homeBatters = copy.deepcopy(batterColumns)
526 | homeBatters[0]["namefield"] = boxData["teamInfo"]["home"]["teamName"] + " Batters"
527 | homeBatters[0]["name"] = boxData["teamInfo"]["home"]["teamName"] + " Batters"
528 | batters = [awayBatters, homeBatters]
529 |
530 | for i in range(0, len(sides)):
531 | side = sides[i]
532 | for batterId_int in [
533 | x
534 | for x in boxData[side]["batters"]
535 | if boxData[side]["players"].get("ID" + str(x), {}).get("battingOrder")
536 | ]:
537 | batterId = str(batterId_int)
538 | namefield = (
539 | str(boxData[side]["players"]["ID" + batterId]["battingOrder"])[0]
540 | if str(boxData[side]["players"]["ID" + batterId]["battingOrder"])[-1]
541 | == "0"
542 | else " "
543 | )
544 | namefield += " " + boxData[side]["players"]["ID" + batterId]["stats"][
545 | "batting"
546 | ].get("note", "")
547 | namefield += (
548 | boxData["playerInfo"]["ID" + batterId]["boxscoreName"]
549 | + " "
550 | + boxData[side]["players"]["ID" + batterId]["position"]["abbreviation"]
551 | )
552 | if not len(
553 | boxData[side]["players"]["ID" + batterId]
554 | .get("stats", {})
555 | .get("batting", {})
556 | ):
557 | # Protect against player with no batting data in the box score (#37)
558 | continue
559 |
560 | batter = {
561 | "namefield": namefield,
562 | "ab": str(
563 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
564 | "atBats"
565 | ]
566 | ),
567 | "r": str(
568 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
569 | "runs"
570 | ]
571 | ),
572 | "h": str(
573 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
574 | "hits"
575 | ]
576 | ),
577 | "doubles": str(
578 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
579 | "doubles"
580 | ]
581 | ),
582 | "triples": str(
583 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
584 | "triples"
585 | ]
586 | ),
587 | "hr": str(
588 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
589 | "homeRuns"
590 | ]
591 | ),
592 | "rbi": str(
593 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"]["rbi"]
594 | ),
595 | "sb": str(
596 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
597 | "stolenBases"
598 | ]
599 | ),
600 | "bb": str(
601 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
602 | "baseOnBalls"
603 | ]
604 | ),
605 | "k": str(
606 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
607 | "strikeOuts"
608 | ]
609 | ),
610 | "lob": str(
611 | boxData[side]["players"]["ID" + batterId]["stats"]["batting"][
612 | "leftOnBase"
613 | ]
614 | ),
615 | "avg": str(
616 | boxData[side]["players"]["ID" + batterId]["seasonStats"]["batting"][
617 | "avg"
618 | ]
619 | ),
620 | "ops": str(
621 | boxData[side]["players"]["ID" + batterId]["seasonStats"]["batting"][
622 | "ops"
623 | ]
624 | ),
625 | "personId": batterId_int,
626 | "battingOrder": str(
627 | boxData[side]["players"]["ID" + batterId]["battingOrder"]
628 | ),
629 | "substitution": (
630 | False
631 | if str(boxData[side]["players"]["ID" + batterId]["battingOrder"])[
632 | -1
633 | ]
634 | == "0"
635 | else True
636 | ),
637 | "note": boxData[side]["players"]["ID" + batterId]["stats"][
638 | "batting"
639 | ].get("note", ""),
640 | "name": boxData["playerInfo"]["ID" + batterId]["boxscoreName"],
641 | "position": boxData[side]["players"]["ID" + batterId]["position"][
642 | "abbreviation"
643 | ],
644 | "obp": str(
645 | boxData[side]["players"]["ID" + batterId]["seasonStats"]["batting"][
646 | "obp"
647 | ]
648 | ),
649 | "slg": str(
650 | boxData[side]["players"]["ID" + batterId]["seasonStats"]["batting"][
651 | "slg"
652 | ]
653 | ),
654 | }
655 | batters[i].append(batter)
656 |
657 | boxData.update({"awayBatters": awayBatters})
658 | boxData.update({"homeBatters": homeBatters})
659 |
660 | # Add team totals
661 | sidesBattingTotals = ["awayBattingTotals", "homeBattingTotals"]
662 | for i in range(0, len(sides)):
663 | side = sides[i]
664 | boxData.update(
665 | {
666 | sidesBattingTotals[i]: {
667 | "namefield": "Totals",
668 | "ab": str(boxData[side]["teamStats"]["batting"]["atBats"]),
669 | "r": str(boxData[side]["teamStats"]["batting"]["runs"]),
670 | "h": str(boxData[side]["teamStats"]["batting"]["hits"]),
671 | "hr": str(boxData[side]["teamStats"]["batting"]["homeRuns"]),
672 | "rbi": str(boxData[side]["teamStats"]["batting"]["rbi"]),
673 | "bb": str(boxData[side]["teamStats"]["batting"]["baseOnBalls"]),
674 | "k": str(boxData[side]["teamStats"]["batting"]["strikeOuts"]),
675 | "lob": str(boxData[side]["teamStats"]["batting"]["leftOnBase"]),
676 | "avg": "",
677 | "ops": "",
678 | "obp": "",
679 | "slg": "",
680 | "name": "Totals",
681 | "position": "",
682 | "note": "",
683 | "substitution": False,
684 | "battingOrder": "",
685 | "personId": 0,
686 | }
687 | }
688 | )
689 |
690 | # Get batting notes
691 | awayBattingNotes = {}
692 | homeBattingNotes = {}
693 | battingNotes = [awayBattingNotes, homeBattingNotes]
694 | for i in range(0, len(sides)):
695 | for n in boxData[sides[i]]["note"]:
696 | battingNotes[i].update(
697 | {len(battingNotes[i]): n["label"] + "-" + n["value"]}
698 | )
699 |
700 | boxData.update({"awayBattingNotes": awayBattingNotes})
701 | boxData.update({"homeBattingNotes": homeBattingNotes})
702 |
703 | # Get pitching box
704 | # Add column headers
705 | pitcherColumns = [
706 | {
707 | "namefield": boxData["teamInfo"]["away"]["teamName"] + " Pitchers",
708 | "ip": "IP",
709 | "h": "H",
710 | "r": "R",
711 | "er": "ER",
712 | "bb": "BB",
713 | "k": "K",
714 | "hr": "HR",
715 | "era": "ERA",
716 | "p": "P",
717 | "s": "S",
718 | "name": boxData["teamInfo"]["away"]["teamName"] + " Pitchers",
719 | "personId": 0,
720 | "note": "",
721 | }
722 | ]
723 | awayPitchers = copy.deepcopy(pitcherColumns)
724 | homePitchers = copy.deepcopy(pitcherColumns)
725 | homePitchers[0]["namefield"] = boxData["teamInfo"]["home"]["teamName"] + " Pitchers"
726 | homePitchers[0]["name"] = boxData["teamInfo"]["away"]["teamName"] + " Pitchers"
727 | pitchers = [awayPitchers, homePitchers]
728 |
729 | for i in range(0, len(sides)):
730 | side = sides[i]
731 | for pitcherId_int in boxData[side]["pitchers"]:
732 | pitcherId = str(pitcherId_int)
733 | if not boxData[side]["players"].get("ID" + pitcherId) or not len(
734 | boxData[side]["players"]["ID" + pitcherId]
735 | .get("stats", {})
736 | .get("pitching", {})
737 | ):
738 | # Skip pitcher with no pitching data in the box score (#37)
739 | # Or skip pitcher listed under the wrong team (from comments on #37)
740 | continue
741 |
742 | namefield = boxData["playerInfo"]["ID" + pitcherId]["boxscoreName"]
743 | namefield += (
744 | " "
745 | + boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"].get(
746 | "note", ""
747 | )
748 | if boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"].get(
749 | "note"
750 | )
751 | else ""
752 | )
753 | pitcher = {
754 | "namefield": namefield,
755 | "ip": str(
756 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
757 | "inningsPitched"
758 | ]
759 | ),
760 | "h": str(
761 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
762 | "hits"
763 | ]
764 | ),
765 | "r": str(
766 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
767 | "runs"
768 | ]
769 | ),
770 | "er": str(
771 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
772 | "earnedRuns"
773 | ]
774 | ),
775 | "bb": str(
776 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
777 | "baseOnBalls"
778 | ]
779 | ),
780 | "k": str(
781 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
782 | "strikeOuts"
783 | ]
784 | ),
785 | "hr": str(
786 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
787 | "homeRuns"
788 | ]
789 | ),
790 | "p": str(
791 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"].get(
792 | "pitchesThrown",
793 | boxData[side]["players"]["ID" + pitcherId]["stats"][
794 | "pitching"
795 | ].get("numberOfPitches", 0),
796 | )
797 | ),
798 | "s": str(
799 | boxData[side]["players"]["ID" + pitcherId]["stats"]["pitching"][
800 | "strikes"
801 | ]
802 | ),
803 | "era": str(
804 | boxData[side]["players"]["ID" + pitcherId]["seasonStats"][
805 | "pitching"
806 | ]["era"]
807 | ),
808 | "name": boxData["playerInfo"]["ID" + pitcherId]["boxscoreName"],
809 | "personId": pitcherId_int,
810 | "note": boxData[side]["players"]["ID" + pitcherId]["stats"][
811 | "pitching"
812 | ].get("note", ""),
813 | }
814 | pitchers[i].append(pitcher)
815 |
816 | boxData.update({"awayPitchers": awayPitchers})
817 | boxData.update({"homePitchers": homePitchers})
818 |
819 | # Get team totals
820 | pitchingTotals = ["awayPitchingTotals", "homePitchingTotals"]
821 | for i in range(0, len(sides)):
822 | side = sides[i]
823 | boxData.update(
824 | {
825 | pitchingTotals[i]: {
826 | "namefield": "Totals",
827 | "ip": str(boxData[side]["teamStats"]["pitching"]["inningsPitched"]),
828 | "h": str(boxData[side]["teamStats"]["pitching"]["hits"]),
829 | "r": str(boxData[side]["teamStats"]["pitching"]["runs"]),
830 | "er": str(boxData[side]["teamStats"]["pitching"]["earnedRuns"]),
831 | "bb": str(boxData[side]["teamStats"]["pitching"]["baseOnBalls"]),
832 | "k": str(boxData[side]["teamStats"]["pitching"]["strikeOuts"]),
833 | "hr": str(boxData[side]["teamStats"]["pitching"]["homeRuns"]),
834 | "p": "",
835 | "s": "",
836 | "era": "",
837 | "name": "Totals",
838 | "personId": 0,
839 | "note": "",
840 | }
841 | }
842 | )
843 |
844 | # Get game info
845 | boxData.update({"gameBoxInfo": r["liveData"]["boxscore"].get("info", [])})
846 |
847 | return boxData
848 |
849 |
850 | def linescore(gamePk, timecode=None):
851 | """Get formatted linescore for a given game."""
852 | linescore = ""
853 | params = {
854 | "gamePk": gamePk,
855 | "fields": "gameData,teams,teamName,shortName,status,abstractGameState,liveData,linescore,innings,num,home,away,runs,hits,errors",
856 | }
857 | if timecode:
858 | params.update({"timecode": timecode})
859 |
860 | r = get("game", params)
861 |
862 | header_name = r["gameData"]["status"]["abstractGameState"]
863 | away_name = r["gameData"]["teams"]["away"]["teamName"]
864 | home_name = r["gameData"]["teams"]["home"]["teamName"]
865 | header_row = []
866 | away = []
867 | home = []
868 |
869 | for x in r["liveData"]["linescore"]["innings"]:
870 | header_row.append(str(x.get("num", "")))
871 | away.append(str(x.get("away", {}).get("runs", 0)))
872 | home.append(str(x.get("home", {}).get("runs", 0)))
873 |
874 | if len(r["liveData"]["linescore"]["innings"]) < 9:
875 | for i in range(len(r["liveData"]["linescore"]["innings"]) + 1, 10):
876 | header_row.append(str(i))
877 | away.append(" ")
878 | home.append(" ")
879 |
880 | header_row.extend(["R", "H", "E"])
881 | away_prefix = r["liveData"]["linescore"].get("teams", {}).get("away", {})
882 | away.extend(
883 | [
884 | str(away_prefix.get("runs", 0)),
885 | str(away_prefix.get("hits", 0)),
886 | str(away_prefix.get("errors", 0)),
887 | ]
888 | )
889 | home_prefix = r["liveData"]["linescore"].get("teams", {}).get("home", {})
890 | home.extend(
891 | [
892 | str(home_prefix.get("runs", 0)),
893 | str(home_prefix.get("hits", 0)),
894 | str(home_prefix.get("errors", 0)),
895 | ]
896 | )
897 |
898 | # Build the linescore
899 | for k in [[header_name, header_row], [away_name, away], [home_name, home]]:
900 | linescore += (
901 | "{:<%s}" % str(len(max([header_name, away_name, home_name], key=len)) + 1)
902 | ).format(k[0])
903 | linescore += ("{:^2}" * (len(k[1]) - 3)).format(*k[1])
904 | linescore += ("{:^4}" * 3).format(*k[1][-3:])
905 | linescore += "\n"
906 |
907 | if len(linescore) > 1:
908 | linescore = linescore[:-1] # strip the extra line break
909 |
910 | return linescore
911 |
912 |
913 | def last_game(teamId):
914 | """Get the gamePk for the given team's most recent completed game."""
915 | previousSchedule = get(
916 | "team",
917 | {
918 | "teamId": teamId,
919 | "hydrate": "previousSchedule",
920 | "fields": "teams,team,id,previousGameSchedule,dates,date,games,gamePk,gameDate,status,abstractGameCode",
921 | },
922 | )
923 | games = []
924 | for d in previousSchedule["teams"][0]["previousGameSchedule"]["dates"]:
925 | games.extend([x for x in d["games"] if x["status"]["abstractGameCode"] == "F"])
926 |
927 | if not len(games):
928 | return None
929 |
930 | return games[-1]["gamePk"]
931 |
932 |
933 | def next_game(teamId):
934 | """Get the gamePk for the given team's next unstarted game."""
935 | nextSchedule = get(
936 | "team",
937 | {
938 | "teamId": teamId,
939 | "hydrate": "nextSchedule",
940 | "fields": "teams,team,id,nextGameSchedule,dates,date,games,gamePk,gameDate,status,abstractGameCode",
941 | },
942 | )
943 | games = []
944 | for d in nextSchedule["teams"][0]["nextGameSchedule"]["dates"]:
945 | games.extend([x for x in d["games"] if x["status"]["abstractGameCode"] == "P"])
946 |
947 | if not len(games):
948 | return None
949 |
950 | return games[0]["gamePk"]
951 |
952 |
953 | def game_scoring_plays(gamePk):
954 | """Get a text-formatted list of scoring plays for a given game."""
955 | sortedPlays = game_scoring_play_data(gamePk)
956 | scoring_plays = ""
957 | for a in sortedPlays["plays"]:
958 | scoring_plays += "{}\n{} {} - {}: {}, {}: {}\n\n".format(
959 | a["result"]["description"],
960 | a["about"]["halfInning"][0:1].upper() + a["about"]["halfInning"][1:],
961 | a["about"]["inning"],
962 | sortedPlays["away"]["name"],
963 | a["result"]["awayScore"],
964 | sortedPlays["home"]["name"],
965 | a["result"]["homeScore"],
966 | )
967 |
968 | if len(scoring_plays) > 1:
969 | scoring_plays = scoring_plays[:-2] # strip the extra line break
970 |
971 | return scoring_plays
972 |
973 |
974 | def game_scoring_play_data(gamePk):
975 | """Returns a python dict of scoring plays for a given game containing 3 keys:
976 |
977 | * home - home team data
978 | * away - away team data
979 | * plays - sorted list of scoring play data
980 | """
981 | r = get(
982 | "game",
983 | {
984 | "gamePk": gamePk,
985 | "fields": (
986 | "gamePk,link,gameData,game,pk,teams,away,id,name,teamCode,fileCode,"
987 | "abbreviation,teamName,locationName,shortName,home,liveData,plays,"
988 | "allPlays,scoringPlays,scoringPlays,atBatIndex,result,description,"
989 | "awayScore,homeScore,about,halfInning,inning,endTime"
990 | ),
991 | },
992 | )
993 | if not len(r["liveData"]["plays"].get("scoringPlays", [])):
994 | return {
995 | "home": r["gameData"]["teams"]["home"],
996 | "away": r["gameData"]["teams"]["away"],
997 | "plays": [],
998 | }
999 |
1000 | unorderedPlays = {}
1001 | for i in r["liveData"]["plays"].get("scoringPlays", []):
1002 | play = next(
1003 | (p for p in r["liveData"]["plays"]["allPlays"] if p.get("atBatIndex") == i),
1004 | None,
1005 | )
1006 | if play:
1007 | unorderedPlays.update({play["about"]["endTime"]: play})
1008 |
1009 | sortedPlays = []
1010 | for x in sorted(unorderedPlays):
1011 | sortedPlays.append(unorderedPlays[x])
1012 |
1013 | return {
1014 | "home": r["gameData"]["teams"]["home"],
1015 | "away": r["gameData"]["teams"]["away"],
1016 | "plays": sortedPlays,
1017 | }
1018 |
1019 |
1020 | def game_highlights(gamePk):
1021 | """Get the highlight video links for a given game."""
1022 | sortedHighlights = game_highlight_data(gamePk)
1023 |
1024 | highlights = ""
1025 | for a in sortedHighlights:
1026 | # if sum(1 for t in a['keywordsAll'] if t['type']=='team_id') == 1:
1027 | # highlights += next(t['displayName'] for t in a['keywordsAll'] if t['type']=='team_id') + '\n'
1028 | highlights += "{} ({})\n{}\n{}\n\n".format(
1029 | a.get("title", a.get("headline", "")),
1030 | a["duration"],
1031 | a.get("description", ""),
1032 | next(
1033 | (s["url"] for s in a["playbacks"] if s["name"] == "mp4Avc"),
1034 | next(
1035 | (
1036 | s["url"]
1037 | for s in a["playbacks"]
1038 | if s["name"] == "FLASH_2500K_1280X720"
1039 | ),
1040 | "Link not found",
1041 | ),
1042 | ),
1043 | )
1044 |
1045 | return highlights
1046 |
1047 |
1048 | def game_highlight_data(gamePk):
1049 | """Returns a list of highlight data for a given game."""
1050 | r = get(
1051 | "schedule",
1052 | {
1053 | "sportId": 1,
1054 | "gamePk": gamePk,
1055 | "hydrate": "game(content(highlights(highlights)))",
1056 | "fields": "dates,date,games,gamePk,content,highlights,items,headline,type,value,title,description,duration,playbacks,name,url",
1057 | },
1058 | )
1059 | gameHighlights = (
1060 | r["dates"][0]["games"][0]
1061 | .get("content", {})
1062 | .get("highlights", {})
1063 | .get("highlights", {})
1064 | )
1065 | if not gameHighlights or not len(gameHighlights.get("items", [])):
1066 | return []
1067 |
1068 | unorderedHighlights = {}
1069 | for v in (
1070 | x
1071 | for x in gameHighlights["items"]
1072 | if isinstance(x, dict) and x["type"] == "video"
1073 | ):
1074 | unorderedHighlights.update({v["date"]: v})
1075 |
1076 | sortedHighlights = []
1077 | for x in sorted(unorderedHighlights):
1078 | sortedHighlights.append(unorderedHighlights[x])
1079 |
1080 | return sortedHighlights
1081 |
1082 |
1083 | def game_pace(season=datetime.now().year, sportId=1):
1084 | """Get a text-formatted list about pace of game for a given season (back to 1999)."""
1085 | r = game_pace_data(season, sportId)
1086 |
1087 | pace = ""
1088 |
1089 | pace += "{} Game Pace Stats\n".format(season)
1090 | for s in r["sports"]:
1091 | for k in s.keys():
1092 | if k in ["season", "sport"]:
1093 | continue
1094 |
1095 | if k == "prPortalCalculatedFields":
1096 | for x in s[k].keys():
1097 | pace += "{}: {}\n".format(x, s[k][x])
1098 | else:
1099 | pace += "{}: {}\n".format(k, s[k])
1100 |
1101 | return pace
1102 |
1103 |
1104 | def game_pace_data(season=datetime.now().year, sportId=1):
1105 | """Returns data about pace of game for a given season (back to 1999)."""
1106 | params = {}
1107 | if season:
1108 | params.update({"season": season})
1109 |
1110 | if sportId:
1111 | params.update({"sportId": sportId})
1112 |
1113 | r = get("gamePace", params)
1114 |
1115 | if not len(r["sports"]):
1116 | raise ValueError(
1117 | "No game pace info found for the {} season. Game pace data appears to begin in 1999.".format(
1118 | season
1119 | )
1120 | )
1121 |
1122 | return r
1123 |
1124 |
1125 | def player_stats(
1126 | personId, group="[hitting,pitching,fielding]", type="season", season=None
1127 | ):
1128 | """Get current season or career stats for a given player."""
1129 | player = player_stat_data(personId, group, type, season)
1130 |
1131 | stats = ""
1132 | stats += player["first_name"]
1133 | if player["nickname"]:
1134 | stats += ' "{nickname}"'.format(**player)
1135 |
1136 | stats += " {last_name}, {position} ({mlb_debut:.4}-".format(**player)
1137 | if not player["active"]:
1138 | stats += "{last_played:.4}".format(**player)
1139 |
1140 | stats += ")\n\n"
1141 |
1142 | for x in player["stats"]:
1143 | stats += (
1144 | x["type"][0:1].upper()
1145 | + x["type"][1:]
1146 | + " "
1147 | + x["group"][0:1].upper()
1148 | + x["group"][1:]
1149 | )
1150 | if x["stats"].get("position"):
1151 | stats += " ({})".format(x["stats"]["position"]["abbreviation"])
1152 |
1153 | stats += "\n"
1154 | for y in x["stats"].keys():
1155 | if y == "position":
1156 | continue
1157 | stats += "{}: {}\n".format(y, x["stats"][y])
1158 |
1159 | stats += "\n"
1160 |
1161 | return stats
1162 |
1163 |
1164 | def player_stat_data(
1165 | personId, group="[hitting,pitching,fielding]", type="season", sportId=1, season=None
1166 | ):
1167 | """Returns a list of current season or career stat data for a given player."""
1168 |
1169 | if season is not None and "season" not in type:
1170 | raise ValueError(
1171 | "The 'season' parameter is only valid when using the 'season' type."
1172 | )
1173 |
1174 | params = {
1175 | "personId": personId,
1176 | "hydrate": "stats(group="
1177 | + group
1178 | + ",type="
1179 | + type
1180 | + (",season=" + str(season) if season else "")
1181 | + ",sportId="
1182 | + str(sportId)
1183 | + "),currentTeam",
1184 | }
1185 | r = get("person", params)
1186 |
1187 | stat_groups = []
1188 |
1189 | player = {
1190 | "id": r["people"][0]["id"],
1191 | "first_name": r["people"][0]["useName"],
1192 | "last_name": r["people"][0]["lastName"],
1193 | "active": r["people"][0]["active"],
1194 | "current_team": r["people"][0]["currentTeam"]["name"],
1195 | "position": r["people"][0]["primaryPosition"]["abbreviation"],
1196 | "nickname": r["people"][0].get("nickName"),
1197 | "last_played": r["people"][0].get("lastPlayedDate"),
1198 | "mlb_debut": r["people"][0].get("mlbDebutDate"),
1199 | "bat_side": r["people"][0]["batSide"]["description"],
1200 | "pitch_hand": r["people"][0]["pitchHand"]["description"],
1201 | }
1202 |
1203 | for s in r["people"][0].get("stats", []):
1204 | for i in range(0, len(s["splits"])):
1205 | stat_group = {
1206 | "type": s["type"]["displayName"],
1207 | "group": s["group"]["displayName"],
1208 | "season": s["splits"][i].get("season"),
1209 | "stats": s["splits"][i]["stat"],
1210 | }
1211 | stat_groups.append(stat_group)
1212 |
1213 | player.update({"stats": stat_groups})
1214 |
1215 | return player
1216 |
1217 |
1218 | def latest_season(sportId=1):
1219 | """Get the latest season for a given sportId. Returns a dict containing seasonId and various dates."""
1220 | params = {
1221 | "sportId": sportId,
1222 | "seasonId": "all",
1223 | }
1224 | all_seasons = get("season", params)
1225 | return next(
1226 | (
1227 | s
1228 | for s in all_seasons.get("seasons", [])
1229 | if (datetime.today().strftime("%Y-%m-%d") < s.get("seasonEndDate", ""))
1230 | ),
1231 | all_seasons["seasons"][-1],
1232 | )
1233 |
1234 |
1235 | def lookup_player(lookup_value, gameType=None, season=None, sportId=1):
1236 | """Get data about players based on first, last, or full name."""
1237 | params = {
1238 | "sportId": sportId,
1239 | "fields": "people,id,fullName,firstName,lastName,primaryNumber,currentTeam,id,primaryPosition,code,abbreviation,useName,boxscoreName,nickName,mlbDebutDate,nameFirstLast,firstLastName,lastFirstName,lastInitName,initLastName,fullFMLName,fullLFMName,nameSlug",
1240 | }
1241 | if gameType:
1242 | params.update(
1243 | {
1244 | "gameType": gameType,
1245 | }
1246 | )
1247 | if not season:
1248 | season_data = latest_season(sportId=sportId)
1249 | season = season_data.get("seasonId", datetime.now().year)
1250 | params.update(
1251 | {
1252 | "season": season,
1253 | }
1254 | )
1255 | r = get("sports_players", params)
1256 |
1257 | players = []
1258 | lookup_values = str(lookup_value).lower().split()
1259 | for player in r["people"]:
1260 | for l in lookup_values:
1261 | for v in player.values():
1262 | if l in str(v).lower():
1263 | break
1264 | else:
1265 | break
1266 | else:
1267 | players.append(player)
1268 |
1269 | return players
1270 |
1271 |
1272 | def lookup_team(lookup_value, activeStatus="Y", season=None, sportIds=1):
1273 | """Get a info about a team or teams based on the team name, city, abbreviation, or file code."""
1274 | params = {
1275 | "activeStatus": activeStatus,
1276 | "sportIds": sportIds,
1277 | "fields": "teams,id,name,teamCode,fileCode,teamName,locationName,shortName",
1278 | }
1279 | if not season:
1280 | season_data = latest_season(sportId=str(sportIds).split(",")[0])
1281 | season = season_data.get("seasonId", datetime.now().year)
1282 | params.update(
1283 | {
1284 | "season": season,
1285 | }
1286 | )
1287 | r = get("teams", params)
1288 |
1289 | teams = []
1290 | for team in r["teams"]:
1291 | for v in team.values():
1292 | if str(lookup_value).lower() in str(v).lower():
1293 | teams.append(team)
1294 | break
1295 |
1296 | return teams
1297 |
1298 |
1299 | def team_leaders(
1300 | teamId, leaderCategories, season=datetime.now().year, leaderGameTypes="R", limit=10
1301 | ):
1302 | """Get stat leaders for a given team."""
1303 | lines = team_leader_data(teamId, leaderCategories, season, leaderGameTypes, limit)
1304 |
1305 | leaders = ""
1306 |
1307 | leaders += "{:<4} {:<20} {:<5}\n".format(*["Rank", "Name", "Value"])
1308 | for a in lines:
1309 | leaders += "{:^4} {:<20} {:^5}\n".format(*a)
1310 |
1311 | return leaders
1312 |
1313 |
1314 | def team_leader_data(
1315 | teamId, leaderCategories, season=datetime.now().year, leaderGameTypes="R", limit=10
1316 | ):
1317 | """Returns a python list of stat leader data for a given team."""
1318 | params = {
1319 | "leaderCategories": leaderCategories,
1320 | "season": season,
1321 | "teamId": teamId,
1322 | "leaderGameTypes": leaderGameTypes,
1323 | "limit": limit,
1324 | }
1325 | params.update({"fields": "teamLeaders,leaders,rank,value,person,fullName"})
1326 |
1327 | r = get("team_leaders", params)
1328 |
1329 | lines = []
1330 | for player in [x for x in r["teamLeaders"][0]["leaders"]]:
1331 | lines.append([player["rank"], player["person"]["fullName"], player["value"]])
1332 |
1333 | return lines
1334 |
1335 |
1336 | def league_leaders(
1337 | leaderCategories,
1338 | season=None,
1339 | limit=10,
1340 | statGroup=None,
1341 | leagueId=None,
1342 | gameTypes=None,
1343 | playerPool=None,
1344 | sportId=1,
1345 | statType=None,
1346 | ):
1347 | """Get stat leaders overall or for a given league (103=AL, 104=NL)."""
1348 | lines = league_leader_data(
1349 | leaderCategories,
1350 | season,
1351 | limit,
1352 | statGroup,
1353 | leagueId,
1354 | gameTypes,
1355 | playerPool,
1356 | sportId,
1357 | statType,
1358 | )
1359 |
1360 | leaders = ""
1361 |
1362 | leaders += "{:<4} {:<20} {:<23} {:<5}\n".format(*["Rank", "Name", "Team", "Value"])
1363 | for a in lines:
1364 | leaders += "{:^4} {:<20} {:<23} {:^5}\n".format(*a)
1365 |
1366 | return leaders
1367 |
1368 |
1369 | def league_leader_data(
1370 | leaderCategories,
1371 | season=None,
1372 | limit=10,
1373 | statGroup=None,
1374 | leagueId=None,
1375 | gameTypes=None,
1376 | playerPool=None,
1377 | sportId=1,
1378 | statType=None,
1379 | ):
1380 | """Returns a python list of stat leaders overall or for a given league (103=AL, 104=NL)."""
1381 | params = {"leaderCategories": leaderCategories, "sportId": sportId, "limit": limit}
1382 | if season:
1383 | params.update({"season": season})
1384 |
1385 | if statType:
1386 | params.update({"statType": statType})
1387 |
1388 | if not season and not statType:
1389 | params.update(
1390 | {"season": datetime.now().year}
1391 | ) # default season to current year if no season or statType provided
1392 |
1393 | if statGroup:
1394 | if statGroup == "batting":
1395 | statGroup = "hitting"
1396 |
1397 | params.update({"statGroup": statGroup})
1398 |
1399 | if gameTypes:
1400 | params.update({"leaderGameTypes": gameTypes})
1401 |
1402 | if leagueId:
1403 | params.update({"leagueId": leagueId})
1404 |
1405 | if playerPool:
1406 | params.update({"playerPool": playerPool})
1407 |
1408 | params.update(
1409 | {
1410 | "fields": "leagueLeaders,leaders,rank,value,team,name,league,name,person,fullName"
1411 | }
1412 | )
1413 |
1414 | r = get("stats_leaders", params)
1415 |
1416 | lines = []
1417 | for player in [x for x in r["leagueLeaders"][0]["leaders"]]:
1418 | lines.append(
1419 | [
1420 | player["rank"],
1421 | player["person"]["fullName"],
1422 | player["team"].get("name", ""),
1423 | player["value"],
1424 | ]
1425 | )
1426 |
1427 | return lines
1428 |
1429 |
1430 | def standings(
1431 | leagueId="103,104",
1432 | division="all",
1433 | include_wildcard=True,
1434 | season=None,
1435 | standingsTypes=None,
1436 | date=None,
1437 | ):
1438 | """Get formatted standings for a given league/division and season."""
1439 | divisions = standings_data(
1440 | leagueId, division, include_wildcard, season, standingsTypes, date
1441 | )
1442 |
1443 | standings = ""
1444 |
1445 | for div in divisions.values():
1446 | standings += div["div_name"] + "\n"
1447 | if include_wildcard:
1448 | standings += (
1449 | "{:^4} {:<21} {:^3} {:^3} {:^4} {:^4} {:^7} {:^5} {:^4}\n".format(
1450 | *[
1451 | "Rank",
1452 | "Team",
1453 | "W",
1454 | "L",
1455 | "GB",
1456 | "(E#)",
1457 | "WC Rank",
1458 | "WC GB",
1459 | "(E#)",
1460 | ]
1461 | )
1462 | )
1463 | for t in div["teams"]:
1464 | standings += "{div_rank:^4} {name:<21} {w:^3} {l:^3} {gb:^4} {elim_num:^4} {wc_rank:^7} {wc_gb:^5} {wc_elim_num:^4}\n".format(
1465 | **t
1466 | )
1467 | else:
1468 | standings += "{:^4} {:<21} {:^3} {:^3} {:^4} {:^4}\n".format(
1469 | *["Rank", "Team", "W", "L", "GB", "(E#)"]
1470 | )
1471 | for t in div["teams"]:
1472 | standings += "{div_rank:^4} {name:<21} {w:^3} {l:^3} {gb:^4} {elim_num:^4}\n".format(
1473 | **t
1474 | )
1475 | standings += "\n"
1476 |
1477 | return standings
1478 |
1479 |
1480 | def standings_data(
1481 | leagueId="103,104",
1482 | division="all",
1483 | include_wildcard=True,
1484 | season=None,
1485 | standingsTypes=None,
1486 | date=None,
1487 | ):
1488 | """Returns a dict of standings data for a given league/division and season."""
1489 | params = {"leagueId": leagueId}
1490 | if date:
1491 | params.update({"date": date})
1492 |
1493 | if not season:
1494 | if date:
1495 | season = date[-4:]
1496 | else:
1497 | season = datetime.now().year
1498 |
1499 | if not standingsTypes:
1500 | standingsTypes = "regularSeason"
1501 |
1502 | params.update({"season": season, "standingsTypes": standingsTypes})
1503 | params.update(
1504 | {
1505 | "hydrate": "team(division)",
1506 | "fields": "records,standingsType,teamRecords,team,name,division,id,nameShort,abbreviation,divisionRank,gamesBack,wildCardRank,wildCardGamesBack,wildCardEliminationNumber,divisionGamesBack,clinched,eliminationNumber,winningPercentage,type,wins,losses,leagueRank,sportRank",
1507 | }
1508 | )
1509 |
1510 | r = get("standings", params)
1511 |
1512 | divisions = {}
1513 |
1514 | for y in r["records"]:
1515 | for x in (
1516 | x
1517 | for x in y["teamRecords"]
1518 | if str(division).lower() == "all"
1519 | or str(division).lower() == x["team"]["division"]["abbreviation"].lower()
1520 | or str(division) == str(x["team"]["division"]["id"])
1521 | ):
1522 | if x["team"]["division"]["id"] not in divisions.keys():
1523 | divisions.update(
1524 | {
1525 | x["team"]["division"]["id"]: {
1526 | "div_name": x["team"]["division"]["name"],
1527 | "teams": [],
1528 | }
1529 | }
1530 | )
1531 |
1532 | team = {
1533 | "name": x["team"]["name"],
1534 | "div_rank": x["divisionRank"],
1535 | "w": x["wins"],
1536 | "l": x["losses"],
1537 | "gb": x["gamesBack"],
1538 | "wc_rank": x.get("wildCardRank", "-"),
1539 | "wc_gb": x.get("wildCardGamesBack", "-"),
1540 | "wc_elim_num": x.get("wildCardEliminationNumber", "-"),
1541 | "elim_num": x.get("eliminationNumber", "-"),
1542 | "team_id": x["team"]["id"],
1543 | "league_rank": x.get("leagueRank", "-"),
1544 | "sport_rank": x.get("sportRank", "-"),
1545 | }
1546 | divisions[x["team"]["division"]["id"]]["teams"].append(team)
1547 |
1548 | return divisions
1549 |
1550 |
1551 | def roster(teamId, rosterType=None, season=datetime.now().year, date=None):
1552 | """Get the roster for a given team."""
1553 | if not rosterType:
1554 | rosterType = "active"
1555 |
1556 | params = {"rosterType": rosterType, "season": season, "teamId": teamId}
1557 | if date:
1558 | params.update({"date": date})
1559 |
1560 | r = get("team_roster", params)
1561 |
1562 | roster = ""
1563 | players = []
1564 | for x in r["roster"]:
1565 | players.append(
1566 | [x["jerseyNumber"], x["position"]["abbreviation"], x["person"]["fullName"]]
1567 | )
1568 |
1569 | for i in range(0, len(players)):
1570 | roster += ("#{:<3} {:<3} {}\n").format(*players[i])
1571 |
1572 | return roster
1573 |
1574 |
1575 | def meta(type, fields=None):
1576 | """Get available values from StatsAPI for use in other queries,
1577 | or look up descriptions for values found in API results.
1578 |
1579 | For example, to get a list of leader categories to use when calling team_leaders():
1580 | statsapi.meta('leagueLeaderTypes')
1581 | """
1582 | types = [
1583 | "awards",
1584 | "baseballStats",
1585 | "eventTypes",
1586 | "freeGameTypes",
1587 | "gameStatus",
1588 | "gameTypes",
1589 | "hitTrajectories",
1590 | "jobTypes",
1591 | "languages",
1592 | "leagueLeaderTypes",
1593 | "logicalEvents",
1594 | "metrics",
1595 | "pitchCodes",
1596 | "pitchTypes",
1597 | "platforms",
1598 | "positions",
1599 | "reviewReasons",
1600 | "rosterTypes",
1601 | "runnerDetailTypes",
1602 | "scheduleTypes",
1603 | "scheduleEventTypes",
1604 | "situationCodes",
1605 | "sky",
1606 | "standingsTypes",
1607 | "statGroups",
1608 | "statTypes",
1609 | "violationTypes",
1610 | "windDirection",
1611 | ]
1612 | if type not in types:
1613 | raise ValueError("Invalid meta type. Available meta types: %s." % types)
1614 |
1615 | return get("meta", {"type": type})
1616 |
1617 |
1618 | def notes(endpoint):
1619 | """Get notes for a given endpoint."""
1620 | msg = ""
1621 | if not endpoint:
1622 | msg = "No endpoint specified."
1623 | else:
1624 | if not ENDPOINTS.get(endpoint):
1625 | msg = "Invalid endpoint specified."
1626 | else:
1627 | msg += "Endpoint: " + endpoint + " \n"
1628 | path_params = [k for k, v in ENDPOINTS[endpoint]["path_params"].items()]
1629 | required_path_params = [
1630 | k
1631 | for k, v in ENDPOINTS[endpoint]["path_params"].items()
1632 | if v["required"]
1633 | ]
1634 | if required_path_params == []:
1635 | required_path_params = "None"
1636 |
1637 | query_params = ENDPOINTS[endpoint]["query_params"]
1638 | required_query_params = ENDPOINTS[endpoint]["required_params"]
1639 | if required_query_params == [[]]:
1640 | required_query_params = "None"
1641 | msg += "All path parameters: %s. \n" % path_params
1642 | msg += (
1643 | "Required path parameters (note: ver will be included by default): %s. \n"
1644 | % required_path_params
1645 | )
1646 | msg += "All query parameters: %s. \n" % query_params
1647 | msg += "Required query parameters: %s. \n" % required_query_params
1648 | if "hydrate" in query_params:
1649 | msg += "The hydrate function is supported by this endpoint. Call the endpoint with {'hydrate':'hydrations'} in the parameters to return a list of available hydrations. For example, statsapi.get('schedule',{'sportId':1,'hydrate':'hydrations','fields':'hydrations'})\n"
1650 | if ENDPOINTS[endpoint].get("note"):
1651 | msg += "Developer notes: %s" % ENDPOINTS[endpoint].get("note")
1652 |
1653 | return msg
1654 |
1655 |
1656 | def get(endpoint, params={}, force=False, *, request_kwargs={}):
1657 | """Call MLB StatsAPI and return JSON data.
1658 |
1659 | This function is for advanced querying of the MLB StatsAPI,
1660 | and is used by the functions in this library.
1661 | """
1662 | # Lookup endpoint from input parameter
1663 | ep = ENDPOINTS.get(endpoint)
1664 | if not ep:
1665 | raise ValueError("Invalid endpoint (" + str(endpoint) + ").")
1666 |
1667 | url = ep["url"]
1668 | logger.debug("URL: {}".format(url))
1669 |
1670 | path_params = {}
1671 | query_params = {}
1672 |
1673 | # Parse parameters into path and query parameters, and discard invalid parameters
1674 | for p, pv in params.items():
1675 | if ep["path_params"].get(p):
1676 | logger.debug("Found path param: {}".format(p))
1677 | if ep["path_params"][p].get("type") == "bool":
1678 | if str(pv).lower() == "false":
1679 | path_params.update({p: ep["path_params"][p].get("False", "")})
1680 | elif str(pv).lower() == "true":
1681 | path_params.update({p: ep["path_params"][p].get("True", "")})
1682 | else:
1683 | path_params.update({p: str(pv)})
1684 | elif p in ep["query_params"]:
1685 | logger.debug("Found query param: {}".format(p))
1686 | query_params.update({p: str(pv)})
1687 | else:
1688 | if force:
1689 | logger.debug(
1690 | "Found invalid param, forcing into query parameters per force flag: {}".format(
1691 | p
1692 | )
1693 | )
1694 | query_params.update({p: str(pv)})
1695 | else:
1696 | logger.debug("Found invalid param, ignoring: {}".format(p))
1697 |
1698 | logger.debug("path_params: {}".format(path_params))
1699 | logger.debug("query_params: {}".format(query_params))
1700 |
1701 | # Replace path parameters with their values
1702 | for k, v in path_params.items():
1703 | logger.debug("Replacing {%s}" % k)
1704 | url = url.replace(
1705 | "{" + k + "}",
1706 | ("/" if ep["path_params"][k]["leading_slash"] else "")
1707 | + v
1708 | + ("/" if ep["path_params"][k]["trailing_slash"] else ""),
1709 | )
1710 | logger.debug("URL: {}".format(url))
1711 |
1712 | while url.find("{") != -1 and url.find("}") > url.find("{"):
1713 | param = url[url.find("{") + 1 : url.find("}")]
1714 | if ep.get("path_params", {}).get(param, {}).get("required"):
1715 | if (
1716 | ep["path_params"][param]["default"]
1717 | and ep["path_params"][param]["default"] != ""
1718 | ):
1719 | logger.debug(
1720 | "Replacing {%s} with default: %s."
1721 | % (param, ep["path_params"][param]["default"])
1722 | )
1723 | url = url.replace(
1724 | "{" + param + "}",
1725 | ("/" if ep["path_params"][param]["leading_slash"] else "")
1726 | + ep["path_params"][param]["default"]
1727 | + ("/" if ep["path_params"][param]["trailing_slash"] else ""),
1728 | )
1729 | else:
1730 | if force:
1731 | logger.warning(
1732 | "Missing required path parameter {%s}, proceeding anyway per force flag..."
1733 | % param
1734 | )
1735 | else:
1736 | raise ValueError("Missing required path parameter {%s}" % param)
1737 | else:
1738 | logger.debug("Removing optional param {%s}" % param)
1739 | url = url.replace("{" + param + "}", "")
1740 |
1741 | logger.debug("URL: {}".format(url))
1742 | # Add query parameters to the URL
1743 | if len(query_params) > 0:
1744 | for k, v in query_params.items():
1745 | logger.debug("Adding query parameter {}={}".format(k, v))
1746 | sep = "?" if url.find("?") == -1 else "&"
1747 | url += sep + k + "=" + v
1748 | logger.debug("URL: {}".format(url))
1749 |
1750 | # Make sure required parameters are present
1751 | satisfied = False
1752 | missing_params = []
1753 | for x in ep.get("required_params", []):
1754 | if len(x) == 0:
1755 | satisfied = True
1756 | else:
1757 | missing_params.extend([a for a in x if a not in query_params])
1758 | if len(missing_params) == 0:
1759 | satisfied = True
1760 | break
1761 |
1762 | if not satisfied and not force:
1763 | if ep.get("note"):
1764 | note = "\n--Endpoint note: " + ep.get("note")
1765 | else:
1766 | note = ""
1767 |
1768 | raise ValueError(
1769 | "Missing required parameter(s): "
1770 | + ", ".join(missing_params)
1771 | + ".\n--Required parameters for the "
1772 | + endpoint
1773 | + " endpoint: "
1774 | + str(ep.get("required_params", []))
1775 | + ". \n--Note: If there are multiple sets in the required parameter list, you can choose any of the sets."
1776 | + note
1777 | )
1778 |
1779 | if len(request_kwargs):
1780 | logger.debug(
1781 | "Including request_kwargs in requests.get call: {}".format(request_kwargs)
1782 | )
1783 |
1784 | # Make the request
1785 | r = requests.get(url, **request_kwargs)
1786 | if r.status_code not in [200, 201]:
1787 | r.raise_for_status()
1788 | else:
1789 | return r.json()
1790 |
1791 | return None
1792 |
--------------------------------------------------------------------------------
/statsapi/endpoints.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | BASE_URL = "https://statsapi.mlb.com/api/"
4 |
5 | ENDPOINTS = {
6 | "attendance": {
7 | "url": BASE_URL + "{ver}/attendance",
8 | "path_params": {
9 | "ver": {
10 | "type": "str",
11 | "default": "v1",
12 | "leading_slash": False,
13 | "trailing_slash": False,
14 | "required": True,
15 | }
16 | },
17 | "query_params": [
18 | "teamId",
19 | "leagueId",
20 | "season",
21 | "date",
22 | "leagueListId",
23 | "gameType",
24 | "fields",
25 | ],
26 | "required_params": [["teamId"], ["leagueId"], ["leagueListid"]],
27 | },
28 | "awards": {
29 | "url": BASE_URL + "{ver}/awards{awardId}{recipients}",
30 | "path_params": {
31 | "ver": {
32 | "type": "str",
33 | "default": "v1",
34 | "leading_slash": False,
35 | "trailing_slash": False,
36 | "required": True,
37 | },
38 | "awardId": {
39 | "type": "str",
40 | "default": None,
41 | "leading_slash": True,
42 | "trailing_slash": False,
43 | "required": False,
44 | },
45 | "recipients": {
46 | "type": "bool",
47 | "default": True,
48 | "True": "/recipients",
49 | "False": "",
50 | "leading_slash": False,
51 | "trailing_slash": False,
52 | "required": False,
53 | },
54 | },
55 | "query_params": ["sportId", "leagueId", "season", "hydrate", "fields"],
56 | "required_params": [[]],
57 | "note": "Call awards endpoint with no parameters to return a list of awardIds.",
58 | },
59 | "conferences": {
60 | "url": BASE_URL + "{ver}/conferences",
61 | "path_params": {
62 | "ver": {
63 | "type": "str",
64 | "default": "v1",
65 | "leading_slash": False,
66 | "trailing_slash": False,
67 | "required": True,
68 | }
69 | },
70 | "query_params": ["conferenceId", "season", "fields"],
71 | "required_params": [[]],
72 | },
73 | "divisions": {
74 | "url": BASE_URL + "{ver}/divisions",
75 | "path_params": {
76 | "ver": {
77 | "type": "str",
78 | "default": "v1",
79 | "leading_slash": False,
80 | "trailing_slash": False,
81 | "required": True,
82 | }
83 | },
84 | "query_params": ["divisionId", "leagueId", "sportId", "season"],
85 | "required_params": [[]],
86 | "note": "Call divisions endpoint with no parameters to return a list of divisions.",
87 | },
88 | "draft": {
89 | "url": BASE_URL + "{ver}/draft{prospects}{year}{latest}",
90 | "path_params": {
91 | "ver": {
92 | "type": "str",
93 | "default": "v1",
94 | "leading_slash": False,
95 | "trailing_slash": False,
96 | "required": True,
97 | },
98 | "prospects": {
99 | "type": "bool",
100 | "default": False,
101 | "True": "/prospects",
102 | "False": "",
103 | "leading_slash": False,
104 | "trailing_slash": False,
105 | "required": False,
106 | },
107 | "year": {
108 | "type": "str",
109 | "default": "",
110 | "leading_slash": True,
111 | "trailing_slash": False,
112 | "required": False,
113 | },
114 | "latest": {
115 | "type": "bool",
116 | "default": False,
117 | "True": "/latest",
118 | "False": "",
119 | "leading_slash": False,
120 | "trailing_slash": False,
121 | "required": False,
122 | },
123 | },
124 | "query_params": [
125 | "limit",
126 | "fields",
127 | "round",
128 | "name",
129 | "school",
130 | "state",
131 | "country",
132 | "position",
133 | "teamId",
134 | "playerId",
135 | "bisPlayerId",
136 | ],
137 | "required_params": [[]],
138 | "note": 'No query parameters are honored when "latest" endpoint is queried (year is still required). Prospects and Latest cannot be used together.',
139 | },
140 | "game": {
141 | "url": BASE_URL + "{ver}/game/{gamePk}/feed/live",
142 | "path_params": {
143 | "ver": {
144 | "type": "str",
145 | "default": "v1.1",
146 | "leading_slash": False,
147 | "trailing_slash": False,
148 | "required": True,
149 | },
150 | "gamePk": {
151 | "type": "str",
152 | "default": "",
153 | "leading_slash": False,
154 | "trailing_slash": False,
155 | "required": True,
156 | },
157 | },
158 | "query_params": ["timecode", "hydrate", "fields"],
159 | "required_params": [[]],
160 | },
161 | "game_diff": {
162 | "url": BASE_URL + "{ver}/game/{gamePk}/feed/live/diffPatch",
163 | "path_params": {
164 | "ver": {
165 | "type": "str",
166 | "default": "v1.1",
167 | "leading_slash": False,
168 | "trailing_slash": False,
169 | "required": True,
170 | },
171 | "gamePk": {
172 | "type": "str",
173 | "default": "",
174 | "leading_slash": False,
175 | "trailing_slash": False,
176 | "required": True,
177 | },
178 | },
179 | "query_params": ["startTimecode", "endTimecode"],
180 | "required_params": [["startTimecode", "endTimecode"]],
181 | },
182 | "game_timestamps": {
183 | "url": BASE_URL + "{ver}/game/{gamePk}/feed/live/timestamps",
184 | "path_params": {
185 | "ver": {
186 | "type": "str",
187 | "default": "v1.1",
188 | "leading_slash": False,
189 | "trailing_slash": False,
190 | "required": True,
191 | },
192 | "gamePk": {
193 | "type": "str",
194 | "default": "",
195 | "leading_slash": False,
196 | "trailing_slash": False,
197 | "required": True,
198 | },
199 | },
200 | "query_params": [],
201 | "required_params": [[]],
202 | },
203 | "game_changes": {
204 | "url": BASE_URL + "{ver}/game/changes",
205 | "path_params": {
206 | "ver": {
207 | "type": "str",
208 | "default": "v1",
209 | "leading_slash": False,
210 | "trailing_slash": False,
211 | "required": True,
212 | }
213 | },
214 | "query_params": ["updatedSince", "sportId", "gameType", "season", "fields"],
215 | "required_params": [["updatedSince"]],
216 | },
217 | "game_contextMetrics": {
218 | "url": BASE_URL + "{ver}/game/{gamePk}/contextMetrics",
219 | "path_params": {
220 | "ver": {
221 | "type": "str",
222 | "default": "v1",
223 | "leading_slash": False,
224 | "trailing_slash": False,
225 | "required": True,
226 | },
227 | "gamePk": {
228 | "type": "str",
229 | "default": "",
230 | "leading_slash": False,
231 | "trailing_slash": False,
232 | "required": True,
233 | },
234 | },
235 | "query_params": ["timecode", "fields"],
236 | "required_params": [[]],
237 | },
238 | "game_winProbability": {
239 | "url": BASE_URL + "{ver}/game/{gamePk}/winProbability",
240 | "path_params": {
241 | "ver": {
242 | "type": "str",
243 | "default": "v1",
244 | "leading_slash": False,
245 | "trailing_slash": False,
246 | "required": True,
247 | },
248 | "gamePk": {
249 | "type": "str",
250 | "default": "",
251 | "leading_slash": False,
252 | "trailing_slash": False,
253 | "required": True,
254 | },
255 | },
256 | "query_params": ["timecode", "fields"],
257 | "required_params": [[]],
258 | "note": "If you only want the current win probability for each team, try the game_contextMetrics endpoint instad.",
259 | },
260 | "game_boxscore": {
261 | "url": BASE_URL + "{ver}/game/{gamePk}/boxscore",
262 | "path_params": {
263 | "ver": {
264 | "type": "str",
265 | "default": "v1",
266 | "leading_slash": False,
267 | "trailing_slash": False,
268 | "required": True,
269 | },
270 | "gamePk": {
271 | "type": "str",
272 | "default": "",
273 | "leading_slash": False,
274 | "trailing_slash": False,
275 | "required": True,
276 | },
277 | },
278 | "query_params": ["timecode", "fields"],
279 | "required_params": [[]],
280 | },
281 | "game_content": {
282 | "url": BASE_URL + "{ver}/game/{gamePk}/content",
283 | "path_params": {
284 | "ver": {
285 | "type": "str",
286 | "default": "v1",
287 | "leading_slash": False,
288 | "trailing_slash": False,
289 | "required": True,
290 | },
291 | "gamePk": {
292 | "type": "str",
293 | "default": "",
294 | "leading_slash": False,
295 | "trailing_slash": False,
296 | "required": True,
297 | },
298 | },
299 | "query_params": ["highlightLimit"],
300 | "required_params": [[]],
301 | },
302 | "game_color": {
303 | "url": BASE_URL + "{ver}/game/{gamePk}/feed/color",
304 | "path_params": {
305 | "ver": {
306 | "type": "str",
307 | "default": "v1",
308 | "leading_slash": False,
309 | "trailing_slash": False,
310 | "required": True,
311 | },
312 | "gamePk": {
313 | "type": "str",
314 | "default": "",
315 | "leading_slash": False,
316 | "trailing_slash": False,
317 | "required": True,
318 | },
319 | },
320 | "query_params": ["timecode", "fields"],
321 | "required_params": [[]],
322 | },
323 | "game_color_diff": {
324 | "url": BASE_URL + "{ver}/game/{gamePk}/feed/color/diffPatch",
325 | "path_params": {
326 | "ver": {
327 | "type": "str",
328 | "default": "v1",
329 | "leading_slash": False,
330 | "trailing_slash": False,
331 | "required": True,
332 | },
333 | "gamePk": {
334 | "type": "str",
335 | "default": "",
336 | "leading_slash": False,
337 | "trailing_slash": False,
338 | "required": True,
339 | },
340 | },
341 | "query_params": ["startTimecode", "endTimecode"],
342 | "required_params": [["startTimeCode", "endTimeCode"]],
343 | },
344 | "game_color_timestamps": {
345 | "url": BASE_URL + "{ver}/game/{gamePk}/feed/color/timestamps",
346 | "path_params": {
347 | "ver": {
348 | "type": "str",
349 | "default": "v1",
350 | "leading_slash": False,
351 | "trailing_slash": False,
352 | "required": True,
353 | },
354 | "gamePk": {
355 | "type": "str",
356 | "default": "",
357 | "leading_slash": False,
358 | "trailing_slash": False,
359 | "required": True,
360 | },
361 | },
362 | "query_params": [],
363 | "required_params": [[]],
364 | },
365 | "game_linescore": {
366 | "url": BASE_URL + "{ver}/game/{gamePk}/linescore",
367 | "path_params": {
368 | "ver": {
369 | "type": "str",
370 | "default": "v1",
371 | "leading_slash": False,
372 | "trailing_slash": False,
373 | "required": True,
374 | },
375 | "gamePk": {
376 | "type": "str",
377 | "default": "",
378 | "leading_slash": False,
379 | "trailing_slash": False,
380 | "required": True,
381 | },
382 | },
383 | "query_params": ["timecode", "fields"],
384 | "required_params": [[]],
385 | },
386 | "game_playByPlay": {
387 | "url": BASE_URL + "{ver}/game/{gamePk}/playByPlay",
388 | "path_params": {
389 | "ver": {
390 | "type": "str",
391 | "default": "v1",
392 | "leading_slash": False,
393 | "trailing_slash": False,
394 | "required": True,
395 | },
396 | "gamePk": {
397 | "type": "str",
398 | "default": "",
399 | "leading_slash": False,
400 | "trailing_slash": False,
401 | "required": True,
402 | },
403 | },
404 | "query_params": ["timecode", "fields"],
405 | "required_params": [[]],
406 | },
407 | "game_uniforms": {
408 | "url": BASE_URL + "{ver}/uniforms/game",
409 | "path_params": {
410 | "ver": {
411 | "type": "str",
412 | "default": "v1",
413 | "leading_slash": False,
414 | "trailing_slash": False,
415 | "required": True,
416 | }
417 | },
418 | "query_params": [
419 | "gamePks",
420 | "fields",
421 | ],
422 | "required_params": [
423 | ["gamePks"],
424 | ],
425 | },
426 | "gamePace": {
427 | "url": BASE_URL + "{ver}/gamePace",
428 | "path_params": {
429 | "ver": {
430 | "type": "str",
431 | "default": "v1",
432 | "leading_slash": False,
433 | "trailing_slash": False,
434 | "required": True,
435 | }
436 | },
437 | "query_params": [
438 | "season",
439 | "teamIds",
440 | "leagueIds",
441 | "leagueListId",
442 | "sportId",
443 | "gameType",
444 | "startDate",
445 | "endDate",
446 | "venueIds",
447 | "orgType",
448 | "includeChildren",
449 | "fields",
450 | ],
451 | "required_params": [["season"]],
452 | },
453 | "highLow": {
454 | "url": BASE_URL + "{ver}/highLow/{orgType}",
455 | "path_params": {
456 | "ver": {
457 | "type": "str",
458 | "default": "v1",
459 | "leading_slash": False,
460 | "trailing_slash": False,
461 | "required": True,
462 | },
463 | "orgType": {
464 | "type": "str",
465 | "default": "",
466 | "leading_slash": False,
467 | "trailing_slash": False,
468 | "required": True,
469 | },
470 | },
471 | "query_params": [
472 | "statGroup",
473 | "sortStat",
474 | "season",
475 | "gameType",
476 | "teamId",
477 | "leagueId",
478 | "sportIds",
479 | "limit",
480 | "fields",
481 | ],
482 | "required_params": [["sortStat", "season"]],
483 | "note": "Valid values for orgType parameter: player, team, division, league, sport, types.",
484 | },
485 | "homeRunDerby": {
486 | "url": BASE_URL + "{ver}/homeRunDerby/{gamePk}{bracket}{pool}",
487 | "path_params": {
488 | "ver": {
489 | "type": "str",
490 | "default": "v1",
491 | "leading_slash": False,
492 | "trailing_slash": False,
493 | "required": True,
494 | },
495 | "gamePk": {
496 | "type": "str",
497 | "default": "",
498 | "leading_slash": False,
499 | "trailing_slash": False,
500 | "required": True,
501 | },
502 | "bracket": {
503 | "type": "bool",
504 | "default": False,
505 | "True": "/bracket",
506 | "False": "",
507 | "leading_slash": False,
508 | "trailing_slash": False,
509 | "required": False,
510 | },
511 | "pool": {
512 | "type": "bool",
513 | "default": False,
514 | "True": "/pool",
515 | "False": "",
516 | "leading_slash": False,
517 | "trailing_slash": False,
518 | "required": False,
519 | },
520 | },
521 | "query_params": ["fields"],
522 | "required_params": [[]],
523 | },
524 | "league": {
525 | "url": BASE_URL + "{ver}/league",
526 | "path_params": {
527 | "ver": {
528 | "type": "str",
529 | "default": "v1",
530 | "leading_slash": False,
531 | "trailing_slash": False,
532 | "required": True,
533 | }
534 | },
535 | "query_params": ["sportId", "leagueIds", "seasons", "fields"],
536 | "required_params": [["sportId"], ["leagueIds"]],
537 | },
538 | "league_allStarBallot": {
539 | "url": BASE_URL + "{ver}/league/{leagueId}/allStarBallot",
540 | "path_params": {
541 | "ver": {
542 | "type": "str",
543 | "default": "v1",
544 | "leading_slash": False,
545 | "trailing_slash": False,
546 | "required": True,
547 | },
548 | "leagueId": {
549 | "type": "str",
550 | "default": "",
551 | "leading_slash": False,
552 | "trailing_slash": False,
553 | "required": True,
554 | },
555 | },
556 | "query_params": ["season", "fields"],
557 | "required_params": [["season"]],
558 | },
559 | "league_allStarWriteIns": {
560 | "url": BASE_URL + "{ver}/league/{leagueId}/allStarWriteIns",
561 | "path_params": {
562 | "ver": {
563 | "type": "str",
564 | "default": "v1",
565 | "leading_slash": False,
566 | "trailing_slash": False,
567 | "required": True,
568 | },
569 | "leagueId": {
570 | "type": "str",
571 | "default": "",
572 | "leading_slash": False,
573 | "trailing_slash": False,
574 | "required": True,
575 | },
576 | },
577 | "query_params": ["season", "fields"],
578 | "required_params": [["season"]],
579 | },
580 | "league_allStarFinalVote": {
581 | "url": BASE_URL + "{ver}/league/{leagueId}/allStarFinalVote",
582 | "path_params": {
583 | "ver": {
584 | "type": "str",
585 | "default": "v1",
586 | "leading_slash": False,
587 | "trailing_slash": False,
588 | "required": True,
589 | },
590 | "leagueId": {
591 | "type": "str",
592 | "default": "",
593 | "leading_slash": False,
594 | "trailing_slash": False,
595 | "required": True,
596 | },
597 | },
598 | "query_params": ["season", "fields"],
599 | "required_params": [["season"]],
600 | },
601 | "people": {
602 | "url": BASE_URL + "{ver}/people",
603 | "path_params": {
604 | "ver": {
605 | "type": "str",
606 | "default": "v1",
607 | "leading_slash": False,
608 | "trailing_slash": False,
609 | "required": True,
610 | }
611 | },
612 | "query_params": ["personIds", "hydrate", "fields"],
613 | "required_params": [["personIds"]],
614 | },
615 | "people_changes": {
616 | "url": BASE_URL + "{ver}/people/changes",
617 | "path_params": {
618 | "ver": {
619 | "type": "str",
620 | "default": "v1",
621 | "leading_slash": False,
622 | "trailing_slash": False,
623 | "required": True,
624 | }
625 | },
626 | "query_params": ["updatedSince", "fields"],
627 | "required_params": [[]],
628 | },
629 | "people_freeAgents": {
630 | "url": BASE_URL + "{ver}/people/freeAgents",
631 | "path_params": {
632 | "ver": {
633 | "type": "str",
634 | "default": "v1",
635 | "leading_slash": False,
636 | "trailing_slash": False,
637 | "required": True,
638 | },
639 | "leagueId": {
640 | "type": "str",
641 | "default": "",
642 | "leading_slash": False,
643 | "trailing_slash": False,
644 | "required": True,
645 | },
646 | },
647 | "query_params": ["order", "hydrate", "fields"],
648 | "required_params": [[]],
649 | },
650 | "person": {
651 | "url": BASE_URL + "{ver}/people/{personId}",
652 | "path_params": {
653 | "ver": {
654 | "type": "str",
655 | "default": "v1",
656 | "leading_slash": False,
657 | "trailing_slash": False,
658 | "required": True,
659 | },
660 | "personId": {
661 | "type": "str",
662 | "default": None,
663 | "leading_slash": False,
664 | "trailing_slash": False,
665 | "required": True,
666 | },
667 | },
668 | "query_params": ["hydrate", "fields"],
669 | "required_params": [[]],
670 | },
671 | "person_stats": {
672 | "url": BASE_URL + "{ver}/people/{personId}/stats/game/{gamePk}",
673 | "path_params": {
674 | "ver": {
675 | "type": "str",
676 | "default": "v1",
677 | "leading_slash": False,
678 | "trailing_slash": False,
679 | "required": True,
680 | },
681 | "personId": {
682 | "type": "str",
683 | "default": None,
684 | "leading_slash": False,
685 | "trailing_slash": False,
686 | "required": True,
687 | },
688 | "gamePk": {
689 | "type": "str",
690 | "default": None,
691 | "leading_slash": False,
692 | "trailing_slash": False,
693 | "required": True,
694 | },
695 | },
696 | "query_params": ["fields"],
697 | "required_params": [[]],
698 | "note": 'Specify "current" instead of a gamePk for a player\'s current game stats.',
699 | },
700 | "jobs": {
701 | "url": BASE_URL + "{ver}/jobs",
702 | "path_params": {
703 | "ver": {
704 | "type": "str",
705 | "default": "v1",
706 | "leading_slash": False,
707 | "trailing_slash": False,
708 | "required": True,
709 | }
710 | },
711 | "query_params": ["jobType", "sportId", "date", "fields"],
712 | "required_params": [["jobType"]],
713 | },
714 | "jobs_umpires": {
715 | "url": BASE_URL + "{ver}/jobs/umpires",
716 | "path_params": {
717 | "ver": {
718 | "type": "str",
719 | "default": "v1",
720 | "leading_slash": False,
721 | "trailing_slash": False,
722 | "required": True,
723 | }
724 | },
725 | "query_params": ["sportId", "date", "fields"],
726 | "required_params": [[]],
727 | },
728 | "jobs_umpire_games": {
729 | "url": BASE_URL + "{ver}/jobs/umpires/games/{umpireId}",
730 | "path_params": {
731 | "ver": {
732 | "type": "str",
733 | "default": "v1",
734 | "leading_slash": False,
735 | "trailing_slash": False,
736 | "required": True,
737 | },
738 | "umpireId": {
739 | "type": "str",
740 | "default": None,
741 | "leading_slash": False,
742 | "trailing_slash": False,
743 | "required": True,
744 | },
745 | },
746 | "query_params": ["season", "fields"],
747 | "required_params": [["season"]],
748 | },
749 | "jobs_datacasters": {
750 | "url": BASE_URL + "{ver}/jobs/datacasters",
751 | "path_params": {
752 | "ver": {
753 | "type": "str",
754 | "default": "v1",
755 | "leading_slash": False,
756 | "trailing_slash": False,
757 | "required": True,
758 | }
759 | },
760 | "query_params": ["sportId", "date", "fields"],
761 | "required_params": [[]],
762 | },
763 | "jobs_officialScorers": {
764 | "url": BASE_URL + "{ver}/jobs/officialScorers",
765 | "path_params": {
766 | "ver": {
767 | "type": "str",
768 | "default": "v1",
769 | "leading_slash": False,
770 | "trailing_slash": False,
771 | "required": True,
772 | }
773 | },
774 | "query_params": ["timecode", "fields"],
775 | "required_params": [[]],
776 | },
777 | "schedule": {
778 | "url": BASE_URL + "{ver}/schedule",
779 | "path_params": {
780 | "ver": {
781 | "type": "str",
782 | "default": "v1",
783 | "leading_slash": False,
784 | "trailing_slash": False,
785 | "required": True,
786 | }
787 | },
788 | "query_params": [
789 | "scheduleType",
790 | "eventTypes",
791 | "hydrate",
792 | "teamId",
793 | "leagueId",
794 | "sportId",
795 | "gamePk",
796 | "gamePks",
797 | "venueIds",
798 | "gameTypes",
799 | "date",
800 | "startDate",
801 | "endDate",
802 | "opponentId",
803 | "fields",
804 | "season",
805 | ],
806 | "required_params": [["sportId"], ["gamePk"], ["gamePks"]],
807 | },
808 | "schedule_tied": {
809 | "url": BASE_URL + "{ver}/schedule/games/tied",
810 | "path_params": {
811 | "ver": {
812 | "type": "str",
813 | "default": "v1",
814 | "leading_slash": False,
815 | "trailing_slash": False,
816 | "required": True,
817 | }
818 | },
819 | "query_params": ["gameTypes", "season", "hydrate", "fields"],
820 | "required_params": [["season"]],
821 | },
822 | "schedule_postseason": {
823 | "url": BASE_URL + "{ver}/schedule/postseason",
824 | "path_params": {
825 | "ver": {
826 | "type": "str",
827 | "default": "v1",
828 | "leading_slash": False,
829 | "trailing_slash": False,
830 | "required": True,
831 | }
832 | },
833 | "query_params": [
834 | "gameTypes",
835 | "seriesNumber",
836 | "teamId",
837 | "sportId",
838 | "season",
839 | "hydrate",
840 | "fields",
841 | ],
842 | "required_params": [[]],
843 | },
844 | "schedule_postseason_series": {
845 | "url": BASE_URL + "{ver}/schedule/postseason/series",
846 | "path_params": {
847 | "ver": {
848 | "type": "str",
849 | "default": "v1",
850 | "leading_slash": False,
851 | "trailing_slash": False,
852 | "required": True,
853 | }
854 | },
855 | "query_params": [
856 | "gameTypes",
857 | "seriesNumber",
858 | "teamId",
859 | "sportId",
860 | "season",
861 | "fields",
862 | ],
863 | "required_params": [[]],
864 | },
865 | "schedule_postseason_tuneIn": {
866 | "url": BASE_URL + "{ver}/schedule/postseason/tuneIn",
867 | "path_params": {
868 | "ver": {
869 | "type": "str",
870 | "default": "v1",
871 | "leading_slash": False,
872 | "trailing_slash": False,
873 | "required": True,
874 | }
875 | },
876 | "query_params": ["teamId", "sportId", "season", "hydrate", "fields"],
877 | "required_params": [[]],
878 | "note": "The schedule_postseason_tuneIn endpoint appears to return no data.",
879 | },
880 | "seasons": {
881 | "url": BASE_URL + "{ver}/seasons{all}",
882 | "path_params": {
883 | "ver": {
884 | "type": "str",
885 | "default": "v1",
886 | "leading_slash": False,
887 | "trailing_slash": False,
888 | "required": True,
889 | },
890 | "all": {
891 | "type": "bool",
892 | "default": False,
893 | "True": "/all",
894 | "False": "",
895 | "leading_slash": False,
896 | "trailing_slash": False,
897 | "required": False,
898 | },
899 | },
900 | "query_params": ["season", "sportId", "divisionId", "leagueId", "fields"],
901 | "required_params": [["sportId"], ["divisionId"], ["leagueId"]],
902 | "note": 'Include "all" parameter with value of True to query all seasons. The divisionId and leagueId parameters are supported when "all" is used.',
903 | },
904 | "season": {
905 | "url": BASE_URL + "{ver}/seasons/{seasonId}",
906 | "path_params": {
907 | "ver": {
908 | "type": "str",
909 | "default": "v1",
910 | "leading_slash": False,
911 | "trailing_slash": False,
912 | "required": True,
913 | },
914 | "seasonId": {
915 | "type": "str",
916 | "default": False,
917 | "leading_slash": False,
918 | "trailing_slash": False,
919 | "required": True,
920 | },
921 | },
922 | "query_params": ["sportId", "fields"],
923 | "required_params": [["sportId"]],
924 | },
925 | "sports": {
926 | "url": BASE_URL + "{ver}/sports",
927 | "path_params": {
928 | "ver": {
929 | "type": "str",
930 | "default": "v1",
931 | "leading_slash": False,
932 | "trailing_slash": False,
933 | "required": True,
934 | }
935 | },
936 | "query_params": ["sportId", "fields"],
937 | "required_params": [[]],
938 | },
939 | "sports_players": {
940 | "url": BASE_URL + "{ver}/sports/{sportId}/players",
941 | "path_params": {
942 | "ver": {
943 | "type": "str",
944 | "default": "v1",
945 | "leading_slash": False,
946 | "trailing_slash": False,
947 | "required": True,
948 | },
949 | "sportId": {
950 | "type": "str",
951 | "default": "1",
952 | "leading_slash": False,
953 | "trailing_slash": False,
954 | "required": True,
955 | },
956 | },
957 | "query_params": ["season", "gameType", "fields"],
958 | "required_params": [["season"]],
959 | },
960 | "standings": {
961 | "url": BASE_URL + "{ver}/standings",
962 | "path_params": {
963 | "ver": {
964 | "type": "str",
965 | "default": "v1",
966 | "leading_slash": False,
967 | "trailing_slash": False,
968 | "required": True,
969 | }
970 | },
971 | "query_params": [
972 | "leagueId",
973 | "season",
974 | "standingsTypes",
975 | "date",
976 | "hydrate",
977 | "fields",
978 | ],
979 | "required_params": [["leagueId"]],
980 | },
981 | "stats": {
982 | "url": BASE_URL + "{ver}/stats",
983 | "path_params": {
984 | "ver": {
985 | "type": "str",
986 | "default": "v1",
987 | "leading_slash": False,
988 | "trailing_slash": False,
989 | "required": True,
990 | }
991 | },
992 | "query_params": [
993 | "stats",
994 | "playerPool",
995 | "position",
996 | "teamId",
997 | "leagueId",
998 | "limit",
999 | "offset",
1000 | "group",
1001 | "gameType",
1002 | "season",
1003 | "sportIds",
1004 | "sortStat",
1005 | "order",
1006 | "hydrate",
1007 | "fields",
1008 | "personId",
1009 | "metrics",
1010 | "startDate",
1011 | "endDate",
1012 | ],
1013 | "required_params": [["stats", "group"]],
1014 | "note": "If no limit is specified, the response will be limited to 50 records.",
1015 | },
1016 | "stats_leaders": {
1017 | "url": BASE_URL + "{ver}/stats/leaders",
1018 | "path_params": {
1019 | "ver": {
1020 | "type": "str",
1021 | "default": "v1",
1022 | "leading_slash": False,
1023 | "trailing_slash": False,
1024 | "required": True,
1025 | }
1026 | },
1027 | "query_params": [
1028 | "leaderCategories",
1029 | "playerPool",
1030 | "leaderGameTypes",
1031 | "statGroup",
1032 | "season",
1033 | "leagueId",
1034 | "sportId",
1035 | "hydrate",
1036 | "limit",
1037 | "fields",
1038 | "statType",
1039 | ],
1040 | "required_params": [["leaderCategories"]],
1041 | "note": "If excluding season parameter to get all time leaders, include statType=statsSingleSeason or you will likely not get any results.",
1042 | },
1043 | "stats_streaks": {
1044 | "url": BASE_URL + "{ver}/stats/streaks",
1045 | "path_params": {
1046 | "ver": {
1047 | "type": "str",
1048 | "default": "v1",
1049 | "leading_slash": False,
1050 | "trailing_slash": False,
1051 | "required": True,
1052 | }
1053 | },
1054 | "query_params": [
1055 | "streakType",
1056 | "streakSpan",
1057 | "gameType",
1058 | "season",
1059 | "sportId",
1060 | "limit",
1061 | "hydrate",
1062 | "fields",
1063 | ],
1064 | "required_params": [["streakType", "streakSpan", "season", "sportId", "limit"]],
1065 | "note": 'Valid streakType values: "hittingStreakOverall" "hittingStreakHome" "hittingStreakAway" "onBaseOverall" "onBaseHome" "onBaseAway". Valid streakSpan values: "career" "season" "currentStreak" "currentStreakInSeason" "notable" "notableInSeason".',
1066 | },
1067 | "teams": {
1068 | "url": BASE_URL + "{ver}/teams",
1069 | "path_params": {
1070 | "ver": {
1071 | "type": "str",
1072 | "default": "v1",
1073 | "leading_slash": False,
1074 | "trailing_slash": False,
1075 | "required": True,
1076 | }
1077 | },
1078 | "query_params": [
1079 | "season",
1080 | "activeStatus",
1081 | "leagueIds",
1082 | "sportId",
1083 | "sportIds",
1084 | "gameType",
1085 | "hydrate",
1086 | "fields",
1087 | ],
1088 | "required_params": [[]],
1089 | },
1090 | "teams_history": {
1091 | "url": BASE_URL + "{ver}/teams/history",
1092 | "path_params": {
1093 | "ver": {
1094 | "type": "str",
1095 | "default": "v1",
1096 | "leading_slash": False,
1097 | "trailing_slash": False,
1098 | "required": True,
1099 | }
1100 | },
1101 | "query_params": ["teamIds", "startSeason", "endSeason", "fields"],
1102 | "required_params": [["teamIds"]],
1103 | },
1104 | "teams_stats": {
1105 | "url": BASE_URL + "{ver}/teams/stats",
1106 | "path_params": {
1107 | "ver": {
1108 | "type": "str",
1109 | "default": "v1",
1110 | "leading_slash": False,
1111 | "trailing_slash": False,
1112 | "required": True,
1113 | }
1114 | },
1115 | "query_params": [
1116 | "season",
1117 | "sportIds",
1118 | "group",
1119 | "gameType",
1120 | "stats",
1121 | "order",
1122 | "sortStat",
1123 | "fields",
1124 | "startDate",
1125 | "endDate",
1126 | ],
1127 | "required_params": [["season", "group", "stats"]],
1128 | "note": "Use meta('statGroups') to look up valid values for group, and meta('statTypes') for valid values for stats.",
1129 | },
1130 | "teams_affiliates": {
1131 | "url": BASE_URL + "{ver}/teams/affiliates",
1132 | "path_params": {
1133 | "ver": {
1134 | "type": "str",
1135 | "default": "v1",
1136 | "leading_slash": False,
1137 | "trailing_slash": False,
1138 | "required": True,
1139 | }
1140 | },
1141 | "query_params": ["teamIds", "sportId", "season", "hydrate", "fields"],
1142 | "required_params": [["teamIds"]],
1143 | },
1144 | "team": {
1145 | "url": BASE_URL + "{ver}/teams/{teamId}",
1146 | "path_params": {
1147 | "ver": {
1148 | "type": "str",
1149 | "default": "v1",
1150 | "leading_slash": False,
1151 | "trailing_slash": False,
1152 | "required": True,
1153 | },
1154 | "teamId": {
1155 | "type": "str",
1156 | "default": None,
1157 | "leading_slash": False,
1158 | "trailing_slash": False,
1159 | "required": True,
1160 | },
1161 | },
1162 | "query_params": ["season", "sportId", "hydrate", "fields"],
1163 | "required_params": [[]],
1164 | },
1165 | "team_alumni": {
1166 | "url": BASE_URL + "{ver}/teams/{teamId}/alumni",
1167 | "path_params": {
1168 | "ver": {
1169 | "type": "str",
1170 | "default": "v1",
1171 | "leading_slash": False,
1172 | "trailing_slash": False,
1173 | "required": True,
1174 | },
1175 | "teamId": {
1176 | "type": "str",
1177 | "default": None,
1178 | "leading_slash": False,
1179 | "trailing_slash": False,
1180 | "required": True,
1181 | },
1182 | },
1183 | "query_params": ["season", "group", "hydrate", "fields"],
1184 | "required_params": [["season", "group"]],
1185 | },
1186 | "team_coaches": {
1187 | "url": BASE_URL + "{ver}/teams/{teamId}/coaches",
1188 | "path_params": {
1189 | "ver": {
1190 | "type": "str",
1191 | "default": "v1",
1192 | "leading_slash": False,
1193 | "trailing_slash": False,
1194 | "required": True,
1195 | },
1196 | "teamId": {
1197 | "type": "str",
1198 | "default": None,
1199 | "leading_slash": False,
1200 | "trailing_slash": False,
1201 | "required": True,
1202 | },
1203 | },
1204 | "query_params": ["season", "date", "fields"],
1205 | "required_params": [[]],
1206 | },
1207 | "team_personnel": {
1208 | "url": BASE_URL + "{ver}/teams/{teamId}/personnel",
1209 | "path_params": {
1210 | "ver": {
1211 | "type": "str",
1212 | "default": "v1",
1213 | "leading_slash": False,
1214 | "trailing_slash": False,
1215 | "required": True,
1216 | },
1217 | "teamId": {
1218 | "type": "str",
1219 | "default": None,
1220 | "leading_slash": False,
1221 | "trailing_slash": False,
1222 | "required": True,
1223 | },
1224 | },
1225 | "query_params": ["date", "fields"],
1226 | "required_params": [[]],
1227 | },
1228 | "team_leaders": {
1229 | "url": BASE_URL + "{ver}/teams/{teamId}/leaders",
1230 | "path_params": {
1231 | "ver": {
1232 | "type": "str",
1233 | "default": "v1",
1234 | "leading_slash": False,
1235 | "trailing_slash": False,
1236 | "required": True,
1237 | },
1238 | "teamId": {
1239 | "type": "str",
1240 | "default": None,
1241 | "leading_slash": False,
1242 | "trailing_slash": False,
1243 | "required": True,
1244 | },
1245 | },
1246 | "query_params": [
1247 | "leaderCategories",
1248 | "season",
1249 | "leaderGameTypes",
1250 | "hydrate",
1251 | "limit",
1252 | "fields",
1253 | ],
1254 | "required_params": [["leaderCategories", "season"]],
1255 | },
1256 | "team_roster": {
1257 | "url": BASE_URL + "{ver}/teams/{teamId}/roster",
1258 | "path_params": {
1259 | "ver": {
1260 | "type": "str",
1261 | "default": "v1",
1262 | "leading_slash": False,
1263 | "trailing_slash": False,
1264 | "required": True,
1265 | },
1266 | "teamId": {
1267 | "type": "str",
1268 | "default": None,
1269 | "leading_slash": False,
1270 | "trailing_slash": False,
1271 | "required": True,
1272 | },
1273 | },
1274 | "query_params": ["rosterType", "season", "date", "hydrate", "fields"],
1275 | "required_params": [[]],
1276 | },
1277 | "team_stats": {
1278 | "url": BASE_URL + "{ver}/teams/{teamId}/stats",
1279 | "path_params": {
1280 | "ver": {
1281 | "type": "str",
1282 | "default": "v1",
1283 | "leading_slash": False,
1284 | "trailing_slash": False,
1285 | "required": True,
1286 | },
1287 | "teamId": {
1288 | "type": "str",
1289 | "default": None,
1290 | "leading_slash": False,
1291 | "trailing_slash": False,
1292 | "required": True,
1293 | },
1294 | },
1295 | "query_params": [
1296 | "season",
1297 | "group",
1298 | "gameType",
1299 | "stats",
1300 | "sportIds",
1301 | "sitCodes",
1302 | "fields",
1303 | ],
1304 | "required_params": [["season", "group"]],
1305 | "note": "Use meta('statGroups') to look up valid values for group, meta('statTypes') for valid values for stats, and meta('situationCodes') for valid values for sitCodes. Use sitCodes with stats=statSplits.",
1306 | },
1307 | "team_uniforms": {
1308 | "url": BASE_URL + "{ver}/uniforms/team",
1309 | "path_params": {
1310 | "ver": {
1311 | "type": "str",
1312 | "default": "v1",
1313 | "leading_slash": False,
1314 | "trailing_slash": False,
1315 | "required": True,
1316 | }
1317 | },
1318 | "query_params": [
1319 | "teamIds",
1320 | "season",
1321 | "fields",
1322 | ],
1323 | "required_params": [
1324 | ["teamIds"],
1325 | ],
1326 | },
1327 | "transactions": {
1328 | "url": BASE_URL + "{ver}/transactions",
1329 | "path_params": {
1330 | "ver": {
1331 | "type": "str",
1332 | "default": "v1",
1333 | "leading_slash": False,
1334 | "trailing_slash": False,
1335 | "required": True,
1336 | }
1337 | },
1338 | "query_params": [
1339 | "teamId",
1340 | "playerId",
1341 | "date",
1342 | "startDate",
1343 | "endDate",
1344 | "sportId",
1345 | "fields",
1346 | ],
1347 | "required_params": [
1348 | ["teamId"],
1349 | ["playerId"],
1350 | ["date"],
1351 | ["startDate", "endDate"],
1352 | ],
1353 | },
1354 | "venue": {
1355 | "url": BASE_URL + "{ver}/venues",
1356 | "path_params": {
1357 | "ver": {
1358 | "type": "str",
1359 | "default": "v1",
1360 | "leading_slash": False,
1361 | "trailing_slash": False,
1362 | "required": True,
1363 | }
1364 | },
1365 | "query_params": ["venueIds", "season", "hydrate", "fields"],
1366 | "required_params": [["venueIds"]],
1367 | },
1368 | "meta": {
1369 | "url": BASE_URL + "{ver}/{type}",
1370 | "path_params": {
1371 | "ver": {
1372 | "type": "str",
1373 | "default": "v1",
1374 | "leading_slash": False,
1375 | "trailing_slash": False,
1376 | "required": True,
1377 | },
1378 | "type": {
1379 | "type": "str",
1380 | "default": None,
1381 | "leading_slash": False,
1382 | "trailing_slash": False,
1383 | "required": True,
1384 | },
1385 | },
1386 | "query_params": [[]],
1387 | "required_params": [[]],
1388 | "note": "The meta endpoint is used to retrieve values to be used within other API calls. Available types: awards, baseballStats, eventTypes, gameStatus, gameTypes, hitTrajectories, jobTypes, languages, leagueLeaderTypes, logicalEvents, metrics, pitchCodes, pitchTypes, platforms, positions, reviewReasons, rosterTypes, scheduleEventTypes, situationCodes, sky, standingsTypes, statGroups, statTypes, windDirection.",
1389 | },
1390 | # v1/analytics - requires authentication
1391 | # v1/game/{gamePk}/guids - statcast data - requires authentication
1392 | }
1393 |
--------------------------------------------------------------------------------
/statsapi/version.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | VERSION = "1.9.0"
4 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddrob99/MLB-StatsAPI/4658baa93d14b40dd3d83d44425cf3ee2ce73680/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_get.py:
--------------------------------------------------------------------------------
1 | import statsapi
2 | import pytest
3 | import requests.exceptions
4 | import responses
5 |
6 |
7 | def fake_dict():
8 | return {
9 | "foo": {
10 | "url": "http://www.foo.com",
11 | "path_params": {
12 | "ver": {
13 | "type": "str",
14 | "default": "v1",
15 | "leading_slash": False,
16 | "trailing_slash": False,
17 | "required": True,
18 | }
19 | },
20 | "query_params": ["bar"],
21 | "required_params": [[]],
22 | }
23 | }
24 |
25 |
26 | def test_get_returns_dictionary(mocker):
27 | # mock the ENDPOINTS dictionary
28 | mocker.patch.dict("statsapi.ENDPOINTS", fake_dict(), clear=True)
29 | # mock the requests object
30 | mock_req = mocker.patch("statsapi.requests", autospec=True)
31 | # mock the status code to always be 200
32 | mock_req.get.return_value.status_code = 200
33 |
34 | result = statsapi.get("foo", {"bar": "baz"})
35 | # assert that result is the same as the return value from calling the json method of a response object
36 | assert result == mock_req.get.return_value.json.return_value
37 |
38 |
39 | def test_get_calls_correct_url(mocker):
40 | # mock the ENDPOINTS dictionary
41 | mocker.patch.dict("statsapi.ENDPOINTS", fake_dict(), clear=True)
42 | # mock the requests object
43 | mock_req = mocker.patch("statsapi.requests", autospec=True)
44 |
45 | statsapi.get("foo", {"bar": "baz"})
46 | mock_req.get.assert_called_with("http://www.foo.com?bar=baz")
47 |
48 |
49 | @responses.activate
50 | def test_get_server_error(mocker):
51 | # mock the ENDPOINTS dictionary
52 | mocker.patch.dict("statsapi.ENDPOINTS", fake_dict(), clear=True)
53 | responses.add(responses.GET, "http://www.foo.com?bar=baz", status=500)
54 |
55 | with pytest.raises(requests.exceptions.HTTPError):
56 | statsapi.get("foo", {"bar": "baz"})
57 |
58 |
59 | def test_get_invalid_endpoint(mocker):
60 | # mock the ENDPOINTS dictionary
61 | mocker.patch.dict("statsapi.ENDPOINTS", fake_dict(), clear=True)
62 | # mock the requests object
63 | mock_req = mocker.patch("statsapi.requests", autospec=True)
64 | # invalid endpoint
65 | with pytest.raises(ValueError):
66 | statsapi.get("bar", {"foo": "baz"})
67 |
68 | # TODO: add test for path requirement not met
69 | # TODO: add test for required params
70 |
--------------------------------------------------------------------------------