Update scores backends (#812)

1. Remove EPL as it has been broken for several years
2. Fix NBA to reflect backend API changes
3. Replace percent string formatting with f-strings now that i3pystatus
   only supports Python 3.6+
This commit is contained in:
Erik Johnson 2021-04-10 14:51:50 -05:00 committed by GitHub
parent 3976efe5cc
commit b0826cf6ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 143 additions and 602 deletions

View File

@ -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 = '<span color="%s">%s</span>' % (color, game[key])
game[key] = val
game[key] = f'<span color="{color}">{game[key]}</span>'
game['scroll'] = self.scroll_arrow \
if len(self.current_backend.games) > 1 \

View File

@ -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 <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'),
('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

View File

@ -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

View File

@ -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

View File

@ -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