diff --git a/i3pystatus/scores/__init__.py b/i3pystatus/scores/__init__.py index 1af0c61..e544cb2 100644 --- a/i3pystatus/scores/__init__.py +++ b/i3pystatus/scores/__init__.py @@ -29,57 +29,55 @@ class ScoresBackend(SettingsBase): # in team_colors. if len(self.team_colors) != len(self._default_colors): self.logger.debug( - 'Overriding %s team colors with: %s', - self.__class__.__name__, - self.team_colors + f'Overriding {self.name} team colors ' + f'with: {self.team_colors}' ) new_colors = copy.copy(self._default_colors) new_colors.update(self.team_colors) self.team_colors = new_colors - self.logger.debug('%s team colors: %s', - self.__class__.__name__, self.team_colors) + self.logger.debug(f'{self.name} team colors: {self.team_colors}') def api_request(self, url): - self.logger.debug('Making %s API request to %s', - self.__class__.__name__, url) + self.logger.debug(f'Making {self.name} API request to {url}') try: with urlopen(url) as content: try: if content.url != url: - self.logger.debug('Request to %s was redirected to %s', - url, content.url) + self.logger.debug( + f'Request to {url} was redirected to {content.url}' + ) content_type = dict(content.getheaders())['Content-Type'] - mime_type = content_type.split(';')[0].lower() - if 'json' not in mime_type: - self.logger.debug('Response from %s is not JSON', - content.url) - return {} charset = re.search(r'charset=(.*)', content_type).group(1) except AttributeError: charset = 'utf-8' response_json = content.read().decode(charset).strip() if not response_json: - self.logger.debug('JSON response from %s was blank', url) + self.logger.debug(f'JSON response from {url} was blank') return {} try: response = json.loads(response_json) except json.decoder.JSONDecodeError as exc: - self.logger.error('Error loading JSON: %s', exc) - self.logger.debug('JSON text that failed to load: %s', - response_json) + self.logger.exception(f'Error encountered while loading JSON') + self.logger.debug(f'Text that failed to load: {response_json}') return {} - self.logger.log(5, 'API response: %s', response) + self.logger.log(5, f'API response: {response}') return response except HTTPError as exc: self.logger.critical( - 'Error %s (%s) making request to %s', - exc.code, exc.reason, exc.url, + f'Error {exc.code} ({exc.reason}) making request to {exc.url}' ) return {} except (ConnectionResetError, URLError) as exc: - self.logger.critical('Error making request to %s: %s', url, exc) + self.logger.critical(f'Error making request to {url}: {exc}') return {} + @property + def name(self): + ''' + Return the backend name + ''' + return self.__class__.__name__ + def get_api_date(self): ''' Figure out the date to use for API requests. Assumes yesterday's date @@ -94,7 +92,7 @@ class ScoresBackend(SettingsBase): try: api_date = datetime.strptime(self.date, '%Y-%m-%d') except (TypeError, ValueError): - self.logger.warning('Invalid date \'%s\'', self.date) + self.logger.warning(f"Invalid date '{self.date}'") if api_date is None: utc_time = pytz.utc.localize(datetime.utcnow()) @@ -113,10 +111,11 @@ class ScoresBackend(SettingsBase): except ValueError: return number if 4 <= number <= 20: - return '%d%s' % (number, 'th') + suffix = 'th' else: ord_map = {1: 'st', 2: 'nd', 3: 'rd'} - return '%d%s' % (number, ord_map.get(number % 10, 'th')) + suffix = ordmap.get(number % 10, 'th') + return f'{number}{suffix}' @staticmethod def force_int(value): @@ -135,8 +134,10 @@ class ScoresBackend(SettingsBase): key = int(key) data = data[key] except (KeyError, IndexError, TypeError): - self.logger.debug('No %s data found at %s, falling back to %s', - self.__class__.__name__, expr, repr(default)) + self.logger.debug( + f'No {self.name} data found at {expr}, ' + f'falling back to {repr(default)}' + ) return default return callback(data) @@ -401,10 +402,8 @@ class Scores(Module): # Check to make sure the team abbreviation is valid if team_uc not in backend._valid_teams: raise ValueError( - 'Invalid %s team \'%s\'' % ( - backend.__class__.__name__, - backend.favorite_teams[index] - ) + f'Invalid {backend.name} team ' + f"'{backend.favorite_teams[index]}'" ) backend.favorite_teams[index] = team_uc @@ -413,10 +412,8 @@ class Scores(Module): # Check to make sure the display order item is valid if order_lc not in backend._valid_display_order: raise ValueError( - 'Invalid %s display_order \'%s\'' % ( - backend.__class__.__name__, - backend.display_order[index] - ) + f'Invalid {backend.name} display_order ' + f"'{backend.display_order[index]}'" ) backend.display_order[index] = order_lc @@ -432,12 +429,11 @@ class Scores(Module): self.condition.wait(self.interval) self.check_scores(force='scheduled') except Exception: - msg = 'Exception in {thread} at {time}, module {name}'.format( - thread=threading.current_thread().name, - time=time.strftime('%c'), - name=self.__class__.__name__, + thread = threading.current_thread().name, + timestamp = time.strftime('%c') + self.logger.exception( + f'Exception in {thread} at {timestamp}, module {self.name}' ) - self.logger.error(msg, exc_info=True) @property def current_backend(self): @@ -465,11 +461,8 @@ class Scores(Module): cur_index = self.current_scroll_index if cur_index is None: self.logger.debug( - 'Cannot scroll, no tracked {backend} games for ' - '{date:%Y-%m-%d}'.format( - backend=self.current_backend.__class__.__name__, - date=self.current_backend.date, - ) + f'Cannot scroll, no tracked {self.current_backend.name} games ' + f'for {self.current_backend.date:%Y-%m-%d}' ) else: new_index = (cur_index + step) % len(self.current_backend.scroll_order) @@ -480,22 +473,17 @@ class Scores(Module): # self.current_scroll_index serves as a shorthand. self.game_map[self.backend_id] = new_index self.logger.debug( - 'Scrolled from %s game %d (ID: %s) to %d (ID: %s)', - self.current_backend.__class__.__name__, - cur_index, - cur_id, - new_index, - self.current_backend.scroll_order[new_index], + f'Scrolled from {self.current_backend.name} ' + f'game {cur_index} (ID: {cur_id}) to {new_index} ' + f'(ID: {self.current_backend.scroll_order[new_index]})' ) self.refresh_display() else: self.logger.debug( - 'Cannot scroll, only one tracked {backend} game ' - '(ID: {id_}) for {date:%Y-%m-%d}'.format( - backend=self.current_backend.__class__.__name__, - id_=self.current_game_id, - date=self.current_backend.date, - ) + f'Cannot scroll, only one tracked ' + f'{self.current_backend.name} game ' + f'(ID: {self.current_game_id}) for ' + f'{self.current_backend.date:%Y-%m-%d}' ) def cycle_backend(self, step=1): @@ -509,9 +497,8 @@ class Scores(Module): # Set the new backend self.backend_id = (self.backend_id + step) % len(self.backends) self.logger.debug( - 'Changed scores backend from %s to %s', - self.backends[old].__class__.__name__, - self.current_backend.__class__.__name__, + f'Changed scores backend from {self.backends[old].name} to ' + f'{self.current_backend.name}' ) # Display the score for the new backend. This gets rid of lag between # when the mouse is clicked and when the new backend is shown, caused @@ -524,15 +511,14 @@ class Scores(Module): if self.current_backend.games: self.game_map[self.backend_id] = 0 self.logger.debug( - 'Resetting to first game in %s scroll list (ID: %s)', - self.current_backend.__class__.__name__, - self.current_game_id, + f'Resetting to first game in {self.current_backend.name} ' + f'scroll list (ID: {self.current_game_id})' ) self.refresh_display() else: self.logger.debug( - 'No %s games, cannot reset to first game in scroll list', - self.current_backend.__class__.__name__, + f'No {self.current_backend.name} games, cannot reset to first ' + 'game in scroll list', ) def launch_web(self): @@ -541,7 +527,7 @@ class Scores(Module): live_url = self.current_backend.scoreboard_url else: live_url = game['live_url'] - self.logger.debug('Launching %s in browser', live_url) + self.logger.debug(f'Launching {live_url} in browser') user_open(live_url) @require(internet) @@ -550,27 +536,30 @@ class Scores(Module): if not self.current_backend.last_update: update_needed = True self.logger.debug( - 'Performing initial %s score check', - self.current_backend.__class__.__name__, + f'Performing initial {self.current_backend.name} score check' ) elif force: update_needed = True self.logger.debug( - '%s score check triggered (%s)', - self.current_backend.__class__.__name__, - force + f'{self.current_backend.name} score check triggered ({force})' ) else: update_diff = time.time() - self.current_backend.last_update - msg = ('Seconds since last %s update (%f) ' % - (self.current_backend.__class__.__name__, update_diff)) + msg = ( + f'Seconds since last {self.current_backend.name} update ' + f'({update_diff}) ' + ) if update_diff >= self.interval: update_needed = True - msg += ('meets or exceeds update interval (%d), update ' - 'triggered' % self.interval) + msg += ( + f'meets or exceeds update interval ({self.interval}), ' + 'update triggered' + ) else: - msg += ('does not exceed update interval (%d), update ' - 'skipped' % self.interval) + msg += ( + f'does not exceed update interval ({self.interval}), ' + 'update skipped' + ) self.logger.debug(msg) if update_needed: @@ -584,10 +573,8 @@ class Scores(Module): # status. if cur_id is None: self.logger.debug( - 'No tracked {backend} games for {date:%Y-%m-%d}'.format( - backend=self.current_backend.__class__.__name__, - date=self.current_backend.date, - ) + f'No tracked {self.current_backend.name} games for ' + f'{self.current_backend.date:%Y-%m-%d}' ) else: cur_pos = self.game_map[self.backend_id] @@ -595,20 +582,15 @@ class Scores(Module): if cur_pos != new_pos: self.game_map[self.backend_id] = new_pos self.logger.debug( - 'Scroll position for current %s game (%s) updated ' - 'from %d to %d', - self.current_backend.__class__.__name__, - cur_id, - cur_pos, - new_pos, + f'Scroll position for current ' + f'{self.current_backend.name} game ' + f'({cur_id}) updated from {cur_pos} to {new_pos}' ) else: self.logger.debug( - 'Scroll position (%d) for current %s game (ID: %s) ' - 'unchanged', - cur_pos, - self.current_backend.__class__.__name__, - cur_id, + f'Scroll position ({cur_pos}) for current ' + f'{self.current_backend.name} game (ID: {cur_id}) ' + 'unchanged' ) else: # Reset the index to 0 if there are any tracked games, @@ -617,18 +599,15 @@ class Scores(Module): if self.current_backend.games: self.game_map[self.backend_id] = 0 self.logger.debug( - 'Tracked %s games updated, setting scroll position to ' - '0 (ID: %s)', - self.current_backend.__class__.__name__, - self.current_game_id + f'Tracked {self.current_backend.name} games updated, ' + f'setting scroll position to 0 ' + f'(ID: {self.current_game_id})' ) else: self.game_map[self.backend_id] = None self.logger.debug( - 'No tracked {backend} games for {date:%Y-%m-%d}'.format( - backend=self.current_backend.__class__.__name__, - date=self.current_backend.date, - ) + f'No tracked {self.current_backend.name} games for ' + f'{self.current_backend.date:%Y-%m-%d}' ) self.current_backend.last_update = time.time() self.refresh_display() @@ -644,15 +623,12 @@ class Scores(Module): else: game = copy.copy(self.current_game) - fstr = str(getattr( - self.current_backend, - 'format_%s' % game['status'] - )) + fstr = str(getattr(self.current_backend, f'format_{game["status"]}')) for team in ('home', 'away'): - abbrev_key = '%s_abbrev' % team + abbrev_key = f'{team}_abbrev' # Set favorite icon, if applicable - game['%s_favorite' % team] = self.favorite_icon \ + game[f'{team}_favorite'] = self.favorite_icon \ if game[abbrev_key] in self.current_backend.favorite_teams \ else '' @@ -663,10 +639,9 @@ class Scores(Module): ) if color is not None: for item in ('abbrev', 'city', 'name', 'name_short'): - key = '%s_%s' % (team, item) + key = f'{team}_{item}' if key in game: - val = '%s' % (color, game[key]) - game[key] = val + game[key] = f'{game[key]}' game['scroll'] = self.scroll_arrow \ if len(self.current_backend.games) > 1 \ diff --git a/i3pystatus/scores/epl.py b/i3pystatus/scores/epl.py deleted file mode 100644 index 60ec17d..0000000 --- a/i3pystatus/scores/epl.py +++ /dev/null @@ -1,383 +0,0 @@ -from i3pystatus.core.util import internet, require -from i3pystatus.scores import ScoresBackend - -import copy -import pytz -import time -from collections import namedtuple -from datetime import datetime - -LIVE_URL = 'http://live.premierleague.com/#/gameweek/%s/matchday/%s/match/%s' -CONTEXT_URL = 'http://live.premierleague.com/syndicationdata/context.json' -SCOREBOARD_URL = 'http://live.premierleague.com/' -API_URL = 'http://live.premierleague.com/syndicationdata/competitionId=%s/seasonId=%s/gameWeekId=%s/scores.json' -STATS_URL = 'http://live.premierleague.com/syndicationdata/competitionId=%s/seasonId=%s/matchDayId=%s/league-table.json' -MATCH_DETAILS_URL = 'http://live.premierleague.com/syndicationdata/competitionId=%s/seasonId=%s/matchDayId=%s/matchId=%s/match-details.json' - -MATCH_STATUS_PREGAME = 1 -MATCH_STATUS_IN_PROGRESS = 2 -MATCH_STATUS_FINAL = 3 -MATCH_STATUS_HALFTIME = 4 - - -class EPL(ScoresBackend): - ''' - Backend to retrieve scores from the English Premier League. For usage - examples, see :py:mod:`here <.scores>`. - - .. rubric:: Promotion / Relegation - - Due to promotion/relegation, the **team_colors** configuration will - eventuall become out of date. When this happens, it will be necessary to - manually set the colors for the newly-promoted teams until the source for - this module is updated. An example of setting colors for newly promoted - teams can be seen below: - - .. code-block:: python - - from i3pystatus import Status - from i3pystatus.scores import epl - - status = Status() - - status.register( - 'scores', - hints={'markup': 'pango'}, - colorize_teams=True, - backends=[ - epl.EPL( - teams=['LIV'], - team_colors={ - 'ABC': '#1D78CA', - 'DEF': '#8AFEC3', - 'GHI': '#33FA6D', - }, - ), - ], - ) - - status.run() - - .. rubric:: Available formatters - - * `{home_name}` — Name of home team (e.g. **Tottenham Hotspur**) - * `{home_name_short}` — Shortened team name (e.g. **Spurs**) - * `{home_abbrev}` — 2 or 3-letter abbreviation for home team's city (e.g. - **TOT**) - * `{home_score}` — Home team's current score - * `{home_wins}` — Home team's number of wins - * `{home_losses}` — Home team's number of losses - * `{home_draws}` — Home team's number of draws - * `{home_points}` — Home team's number of standings points - * `{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 (e.g. **Manchester United**) - * `{away_name_short}` — Name of away team's city (e.g. **Man Utd**) - * `{away_abbrev}` — 2 or 3-letter abbreviation for away team's name (e.g. - **MUN**) - * `{away_score}` — Away team's current score - * `{away_wins}` — Away team's number of wins - * `{away_losses}` — Away team's number of losses - * `{away_draws}` — Away team's number of draws - * `{away_points}` — Away team's number of standings points - * `{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. - * `{minute}` — Current minute of game when in progress - * `{start_time}` — Start time of game in system's localtime (supports - strftime formatting, e.g. `{start_time:%I:%M %p}`) - - .. rubric:: Team abbreviations - - * **ARS** — Arsenal - * **AVL** — Aston Villa - * **BOU** — Bournemouth - * **CHE** — Chelsea - * **CRY** — Crystal Palace - * **EVE** — Everton - * **LEI** — Leicester City - * **LIV** — Liverpool - * **MCI** — Manchester City - * **MUN** — Manchester United - * **NEW** — Newcastle United - * **NOR** — Norwich City - * **SOU** — Southampton - * **STK** — Stoke City - * **SUN** — Sunderland Association - * **SWA** — Swansea City - * **TOT** — Tottenham Hotspur - * **WAT** — Watford - * **WBA** — West Bromwich Albion - * **WHU** — West Ham United - ''' - 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 `.'), - ('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'), - ('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 date will be determined by ' - 'the return value of an API call to the **context_url**. ' - 'Due to API limitations, the date can presently only be ' - 'overridden to another date in the current week. This ' - 'option exists primarily for troubleshooting purposes.'), - ('live_url', 'URL string to launch EPL Live Match Centre. This value ' - 'should not need to be changed.'), - ('scoreboard_url', 'Link to the EPL 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.'), - ('stats_url', 'Alternate URL string from which to retrieve team ' - 'statistics. Like **live_url**, this value should not ' - 'need to be changed.'), - ('match_details_url', 'Alternate URL string from which to retrieve ' - 'match details. Like **live_url**, this value ' - 'should not need to be changed.'), - ) - - required = () - - _default_colors = { - 'ARS': '#ED1B22', - 'AVL': '#94BEE5', - 'BOU': '#CB0B0F', - 'CHE': '#195FAF', - 'CRY': '#195FAF', - 'EVE': '#004F9E', - 'LEI': '#304FB6', - 'LIV': '#D72129', - 'MCI': '#74B2E0', - 'MUN': '#DD1921', - 'NEW': '#06B3EB', - 'NOR': '#00A651', - 'SOU': '#DB1C26', - 'STK': '#D81732', - 'SUN': '#BC0007', - 'SWA': '#B28250', - 'TOT': '#DADADA', - 'WAT': '#E4D500', - 'WBA': '#B43C51', - 'WHU': '#9DE4FA', - } - - _valid_display_order = ['in_progress', 'final', 'pregame'] - - display_order = _valid_display_order - format_no_games = 'EPL: No games' - format_pregame = '[{scroll} ]EPL: [{away_favorite} ]{away_abbrev} ({away_points}, {away_wins}-{away_losses}-{away_draws}) at [{home_favorite} ]{home_abbrev} ({home_points}, {home_wins}-{home_losses}-{home_draws}) {start_time:%H:%M %Z}' - format_in_progress = '[{scroll} ]EPL: [{away_favorite} ]{away_abbrev} {away_score}[ ({away_power_play})], [{home_favorite} ]{home_abbrev} {home_score}[ ({home_power_play})] ({minute})' - format_final = '[{scroll} ]EPL: [{away_favorite} ]{away_abbrev} {away_score} ({away_points}, {away_wins}-{away_losses}-{away_draws}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_points}, {home_wins}-{home_losses}-{home_draws}) (Final)' - team_colors = _default_colors - context_url = CONTEXT_URL - live_url = LIVE_URL - scoreboard_url = SCOREBOARD_URL - api_url = API_URL - stats_url = STATS_URL - match_details_url = MATCH_DETAILS_URL - - def get_api_date(self): - # NOTE: We're not really using this date for EPL API calls, but we do - # need it to allow for a 'date' param to override which date we use for - # scores. - if self.date is not None and not isinstance(self.date, datetime): - try: - self.date = datetime.strptime(self.date, '%Y-%m-%d') - except (TypeError, ValueError): - self.logger.warning('Invalid date \'%s\'', self.date) - - if self.date is None: - self.date = datetime.strptime(self.context.date, '%Y%m%d') - - def get_context(self): - response = self.api_request(self.context_url) - if not response: - # There is no context data, but we still need a date to use in - # __init__.py to log that there are no games for the given date. - # Fall back to the parent class' function to set a date. - super(EPL, self).get_api_date() - return False - context_tuple = namedtuple( - 'Context', - ('competition', 'date', 'game_week', 'match_day', 'season') - ) - self.context = context_tuple( - *[ - response.get(x, '') - for x in ('competitionId', 'currentDay', 'gameWeekId', - 'matchDayId', 'seasonId') - ] - ) - return True - - def get_team_stats(self): - ret = {} - url = self.stats_url % (self.context.competition, - self.context.season, - self.context.match_day) - for item in self.api_request(url).get('Data', []): - try: - key = item.pop('TeamCode') - except KeyError: - self.logger.debug('Error occurred obtaining %s team stats', - self.__class__.__name__, - exc_info=True) - continue - ret[key] = item - return ret - - def get_minute(self, data, id_): - match_status = data[id_].get('StatusId', MATCH_STATUS_PREGAME) - if match_status == MATCH_STATUS_HALFTIME: - return 'Halftime' - if match_status == MATCH_STATUS_IN_PROGRESS: - url = self.match_details_url % (self.context.competition, - self.context.season, - data[id_].get('MatchDayId', ''), - id_) - try: - response = self.api_request(url) - return '%s\'' % response['Data']['Minute'] - except (KeyError, TypeError): - return '?\'' - else: - return '?\'' - - def check_scores(self): - if not self.get_context(): - data = team_game_map = {} - else: - self.get_api_date() - - url = self.api_url % (self.context.competition, - self.context.season, - self.context.game_week) - - for item in self.api_request(url).get('Data', []): - if item.get('Key', '') == self.date.strftime('%Y%m%d'): - game_list = item.get('Scores', []) - break - else: - game_list = [] - - self.logger.debug('game_list = %s', game_list) - - team_stats = self.get_team_stats() - - # 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: - continue - - try: - for key in ('HomeTeam', 'AwayTeam'): - team = game[key]['Code'].upper() - if team in self.favorite_teams: - team_game_map.setdefault(team, []).append(id_) - except KeyError: - continue - - data[id_] = game - # Merge in the team stats, because they are not returned in the - # initial API request. - for key in ('HomeTeam', 'AwayTeam'): - team = game[key]['Code'].upper() - data[id_][key]['Stats'] = team_stats.get(team, {}) - # Add the minute, if applicable - data[id_]['Minute'] = self.get_minute(data, id_) - - 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', 'Id') - _update('minute', 'Minute') - ret['live_url'] = self.live_url % (self.context.game_week, - self.context.match_day, - ret['id']) - - status_map = { - MATCH_STATUS_PREGAME: 'pregame', - MATCH_STATUS_IN_PROGRESS: 'in_progress', - MATCH_STATUS_FINAL: 'final', - MATCH_STATUS_HALFTIME: 'in_progress', - } - status_code = game.get('StatusId') - if status_code is None: - self.logger.debug('%s game %s is missing StatusId', - self.__class__.__name__, ret['id']) - status_code = 1 - ret['status'] = status_map[status_code] - - for ret_key, game_key in (('home', 'HomeTeam'), ('away', 'AwayTeam')): - _update('%s_score' % ret_key, '%s:Score' % game_key, default=0) - _update('%s_name' % ret_key, '%s:Name' % game_key) - _update('%s_name_short' % ret_key, '%s:ShortName' % game_key) - _update('%s_abbrev' % ret_key, '%s:Code' % game_key) - _update('%s_wins' % ret_key, '%s:Stats:Won' % game_key, default=0) - _update('%s_losses' % ret_key, '%s:Stats:Lost' % game_key) - _update('%s_draws' % ret_key, '%s:Stats:Drawn' % game_key) - _update('%s_points' % ret_key, '%s:Stats:Points' % game_key) - - try: - game_time = datetime.strptime( - game.get('DateTime', ''), - '%Y-%m-%dT%H:%M:%S' - ) - 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 game time for %s game %s:', - self.__class__.__name__, - ret['id'], - exc_info=True - ) - game_time = datetime.datetime(1970, 1, 1) - - london = pytz.timezone('Europe/London') - ret['start_time'] = london.localize(game_time).astimezone() - - self.logger.debug('Returned %s formatter data: %s', - self.__class__.__name__, ret) - - return ret diff --git a/i3pystatus/scores/mlb.py b/i3pystatus/scores/mlb.py index bc2e112..ba417f8 100644 --- a/i3pystatus/scores/mlb.py +++ b/i3pystatus/scores/mlb.py @@ -9,9 +9,9 @@ import time from datetime import datetime from urllib.request import urlopen -LIVE_URL = 'https://www.mlb.com/gameday/%s' +LIVE_URL = 'https://www.mlb.com/gameday/{id}' SCOREBOARD_URL = 'http://m.mlb.com/scoreboard' -API_URL = 'https://statsapi.mlb.com/api/v1/schedule?sportId=1,51&date=%04d-%02d-%02d&gameTypes=E,S,R,A,F,D,L,W&hydrate=team(),linescore(matchup,runners),stats,game(content(media(featured,epg),summary),tickets),seriesStatus(useOverride=true)&useLatestGames=false&language=en&leagueId=103,104,420' +API_URL = 'https://statsapi.mlb.com/api/v1/schedule?sportId=1,51&date={date:%Y-%m-%d}&gameTypes=E,S,R,A,F,D,L,W&hydrate=team(),linescore(matchup,runners),stats,game(content(media(featured,epg),summary),tickets),seriesStatus(useOverride=true)&useLatestGames=false&language=en&leagueId=103,104,420' class MLB(ScoresBackend): @@ -188,7 +188,7 @@ class MLB(ScoresBackend): @require(internet) def check_scores(self): self.get_api_date() - url = self.api_url % (self.date.year, self.date.month, self.date.day) + url = self.api_url.format(date=self.date) game_list = self.get_nested( self.api_request(url), @@ -231,44 +231,43 @@ class MLB(ScoresBackend): def process_game(self, game): ret = {} - self.logger.debug('Processing %s game data: %s', - self.__class__.__name__, game) + self.logger.debug(f'Processing {self.name} game data: {game}') linescore = self.get_nested(game, 'linescore', default={}) ret['id'] = game['gamePk'] ret['inning'] = self.get_nested(linescore, 'currentInning', default=0) ret['outs'] = self.get_nested(linescore, 'outs') - ret['live_url'] = self.live_url % ret['id'] + ret['live_url'] = self.live_url.format(id=ret['id']) for team in ('away', 'home'): - team_data = self.get_nested(game, 'teams:%s' % team, default={}) + team_data = self.get_nested(game, f'teams:{team}', default={}) if team == 'home': ret['venue'] = self.get_nested(team_data, 'venue:name') - ret['%s_city' % team] = self.get_nested( + ret[f'{team}_city'] = self.get_nested( team_data, 'team:locationName') - ret['%s_name' % team] = self.get_nested( + ret[f'{team}_name'] = self.get_nested( team_data, 'team:teamName') - ret['%s_abbrev' % team] = self.get_nested( + ret[f'{team}_abbrev'] = self.get_nested( team_data, 'team:abbreviation') - ret['%s_wins' % team] = self.get_nested( + ret[f'{team}_wins'] = self.get_nested( team_data, 'leagueRecord:wins', default=0) - ret['%s_losses' % team] = self.get_nested( + ret[f'{team}_losses'] = self.get_nested( team_data, 'leagueRecord:losses', default=0) - ret['%s_score' % team] = self.get_nested( + ret[f'{team}_score'] = self.get_nested( linescore, - 'teams:%s:runs' % team, + f'teams:{team}:runs', default=0) for key in ('delay', 'postponed', 'suspended'): @@ -313,17 +312,14 @@ class MLB(ScoresBackend): # 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['gamePk'], - exc_info=True + self.logger.exception( + f'Error encountered determining {self.name} game time for ' + f'game {game["gamePk"]}' ) game_time = datetime(1970, 1, 1) ret['start_time'] = pytz.timezone('UTC').localize(game_time).astimezone() - self.logger.debug('Returned %s formatter data: %s', - self.__class__.__name__, ret) + self.logger.debug(f'Returned {self.name} formatter data: {ret}') return ret diff --git a/i3pystatus/scores/nba.py b/i3pystatus/scores/nba.py index 523f946..b2f5765 100644 --- a/i3pystatus/scores/nba.py +++ b/i3pystatus/scores/nba.py @@ -6,10 +6,8 @@ import pytz import time from datetime import datetime -LIVE_URL = 'http://www.nba.com/gametracker/#/%s/lp' -SCOREBOARD_URL = 'http://www.nba.com/scores' -API_URL = 'http://data.nba.com/data/10s/json/cms/noseason/scoreboard/%04d%02d%02d/games.json' -STANDINGS_URL = 'http://data.nba.com/data/json/cms/%s/league/standings.json' +LIVE_URL = 'https://www.nba.com/game/{id}' +API_URL = 'https://cdn.nba.com/static/json/liveData/scoreboard/todaysScoreboard_00.json' class NBA(ScoresBackend): @@ -43,7 +41,6 @@ class NBA(ScoresBackend): followed. Otherwise, this formatter will be blank. * `{time_remaining}` — Time remaining in the current quarter/OT period * `{quarter}` — Number of the current quarter - * `{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, this formatter will show @@ -171,66 +168,32 @@ class NBA(ScoresBackend): format_final = '[{scroll} ]NBA: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Final[/{overtime}])' team_colors = _default_colors live_url = LIVE_URL - scoreboard_url = SCOREBOARD_URL api_url = API_URL - standings_url = STANDINGS_URL def check_scores(self): self.get_api_date() - url = self.api_url % (self.date.year, self.date.month, self.date.day) - response = self.api_request(url) - game_list = self.get_nested(response, - 'sports_content:games:game', - default=[]) - - standings_year = self.get_nested( - response, - 'sports_content:sports_meta:season_meta:standings_season_year', - default=self.date.year, - ) - - stats_list = self.get_nested( - self.api_request(self.standings_url % standings_year), - 'sports_content:standings:team', - default=[], - ) - team_stats = {} - for item in stats_list: - try: - key = item.pop('abbreviation') - except KeyError: - self.logger.debug('Error occurred obtaining team stats', - exc_info=True) - continue - team_stats[key] = item.get('team_stats', {}) - - self.logger.debug('%s team stats: %s', - self.__class__.__name__, team_stats) + 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['game_url'] + id_ = game['gameId'] except KeyError: continue try: - for key in ('home', 'visitor'): - team = game[key]['abbreviation'].upper() + 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 - # Merge in the team stats, because they are not returned in the - # initial API request. - for key in ('home', 'visitor'): - team = data[id_][key]['abbreviation'].upper() - data[id_][key].update(team_stats.get(team, {})) self.interpret_api_return(data, team_game_map) @@ -243,11 +206,10 @@ class NBA(ScoresBackend): callback=callback, default=default) - self.logger.debug('Processing %s game data: %s', - self.__class__.__name__, game) + self.logger.debug(f'Processing {self.name} game data: {game}') - _update('id', 'game_url') - ret['live_url'] = self.live_url % ret['id'] + _update('id', 'gameId') + ret['live_url'] = self.live_url.format(id=ret['id']) status_map = { '1': 'pregame', @@ -258,8 +220,9 @@ class NBA(ScoresBackend): status_code = period_data.get('game_status', '1') status = status_map.get(status_code) if status is None: - self.logger.debug('Unknown %s game status code \'%s\'', - self.__class__.__name__, status_code) + self.logger.debug( + f"Unknown {self.name} game status code '{status_code}'" + ) status_code = '1' ret['status'] = status_map[status_code] @@ -269,64 +232,59 @@ class NBA(ScoresBackend): period_diff = period_number - total_periods ret['quarter'] = 'OT' \ if period_diff == 1 \ - else '%dOT' % period_diff if period_diff > 1 \ + else f'{period_diff}OT' if period_diff > 1 \ else self.add_ordinal(period_number) else: ret['quarter'] = '' - ret['time_remaining'] = period_data.get('game_clock') + ret['time_remaining'] = game.get('game_clock') if ret['time_remaining'] == '': ret['time_remaining'] = 'End' elif ret['time_remaining'] is None: ret['time_remaining'] = '' ret['overtime'] = ret['quarter'] if 'OT' in ret['quarter'] else '' - _update('venue', 'arena') - - for ret_key, game_key in (('home', 'home'), ('away', 'visitor')): - _update('%s_score' % ret_key, '%s:score' % game_key, + for key in ('home', 'away'): + team_key = f'{key}Team' + _update(f'{key}_score', f'{team_key}:score', callback=self.force_int, default=0) - _update('%s_city' % ret_key, '%s:city' % game_key) - _update('%s_name' % ret_key, '%s:nickname' % game_key) - _update('%s_abbrev' % ret_key, '%s:abbreviation' % game_key) + _update(f'{key}_city', f'{team_key}:teamCity') + _update(f'{key}_name', f'{team_key}:teamName') + _update(f'{key}_abbrev', f'{team_key}:teamTricode') if 'playoffs' in game: - _update('%s_wins' % ret_key, 'playoffs:%s_wins' % game_key, + _update(f'{key}_wins', f'playoffs:{key}_wins', callback=self.force_int, default=0) - _update('%s_seed' % ret_key, 'playoffs:%s_seed' % game_key, + _update(f'{key}_seed', f'playoffs:{key}_seed', callback=self.force_int, default=0) else: - _update('%s_wins' % ret_key, '%s:wins' % game_key, + _update(f'{key}_wins', f'{team_key}:wins', callback=self.force_int, default=0) - _update('%s_losses' % ret_key, '%s:losses' % game_key, + _update(f'{key}_losses', f'{team_key}:losses', callback=self.force_int, default=0) - ret['%s_seed' % ret_key] = '' + 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 - game_time_str = '%s%s' % (game.get('date', ''), game.get('time', '')) try: - game_time = datetime.strptime(game_time_str, '%Y%m%d%H%M') + game_et = game.get('gameEt', '') + game_time = datetime.strptime(game_et, '%Y-%m-%dT%H:%M:%S%z') 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 game time for %s game %s:', - self.__class__.__name__, - game['id'], - exc_info=True + 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) - eastern = pytz.timezone('US/Eastern') - ret['start_time'] = eastern.localize(game_time).astimezone() + ret['start_time'] = game_time.astimezone() - self.logger.debug('Returned %s formatter data: %s', - self.__class__.__name__, ret) + self.logger.debug(f'Returned {self.name} formatter data: {ret}') return ret diff --git a/i3pystatus/scores/nhl.py b/i3pystatus/scores/nhl.py index 079cd10..165cedf 100644 --- a/i3pystatus/scores/nhl.py +++ b/i3pystatus/scores/nhl.py @@ -9,9 +9,9 @@ import time from datetime import datetime from urllib.request import urlopen -LIVE_URL = 'https://www.nhl.com/gamecenter/%s' +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=%04d-%02d-%02d&endDate=%04d-%02d-%02d&expand=schedule.teams,schedule.linescore,schedule.broadcasts.all&site=en_nhl&teamId=' +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=' class NHL(ScoresBackend): @@ -213,8 +213,7 @@ class NHL(ScoresBackend): @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) + url = self.api_url.format(date=self.date) game_list = self.get_nested(self.api_request(url), 'dates:0:games', @@ -244,13 +243,12 @@ class NHL(ScoresBackend): def process_game(self, game): ret = {} - self.logger.debug('Processing %s game data: %s', - self.__class__.__name__, game) + 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 % ret['id'] + ret['live_url'] = self.live_url.format(id=ret['id']) ret['period'] = self.get_nested( linescore, 'currentPeriodOrdinal') @@ -359,17 +357,14 @@ class NHL(ScoresBackend): # 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 + 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('Returned %s formatter data: %s', - self.__class__.__name__, ret) + self.logger.debug(f'Returned {self.name} formatter data: {ret}') return ret