Source code for i3pystatus.scores.nba

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

import copy
import pytz
import re
import time
from datetime import datetime, timezone

LIVE_URL = 'https://www.nba.com/game/{id}'
API_URL = 'https://cdn.nba.com/static/json/liveData/scoreboard/todaysScoreboard_00.json'


[docs]class NBA(ScoresBackend): ''' Backend to retrieve NBA scores. For usage examples, see :py:mod:`here <.scores>`. .. rubric:: Available formatters * `{home_team}` — Depending on the value of the ``team_format`` option, will contain either the home team's name, abbreviation, or 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_seed}` — During the playoffs, shows the home team's playoff seed. When not in the playoffs, this formatter will be blank. * `{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_team}` — Depending on the value of the ``team_format`` option, will contain either the away team's name, abbreviation, or 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_seed}` — During the playoffs, shows the away team's playoff seed. When not in the playoffs, this formatter will be blank. * `{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. * `{time_remaining}` — Time remaining in the current quarter/OT period * `{quarter}` — Number of the current quarter * `{start_time}` — Start time of game in system's localtime (supports strftime formatting, e.g. `{start_time:%I:%M %p}`) * `{overtime}` — If the game ended in overtime, this formatter will show ``OT``. If the game ended in regulation, or has not yet completed, this formatter will be blank. .. rubric:: Team abbreviations * **ATL** — Atlanta Hawks * **BKN** — Brooklyn Nets * **BOS** — Boston Celtics * **CHA** — Charlotte Hornets * **CHI** — Chicago Bulls * **CLE** — Cleveland Cavaliers * **DAL** — Dallas Mavericks * **DEN** — Denver Nuggets * **DET** — Detroit Pistons * **GSW** — Golden State Warriors * **HOU** — Houston Rockets * **IND** — Indiana Pacers * **MIA** — Miami Heat * **MEM** — Memphis Grizzlies * **MIL** — Milwaukee Bucks * **LAC** — Los Angeles Clippers * **LAL** — Los Angeles Lakers * **MIN** — Minnesota Timberwolves * **NOP** — New Orleans Pelicans * **NYK** — New York Knicks * **OKC** — Oklahoma City Thunder * **ORL** — Orlando Magic * **PHI** — Philadelphia 76ers * **PHX** — Phoenix Suns * **POR** — Portland Trailblazers * **SAC** — Sacramento Kings * **SAS** — San Antonio Spurs * **TOR** — Toronto Raptors * **UTA** — Utah Jazz * **WAS** — Washington Wizards ''' 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', 'Format used to display game information'), ('status_pregame', 'Format string used for the ``{game_status}`` ' 'formatter when the game has not started '), ('status_in_progress', 'Format string used for the ``{game_status}`` ' 'formatter when the game is in progress'), ('status_final', 'Format string used for the ``{game_status}`` ' 'formatter when the game has finished'), ('status_postponed', 'Format string used for the ``{game_status}`` ' 'formatter when the game has been postponed'), ('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.'), ('team_format', 'One of ``name``, ``abbreviation``, or ``city``. If ' 'not specified, takes the value from the ``scores`` ' 'module.'), ('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', 'URL string to launch NBA Game Tracker. 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 = { 'ATL': '#E2383F', 'BKN': '#DADADA', 'BOS': '#178D58', 'CHA': '#00798D', 'CHI': '#CD1041', 'CLE': '#FDBA31', 'DAL': '#006BB7', 'DEN': '#5593C3', 'DET': '#207EC0', 'GSW': '#DEB934', 'HOU': '#CD1042', 'IND': '#FFBB33', 'MIA': '#A72249', 'MEM': '#628BBC', 'MIL': '#4C7B4B', 'LAC': '#ED174C', 'LAL': '#FDB827', 'MIN': '#35749F', 'NOP': '#A78F59', 'NYK': '#F68428', 'OKC': '#F05033', 'ORL': '#1980CB', 'PHI': '#006BB7', 'PHX': '#E76120', 'POR': '#B03037', 'SAC': '#7A58A1', 'SAS': '#DADADA', 'TOR': '#CD112C', 'UTA': '#4B7059', 'WAS': '#E51735', } _valid_teams = [x for x in _default_colors] _valid_display_order = ['in_progress', 'final', 'pregame', 'postponed'] display_order = _valid_display_order format_no_games = 'NBA: No games' format = '[{scroll} ]NBA: [{away_favorite} ][{away_seed} ]{away_team} [{away_score} ]({away_wins}-{away_losses}) at [{home_favorite} ][{home_seed} ]{home_team} [{home_score} ]({home_wins}-{home_losses}) {game_status}' status_pregame = '{start_time:%H:%M %Z}' status_in_progress = '({time_remaining} {quarter})' status_final = '(Final[/{overtime}])' status_postponed = 'PPD' team_colors = _default_colors live_url = LIVE_URL api_url = API_URL # These will inherit from the Scores class if not overridden team_format = None def check_scores(self): self.get_api_date() response = self.api_request(self.api_url) game_list = self.get_nested(response, 'scoreboard:games', default=[]) # Convert list of games to dictionary for easy reference later on data = {} team_game_map = {} for game in game_list: try: id_ = game['gameId'] except KeyError: continue try: for key in ('homeTeam', 'awayTeam'): team = game[key]['teamTricode'] 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(f'Processing {self.name} game data: {game}') _update('id', 'gameId') ret['live_url'] = self.live_url.format(id=ret['id']) if game.get('gameStatusText', '') == 'PPD': ret['status'] = 'postponed' else: status_map = { 1: 'pregame', 2: 'in_progress', 3: 'final', } status_code = int(game.get('gameStatus', 1)) status = status_map.get(status_code) if status is None: self.logger.debug( f"Unknown {self.name} game status code '{status_code}'" ) status_code = '1' ret['status'] = status_map[status_code] if ret['status'] in ('in_progress', 'final'): period_number = int(game.get('period', 1)) total_periods = int(game.get('regulationPeriods', 4)) period_diff = period_number - total_periods ret['quarter'] = 'OT' \ if period_diff == 1 \ else f'{period_diff}OT' if period_diff > 1 \ else self.add_ordinal(period_number) else: ret['quarter'] = '' clock = game.get('gameClock', '') ret['time_remaining'] = '' if clock: try: mins, secs = re.match(r'^PT(\d+)M(\d+\.\d)0?S$', clock).groups() except AttributeError: self.logger.warning(f'Failed to parse gameClock value: {clock}') else: mins = mins.lstrip('0') if mins: secs = secs.split('.')[0] if not mins and secs == '00.0': ret['time_remaining'] = 'End' else: ret['time_remaining'] = f'{mins}:{secs}' ret['overtime'] = ret['quarter'] if 'OT' in ret['quarter'] else '' for key in ('home', 'away'): team_key = f'{key}Team' _update(f'{key}_score', f'{team_key}:score', callback=self.zero_fallback, default='0') _update(f'{key}_city', f'{team_key}:teamCity') _update(f'{key}_name', f'{team_key}:teamName') _update(f'{key}_abbreviation', f'{team_key}:teamTricode') if 'playoffs' in game: _update(f'{key}_wins', f'playoffs:{key}_wins', callback=self.zero_fallback, default='0') _update(f'{key}_seed', f'playoffs:{key}_seed', callback=self.zero_fallback, default='0') else: _update(f'{key}_wins', f'{team_key}:wins', callback=self.zero_fallback, default='0') _update(f'{key}_losses', f'{team_key}:losses', callback=self.zero_fallback, default='0') ret[f'{key}_seed'] = '' if 'playoffs' in game: ret['home_losses'] = ret['away_wins'] ret['away_losses'] = ret['home_wins'] # From API data, date is YYYYMMDD, time is HHMM try: game_time = datetime.strptime( game.get('gameTimeUTC', ''), '%Y-%m-%dT%H:%M:%SZ' ).replace(tzinfo=timezone.utc) 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.exception( f'Error encountered determining game time for {self.name} ' f'game {game["id"]} (time string: {game_et})' ) game_time = datetime.datetime(1970, 1, 1) ret['start_time'] = game_time.astimezone() self.logger.debug(f'Returned {self.name} formatter data: {ret}') return ret