Source code for i3pystatus.scores.nhl

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 = 'https://www.nhl.com/gamecenter/{id}'
SCOREBOARD_URL = 'https://www.nhl.com/scores'
API_URL = 'https://statsapi.web.nhl.com/api/v1/schedule?startDate={date:%Y-%m-%d}&endDate={date:%Y-%m-%d}&expand=schedule.teams,schedule.linescore,schedule.broadcasts.all&site=en_nhl&teamId='


[docs]class NHL(ScoresBackend): ''' Backend to retrieve NHL 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_otl}` — Home team's number of overtime 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. * `{home_empty_net}` — Shows the value from the ``empty_net`` parameter when the home team's net is empty. * `{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_otl}` — Away team's number of overtime 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. * `{away_empty_net}` — Shows the value from the ``empty_net`` parameter when the away team's net is empty. * `{period}` — Current period * `{venue}` — Name of arena 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}`) * `{overtime}` — If the game ended in overtime or a shootout, this formatter will show ``OT`` kor ``SO``. If the game ended in regulation, or has not yet completed, this formatter will be blank. .. rubric:: Playoffs In the playoffs, losses are not important (as the losses will be equal to the other team's wins). Therefore, it is a good idea during the playoffs to manually set format strings to exclude information on team losses. For example: .. code-block:: python from i3pystatus import Status from i3pystatus.scores import nhl status = Status() status.register( 'scores', hints={'markup': 'pango'}, colorize_teams=True, favorite_icon='<span size="small" color="#F5FF00">★</span>', backends=[ nhl.NHL( favorite_teams=['CHI'], format='[{scroll} ]NHL: [{away_favorite} ]{away_team} ({away_wins}) at [{home_favorite} ]{home_team} ({home_wins}) {game_status}' ), ], ) .. rubric:: Team abbreviations * **ANA** — Anaheim Ducks * **ARI** — Arizona Coyotes * **BOS** — Boston Bruins * **BUF** — Buffalo Sabres * **CAR** — Carolina Hurricanes * **CBJ** — Columbus Blue Jackets * **CGY** — Calgary Flames * **CHI** — Chicago Blackhawks * **COL** — Colorado Avalanche * **DAL** — Dallas Stars * **DET** — Detroit Red Wings * **EDM** — Edmonton Oilers * **FLA** — Florida Panthers * **LAK** — Los Angeles Kings * **MIN** — Minnesota Wild * **MTL** — Montreal Canadiens * **NJD** — New Jersey Devils * **NSH** — Nashville Predators * **NYI** — New York Islanders * **NYR** — New York Rangers * **OTT** — Ottawa Senators * **PHI** — Philadelphia Flyers * **PIT** — Pittsburgh Penguins * **SEA** — Seattle Kraken * **SJS** — San Jose Sharks * **STL** — St. Louis Blues * **TBL** — Tampa Bay Lightning * **TOR** — Toronto Maple Leafs * **VAN** — Vancouver Canucks * **VGK** — Vegas Golden Knights * **WPG** — Winnipeg Jets * **WSH** — Washington Capitals ''' 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'), ('empty_net', 'Value for the ``{away_empty_net}`` or ' '``{home_empty_net}`` formatter when the net is empty. ' 'When the net is not empty, these formatters will be ' 'empty strings.'), ('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 NHL GameCenter. This value should ' 'not need to be changed.'), ('scoreboard_url', 'Link to the NHL.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 = { 'ANA': '#B4A277', 'ARI': '#AC313A', 'BOS': '#F6BD27', 'BUF': '#1568C5', 'CAR': '#FA272E', 'CBJ': '#1568C5', 'CGY': '#D23429', 'CHI': '#CD0E24', 'COL': '#9F415B', 'DAL': '#058158', 'DET': '#E51937', 'EDM': '#2F6093', 'FLA': '#E51837', 'LAK': '#DADADA', 'MIN': '#176B49', 'MTL': '#C8011D', 'NJD': '#CC0000', 'NSH': '#FDB71A', 'NYI': '#F8630D', 'NYR': '#1576CA', 'OTT': '#C50B2F', 'PHI': '#FF690B', 'PIT': '#FFB81C', 'SEA': '#96D8D8', 'SJS': '#007888', 'STL': '#1764AD', 'TBL': '#296AD5', 'TOR': '#296AD5', 'VAN': '#0454FA', 'VGK': '#B4975A', 'WPG': '#1568C5', 'WSH': '#E51937', } _valid_teams = [x for x in _default_colors] _valid_display_order = ['in_progress', 'final', 'pregame', 'postponed'] display_order = _valid_display_order format_no_games = 'NHL: No games' format = '[{scroll} ]NHL: [{away_favorite} ]{away_team} [{away_score} ]({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_team} [{home_score} ]({home_wins}-{home_losses}-{home_otl}) {game_status}' status_pregame = '{start_time:%H:%M %Z}' status_in_progress = '({time_remaining} {period})' status_final = '(Final[/{overtime}])' status_postponed = 'PPD' empty_net = 'EN' team_colors = _default_colors live_url = LIVE_URL scoreboard_url = SCOREBOARD_URL api_url = API_URL # These will inherit from the Scores class if not overridden team_format = None @require(internet) def check_scores(self): self.get_api_date() url = self.api_url.format(date=self.date) game_list = self.get_nested(self.api_request(url), 'dates:0: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['gamePk'] except KeyError: continue try: for key in ('home', 'away'): team = game['teams'][key]['team']['abbreviation'].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 = {} self.logger.debug(f'Processing {self.name} game data: {game}') linescore = self.get_nested(game, 'linescore', default={}) ret['id'] = game['gamePk'] ret['live_url'] = self.live_url.format(id=ret['id']) ret['period'] = self.get_nested( linescore, 'currentPeriodOrdinal') ret['time_remaining'] = self.get_nested( linescore, 'currentPeriodTimeRemaining', callback=lambda x: x.capitalize()) ret['venue'] = self.get_nested( game, 'venue:name') pp_strength = self.get_nested(linescore, 'powerPlayStrength') for team in ('away', 'home'): team_data = self.get_nested(game, f'teams:{team}', default={}) if team == 'home': ret['venue'] = self.get_nested(team_data, 'venue:name') ret[f'{team}_score'] = self.get_nested( team_data, 'score', callback=self.zero_fallback, default=0) ret[f'{team}_wins'] = self.get_nested( team_data, 'leagueRecord:wins', callback=self.zero_fallback, default=0) ret[f'{team}_losses'] = self.get_nested( team_data, 'leagueRecord:losses', callback=self.zero_fallback, default=0) ret[f'{team}_otl'] = self.get_nested( team_data, 'leagueRecord:ot', callback=self.zero_fallback, default=0) ret[f'{team}_city'] = self.get_nested( team_data, 'team:shortName') ret[f'{team}_name'] = self.get_nested( team_data, 'team:teamName') ret[f'{team}_abbreviation'] = self.get_nested( team_data, 'team:abbreviation') ret[f'{team}_power_play'] = self.get_nested( linescore, f'teams:{team}:powerPlay', callback=lambda x: pp_strength if x and pp_strength != 'Even' else '') ret[f'{team}_empty_net'] = self.get_nested( linescore, f'teams:{team}:goaliePulled', callback=lambda x: self.empty_net if x else '') if game.get('gameType') == 'P': # Calculate wins/losses in current playoff series home_rem = ret['home_wins'] % 4 away_rem = ret['away_wins'] % 4 if ret['home_wins'] == ret['away_wins']: if home_rem == 0: # Both teams have multiples of 4 wins, so series has no # completed games. ret['home_wins'] = ret['away_wins'] = 0 else: ret['home_wins'] = home_rem ret['away_wins'] = away_rem elif ret['home_wins'] > ret['away_wins']: ret['home_wins'] = 4 if home_rem == 0 else home_rem ret['away_wins'] = away_rem else: ret['away_wins'] = 4 if away_rem == 0 else away_rem ret['home_wins'] = home_rem # Series losses are the other team's wins ret['home_losses'] = ret['away_wins'] ret['away_losses'] = ret['home_wins'] if self.get_nested(game, 'status:detailedState').lower() == 'postponed': ret['status'] = 'postponed' else: ret['status'] = self.get_nested( game, 'status:abstractGameState', callback=lambda x: x.lower().replace(' ', '_')) if ret['status'] == 'live': ret['status'] = 'in_progress' elif ret['status'] == 'final': ret['overtime'] = self.get_nested( linescore, 'currentPeriodOrdinal', callback=lambda x: x if 'OT' in x or x == 'SO' else '') elif ret['status'] != 'in_progress': ret['status'] = 'pregame' # Game time is in UTC, ISO format, thank the FSM # Ex. 2016-04-02T17:00:00Z game_time_str = game.get('gameDate', '') try: game_time = datetime.strptime(game_time_str, '%Y-%m-%dT%H:%M:%SZ') 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 {self.name} game time for ' f'game {game["id"]}' ) game_time = datetime.datetime(1970, 1, 1) ret['start_time'] = pytz.utc.localize(game_time).astimezone() self.logger.debug(f'Returned {self.name} formatter data: {ret}') return ret