Source code for i3pystatus.scores.mlb

from i3pystatus.core.util import internet, require
from i3pystatus.scores import ScoresBackend

import copy
import json
import pytz
import re
import time
from datetime import datetime
from urllib.request import urlopen

LIVE_URL = 'http://mlb.mlb.com/mlb/gameday/index.jsp?gid=%s'
SCOREBOARD_URL = 'http://m.mlb.com/scoreboard'
API_URL = 'http://gd2.mlb.com/components/game/mlb/year_%04d/month_%02d/day_%02d/miniscoreboard.json'


[docs]class MLB(ScoresBackend): ''' Backend to retrieve MLB scores. For usage examples, see :py:mod:`here <.scores>`. .. rubric:: Available formatters * `{home_name}` — Name of home team * `{home_city}` — Name of home team's city * `{home_abbrev}` — 2 or 3-letter abbreviation for home team's city * `{home_score}` — Home team's current score * `{home_wins}` — Home team's number of wins * `{home_losses}` — Home team's number of losses * `{home_favorite}` — Displays the value for the :py:mod:`.scores` module's ``favorite`` attribute, if the home team is one of the teams being followed. Otherwise, this formatter will be blank. * `{away_name}` — Name of away team * `{away_city}` — Name of away team's city * `{away_abbrev}` — 2 or 3-letter abbreviation for away team's city * `{away_score}` — Away team's current score * `{away_wins}` — Away team's number of wins * `{away_losses}` — Away team's number of losses * `{away_favorite}` — Displays the value for the :py:mod:`.scores` module's ``favorite`` attribute, if the away team is one of the teams being followed. Otherwise, this formatter will be blank. * `{top_bottom}` — Displays the value of either ``inning_top`` or ``inning_bottom`` based on whether the game is in the top or bottom of an inning. * `{inning}` — Current inning * `{outs}` — Number of outs in current inning * `{venue}` — Name of ballpark where game is being played * `{start_time}` — Start time of game in system's localtime (supports strftime formatting, e.g. `{start_time:%I:%M %p}`) * `{delay}` — Reason for delay, if game is currently delayed. Otherwise, this formatter will be blank. * `{postponed}` — Reason for postponement, if game has been postponed. Otherwise, this formatter will be blank. * `{extra_innings}` — When a game lasts longer than 9 innings, this formatter will show that number of innings. Otherwise, it will blank. .. rubric:: Team abbreviations * **ARI** — Arizona Diamondbacks * **ATL** — Atlanta Braves * **BAL** — Baltimore Orioles * **BOS** — Boston Red Sox * **CHC** — Chicago Cubs * **CIN** — Cincinnati Reds * **CLE** — Cleveland Indians * **COL** — Colorado Rockies * **CWS** — Chicago White Sox * **DET** — Detroit Tigers * **HOU** — Houston Astros * **KC** — Kansas City Royals * **LAA** — Los Angeles Angels of Anaheim * **LAD** — Los Angeles Dodgers * **MIA** — Miami Marlins * **MIL** — Milwaukee Brewers * **MIN** — Minnesota Twins * **NYY** — New York Yankees * **NYM** — New York Mets * **OAK** — Oakland Athletics * **PHI** — Philadelphia Phillies * **PIT** — Pittsburgh Pirates * **SD** — San Diego Padres * **SEA** — Seattle Mariners * **SF** — San Francisco Giants * **STL** — St. Louis Cardinals * **TB** — Tampa Bay Rays * **TEX** — Texas Rangers * **TOR** — Toronto Blue Jays * **WSH** — Washington Nationals ''' interval = 300 settings = ( ('favorite_teams', 'List of abbreviations of favorite teams. Games ' 'for these teams will appear first in the scroll ' 'list. A detailed description of how games are ' 'ordered can be found ' ':ref:`here <scores-game-order>`.'), ('all_games', 'If set to ``True``, all games will be present in ' 'the scroll list. If set to ``False``, then only ' 'games from **favorite_teams** will be present in ' 'the scroll list.'), ('display_order', 'When **all_games** is set to ``True``, this ' 'option will dictate the order in which games from ' 'teams not in **favorite_teams** are displayed'), ('format_no_games', 'Format used when no tracked games are scheduled ' 'for the current day (does not support formatter ' 'placeholders)'), ('format_pregame', 'Format used when the game has not yet started'), ('format_in_progress', 'Format used when the game is in progress'), ('format_final', 'Format used when the game is complete'), ('format_postponed', 'Format used when the game has been postponed'), ('format_suspended', 'Format used when the game has been suspended'), ('inning_top', 'Value for the ``{top_bottom}`` formatter when game ' 'is in the top half of an inning'), ('inning_bottom', 'Value for the ``{top_bottom}`` formatter when game ' 'is in the bottom half of an inning'), ('team_colors', 'Dictionary mapping team abbreviations to hex color ' 'codes. If overridden, the passed values will be ' 'merged with the defaults, so it is not necessary to ' 'define all teams if specifying this value.'), ('date', 'Date for which to display game scores, in **YYYY-MM-DD** ' 'format. If unspecified, the current day\'s games will be ' 'displayed starting at 10am Eastern time, with last ' 'evening\'s scores being shown before then. This option ' 'exists primarily for troubleshooting purposes.'), ('live_url', 'Alternate URL string to launch MLB Gameday. This value ' 'should not need to be changed'), ('scoreboard_url', 'Link to the MLB.com scoreboard page. Like ' '**live_url**, this value should not need to be ' 'changed.'), ('api_url', 'Alternate URL string from which to retrieve score data. ' 'Like **live_url*** this value should not need to be ' 'changed.'), ) required = () _default_colors = { 'ARI': '#A71930', 'ATL': '#CE1141', 'BAL': '#DF4601', 'BOS': '#BD3039', 'CHC': '#004EC1', 'CIN': '#C6011F', 'CLE': '#E31937', 'COL': '#5E5EB6', 'CWS': '#DADADA', 'DET': '#FF6600', 'HOU': '#EB6E1F', 'KC': '#0046DD', 'LAA': '#BA0021', 'LAD': '#005A9C', 'MIA': '#F14634', 'MIL': '#0747CC', 'MIN': '#D31145', 'NYY': '#0747CC', 'NYM': '#FF5910', 'OAK': '#006659', 'PHI': '#E81828', 'PIT': '#FFCC01', 'SD': '#285F9A', 'SEA': '#2E8B90', 'SF': '#FD5A1E', 'STL': '#B53B30', 'TB': '#8FBCE6', 'TEX': '#C0111F', 'TOR': '#0046DD', 'WSH': '#C70003', } _valid_teams = [x for x in _default_colors] _valid_display_order = ['in_progress', 'suspended', 'final', 'postponed', 'pregame'] display_order = _valid_display_order format_no_games = 'MLB: No games' format_pregame = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}) {start_time:%H:%M %Z}[ ({delay} Delay)]' format_in_progress = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score}, [{home_favorite} ]{home_abbrev} {home_score} ({top_bottom} {inning}, {outs} Out)[ ({delay} Delay)]' format_final = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Final[/{extra_innings}])' format_postponed = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}) (PPD: {postponed})' format_suspended = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Suspended: {suspended})' inning_top = 'Top' inning_bottom = 'Bot' team_colors = _default_colors live_url = LIVE_URL scoreboard_url = SCOREBOARD_URL api_url = API_URL @require(internet) def check_scores(self): self.get_api_date() url = self.api_url % (self.date.year, self.date.month, self.date.day) game_list = self.get_nested(self.api_request(url), 'data:games:game', default=[]) if not isinstance(game_list, list): # When only one game is taking place during a given day, the game # data is just a single dict containing that game's data, rather # than a list of dicts. Encapsulate the single game dict in a list # to make it process correctly in the loop below. game_list = [game_list] # Convert list of games to dictionary for easy reference later on data = {} team_game_map = {} for game in game_list: try: id_ = game['id'] except (KeyError, TypeError): continue try: for team in (game['home_name_abbrev'], game['away_name_abbrev']): team = team.upper() if team in self.favorite_teams: team_game_map.setdefault(team, []).append(id_) except KeyError: continue data[id_] = game self.interpret_api_return(data, team_game_map) def process_game(self, game): ret = {} def _update(ret_key, game_key=None, callback=None, default='?'): ret[ret_key] = self.get_nested(game, game_key or ret_key, callback=callback, default=default) self.logger.debug('Processing %s game data: %s', self.__class__.__name__, game) for key in ('id', 'venue'): _update(key) for key in ('inning', 'outs'): _update(key, callback=self.force_int, default=0) ret['live_url'] = self.live_url % game['gameday_link'] for team in ('home', 'away'): _update('%s_wins' % team, '%s_win' % team, callback=self.force_int) _update('%s_losses' % team, '%s_loss' % team, callback=self.force_int) _update('%s_score' % team, '%s_team_runs' % team, callback=self.force_int, default=0) _update('%s_abbrev' % team, '%s_name_abbrev' % team) for item in ('city', 'name'): _update('%s_%s' % (team, item), '%s_team_%s' % (team, item)) try: ret['status'] = game.get('status').lower().replace(' ', '_') except AttributeError: # During warmup ret['status'] may be a dictionary. Treat these as # pregame ret['status'] = 'pregame' for key in ('delay', 'postponed', 'suspended'): ret[key] = '' if ret['status'] == 'delayed_start': ret['status'] = 'pregame' ret['delay'] = game.get('reason', 'Unknown') elif ret['status'] == 'delayed': ret['status'] = 'in_progress' ret['delay'] = game.get('reason', 'Unknown') elif ret['status'] == 'postponed': ret['postponed'] = game.get('reason', 'Unknown Reason') elif ret['status'] == 'suspended': ret['suspended'] = game.get('reason', 'Unknown Reason') elif ret['status'] in ('game_over', 'completed_early'): ret['status'] = 'final' elif ret['status'] not in ('in_progress', 'final'): ret['status'] = 'pregame' try: inning = game.get('inning', '0') ret['extra_innings'] = inning \ if ret['status'] == 'final' and int(inning) != 9 \ else '' except ValueError: ret['extra_innings'] = '' top_bottom = game.get('top_inning') ret['top_bottom'] = self.inning_top if top_bottom == 'Y' \ else self.inning_bottom if top_bottom == 'N' \ else '' time_zones = { 'PT': 'US/Pacific', 'MT': 'US/Mountain', 'CT': 'US/Central', 'ET': 'US/Eastern', } game_tz = pytz.timezone( time_zones.get( game.get('time_zone', 'ET'), 'US/Eastern' ) ) date_and_time = [] if 'resume_time_date' in game and game['resume_time_date']: date_and_time.append(game['resume_time_date']) elif 'time_date' in game and game['time_date']: date_and_time.append(game['time_date']) else: keys = ('original_date', 'time') if all(key in game for key in keys): for key in keys: if game[key]: date_and_time.append(game[key]) if 'resume_ampm' in game and game['resume_ampm']: date_and_time.append(game['resume_ampm']) elif 'ampm' in game and game['ampm']: date_and_time.append(game['ampm']) game_time_str = ' '.join(date_and_time) try: game_time = datetime.strptime(game_time_str, '%Y/%m/%d %I:%M %p') except ValueError as exc: # Log when the date retrieved from the API return doesn't match the # expected format (to help troubleshoot API changes), and set an # actual datetime so format strings work as expected. The times # will all be wrong, but the logging here will help us make the # necessary changes to adapt to any API changes. self.logger.error( 'Error encountered determining %s game time for game %s:', self.__class__.__name__, game['id'], exc_info=True ) game_time = datetime(1970, 1, 1) ret['start_time'] = game_tz.localize(game_time).astimezone() self.logger.debug('Returned %s formatter data: %s', self.__class__.__name__, ret) return ret