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