Add NHL backend for scores module

This commit is contained in:
Erik Johnson 2016-04-06 01:16:00 -05:00
parent afdcf32388
commit ddde786763

302
i3pystatus/scores/nhl.py Normal file
View File

@ -0,0 +1,302 @@
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/%s'
SCOREBOARD_URL = 'https://www.nhl.com/scores'
API_URL = 'https://statsapi.web.nhl.com/api/v1/schedule?startDate=%04d-%02d-%02d&endDate=%04d-%02d-%02d&expand=schedule.teams,schedule.linescore,schedule.broadcasts.all&site=en_nhl&teamId='
class NHL(ScoresBackend):
'''
Backend to retrieve NHL 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}` 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_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_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_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:: 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
* **SJS** San Jose Sharks
* **STL** St. Louis Blues
* **TBL** Tampa Bay Lightning
* **TOR** Toronto Maple Leafs
* **VAN** Vancouver Canucks
* **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_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'),
('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.'),
('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': '#D9CBAE',
'SJS': '#007888',
'STL': '#1764AD',
'TBL': '#296AD5',
'TOR': '#296AD5',
'VAN': '#0454FA',
'WPG': '#1568C5',
'WSH': '#E51937',
}
_valid_teams = [x for x in _default_colors]
_valid_display_order = ['in_progress', 'final', 'pregame']
display_order = _valid_display_order
format_no_games = 'NHL: No games'
format_pregame = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}-{home_otl}) {start_time:%H:%M %Z}'
format_in_progress = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score}[ ({away_power_play})][ ({away_empty_net})], [{home_favorite} ]{home_abbrev} {home_score}[ ({home_power_play})][ ({home_empty_net})] ({time_remaining} {period})'
format_final = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}-{home_otl}) (Final[/{overtime}])'
empty_net = 'EN'
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,
self.date.year, self.date.month, self.date.day)
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 = {}
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)
_update('id', 'gamePk')
ret['live_url'] = self.live_url % ret['id']
_update('period', 'linescore:currentPeriodOrdinal', default='')
_update('time_remaining',
'linescore:currentPeriodTimeRemaining',
lambda x: x.capitalize(),
default='')
_update('venue', 'venue:name')
pp_strength = self.get_nested(game,
'linescore:powerPlayStrength',
default='')
for team in ('home', 'away'):
_update('%s_score' % team,
'teams:%s:score' % team,
callback=self.force_int,
default=0)
_update('%s_wins' % team,
'teams:%s:leagueRecord:wins' % team,
callback=self.force_int,
default=0)
_update('%s_losses' % team,
'teams:%s:leagueRecord:losses' % team,
callback=self.force_int,
default=0)
_update('%s_otl' % team,
'teams:%s:leagueRecord:ot' % team,
callback=self.force_int,
default=0)
_update('%s_city' % team, 'teams:%s:team:shortName' % team)
_update('%s_name' % team, 'teams:%s:team:teamName' % team)
_update('%s_abbrev' % team, 'teams:%s:team:abbreviation' % team)
_update('%s_power_play' % team,
'linescore:teams:%s:powerPlay' % team,
lambda x: pp_strength if x and pp_strength != 'Even' else '')
_update('%s_empty_net' % team,
'linescore:teams:%s:goaliePulled' % team,
lambda x: self.empty_net if x else '')
_update('status',
'status:abstractGameState',
lambda x: x.lower().replace(' ', '_'))
if ret['status'] == 'live':
ret['status'] = 'in_progress'
elif ret['status'] == 'final':
_update('overtime',
'linescore:currentPeriodOrdinal',
lambda x: x if x in ('OT', '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.error(
'Error encountered determining %s game time for game %s:',
self.__class__.__name__,
game['id'],
exc_info=True
)
game_time = datetime.datetime(1970, 1, 1)
ret['start_time'] = pytz.utc.localize(game_time).astimezone()
self.logger.debug('Returned %s formatter data: %s',
self.__class__.__name__, ret)
return ret