Package nflgame :: Module live
[frames] | no frames]

Source Code for Module nflgame.live

  1  """ 
  2  The live module provides a mechanism of periodically checking which games are 
  3  being actively played. 
  4   
  5  It requires the third party library pytz to be 
  6  installed, which makes sure game times are compared properly with respect 
  7  to time zones. pytz can be downloaded from PyPI: 
  8  http://pypi.python.org/pypi/pytz/ 
  9   
 10  It works by periodically downloading data from NFL.com for games that started 
 11  before the current time. Once a game completes, the live module stops asking 
 12  NFL.com for data for that game. 
 13   
 14  If there are no games being actively played (i.e., it's been more than N hours 
 15  since the last game started), then the live module sleeps for longer periods 
 16  of time. 
 17   
 18  Thus, the live module can switch between two different modes: active and 
 19  inactive. 
 20   
 21  In the active mode, the live module downloads data from NFL.com in 
 22  short intervals. A transition to an inactive mode occurs when no more games 
 23  are being played. 
 24   
 25  In the inactive mode, the live module only checks if a game is playing (or 
 26  about to play) every 15 minutes. If a game is playing or about to play, the 
 27  live module switches to the active mode. Otherwise, it stays in the inactive 
 28  mode. 
 29   
 30  With this strategy, if the live module is working properly, you could 
 31  theoretically keep it running for the entire season. 
 32   
 33  (N.B. Half-time is ignored. Games are either being actively played or not.) 
 34   
 35  Alpha status 
 36  ============ 
 37  This module is emphatically in alpha status. I believe things will work OK for 
 38  the regular season, but the postseason brings new challenges. Moreover, it 
 39  will probably affect the API at least a little bit. 
 40  """ 
 41  import datetime 
 42  import time 
 43  import urllib2 
 44  import xml.dom.minidom as xml 
 45   
 46  try: 
 47      import pytz 
 48  except ImportError: 
 49      pass 
 50   
 51  import nflgame.game 
 52  import nflgame.schedule 
 53   
 54  # [00:21] <rasher> burntsushi: Alright, the schedule changes on Wednesday 7:00 
 55  # UTC during the regular season 
 56   
 57  _MAX_GAME_TIME = 60 * 60 * 6 
 58  """ 
 59  The assumed maximum time allowed for a game to complete. This is used to 
 60  determine whether a particular game that isn't over is currently active. 
 61  """ 
 62   
 63  _WEEK_INTERVAL = 60 * 60 * 12 
 64  """ 
 65  How often to check what the current week is. By default, it is twice a day. 
 66  """ 
 67   
 68  _CUR_SCHEDULE_URL = "http://www.nfl.com/liveupdate/scorestrip/ss.xml" 
 69  """ 
 70  Pinged infrequently to discover the current week number, year and week type. 
 71  The actual schedule of games is taken from the schedule module. 
 72  """ 
 73   
 74  # http://www.nfl.com/ajax/scorestrip?season=2009&seasonType=POST&week=22 
 75  _POST_URL = "http://static.nfl.com/liveupdate/scorestrip/postseason/ss.xml" 
 76  """ 
 77  The URL for the XML schedule of the post season. This is only used 
 78  during the post season. 
 79   
 80  TODO: How do we know if it's the post season? 
 81  """ 
 82   
 83  _EASTERN_TZ = pytz.timezone('US/Eastern') 
 84  """Used to convert game times in EST to UTC.""" 
 85   
 86  _cur_week = None 
 87  """The current week. It is updated infrequently automatically.""" 
 88   
 89  _cur_year = None 
 90  """The current year. It is updated infrequently automatically.""" 
 91   
 92  _preseason = False 
 93  """True when it's the preseason.""" 
 94   
 95  _regular = False 
 96  """True when it's the regular season.""" 
 97   
 98  _completed = [] 
 99  """ 
100  A list of game eids that have been completed since the live module started 
101  checking for updated game stats. 
102  """ 
103   
104   
105 -def current_year_and_week():
106 """ 107 Returns a tuple (year, week) where year is the current year of the season 108 and week is the current week number of games being played. 109 i.e., (2012, 3). 110 111 N.B. This always downloads the schedule XML data. 112 """ 113 dom = xml.parse(urllib2.urlopen(_CUR_SCHEDULE_URL)) 114 gms = dom.getElementsByTagName('gms')[0] 115 year = int(gms.getAttribute('y')) 116 week = int(gms.getAttribute('w')) 117 return (year, week)
118 119
120 -def current_games(year=None, week=None, 121 regular=True, postseason=False, preseason=False):
122 """ 123 Returns a list of game.Games of games that are currently playing. 124 This fetches all current information from NFL.com. 125 126 If either year or week is none, then the current year and week are 127 fetched from the schedule on NFL.com. If they are *both* provided, then 128 the schedule from NFL.com won't need to be downloaded, and thus saving 129 time. 130 131 So for example:: 132 133 year, week = nflgame.live.current_year_and_week() 134 while True: 135 games = nflgame.live.current_games(year, week) 136 # Do something with games 137 time.sleep(60) 138 139 Finally, if the optional parameter postseason/preseason is True, then the 140 week parameter will refer to the postseason/preseason week rather than the 141 regular season week. 142 """ 143 if year is None or week is None: 144 year, week = current_year_and_week() 145 146 games = [] 147 now = _now() 148 gen = _games_in_week(year, week, regular=regular, postseason=postseason, 149 preseason=preseason) 150 for info in gen: 151 gametime = _game_datetime(info) 152 if gametime >= now: 153 if (gametime - now).total_seconds() <= 60 * 60 * 36: 154 games.append(info['eid']) 155 elif (now - gametime).total_seconds() <= _MAX_GAME_TIME: 156 games.append(info['eid']) 157 return games
158 159
160 -def run(callback, active_interval=15, inactive_interval=900, stop=None):
161 """ 162 Starts checking for games that are currently playing. 163 164 Every time there is an update, callback will be called with two lists: 165 active and completed. The active list is a list of game.Game that are 166 currently being played. The completed list is a list of game.Game that 167 have just finished. A game will appear in the completed list only once, 168 after which that game will not be in either the active or completed lists. 169 No game can ever be in both lists at the same time. 170 171 It is possible that a game in the active list is not yet playing because 172 it hasn't started yet. It ends up in the active list because the "pregame" 173 has started on NFL.com's GameCenter web site, and sometimes game data is 174 partially filled. When this is the case, the 'playing' method on 175 a nflgame.game.Game will return False. 176 177 When in the active mode (see live module description), active_interval 178 specifies the number of seconds to wait between checking for updated game 179 data. Please do not make this number too low to avoid angering NFL.com. 180 If you anger them too much, it is possible that they could ban your IP 181 address. 182 183 Note that NFL.com's GameCenter page is updated every 15 seconds, so 184 setting the active_interval much smaller than that is wasteful. 185 186 When in the inactive mode (see live module description), inactive_interval 187 specifies the number of seconds to wait between checking whether any games 188 have started or are about to start. 189 190 With the default parameters, run will never stop. However, you may set 191 stop to a Python datetime.datetime value. After time passes the stopping 192 point, run will quit. (Technically, it's possible that it won't quit until 193 at most inactive_interval seconds after the stopping point is reached.) 194 The stop value is compared against datetime.datetime.now(). 195 """ 196 active = False 197 last_week_check = _update_week_number() 198 199 # Before we start with the main loop, we make a first pass at what we 200 # believe to be the active games. Of those, we check to see if any of 201 # them are actually already over, and add them to _completed. 202 for info in _active_games(inactive_interval): 203 game = nflgame.game.Game(info['eid']) 204 205 # If we couldn't get a game, that probably means the JSON feed 206 # isn't available yet. (i.e., we're early.) 207 if game is None: 208 continue 209 210 # Otherwise, if the game is over, add it to our list of completed 211 # games and move on. 212 if game.game_over(): 213 _completed.append(info['eid']) 214 215 while True: 216 if stop is not None and datetime.datetime.now() > stop: 217 return 218 219 if time.time() - last_week_check > _WEEK_INTERVAL: 220 last_week_check = _update_week_number() 221 222 games = _active_games(inactive_interval) 223 if active: 224 active = _run_active(callback, games) 225 if not active: 226 continue 227 time.sleep(active_interval) 228 else: 229 active = not _run_inactive(games) 230 if active: 231 continue 232 time.sleep(inactive_interval)
233 234
235 -def _run_active(callback, games):
236 """ 237 The active mode traverses each of the active games and fetches info for 238 each from NFL.com. 239 240 Then each game (that has info available on NFL.com---that is, the game 241 has started) is added to one of two lists: active and completed, which 242 are passed as the first and second parameters to callback. A game is 243 put in the active list if it's still being played, and into the completed 244 list if it has finished. In the latter case, it is added to a global store 245 of completed games and will never be passed to callback again. 246 """ 247 # There are no active games, so just quit and return False. Which means 248 # we'll transition to inactive mode. 249 if len(games) == 0: 250 return False 251 252 active, completed = [], [] 253 for info in games: 254 game = nflgame.game.Game(info['eid']) 255 256 # If no JSON was retrieved, then we're probably just a little early. 257 # So just ignore it for now---but we'll keep trying! 258 if game is None: 259 continue 260 261 # If the game is over, added it to completed and _completed. 262 if game.game_over(): 263 completed.append(game) 264 _completed.append(info['eid']) 265 else: 266 active.append(game) 267 268 callback(active, completed) 269 return True
270 271
272 -def _run_inactive(games):
273 """ 274 The inactive mode simply checks if there are any active games. If there 275 are, inactive mode needs to stop and transition to active mode---thus 276 we return False. If there aren't any active games, then the inactive 277 mode should continue, where we return True. 278 279 That is, so long as there are no active games, we go back to sleep. 280 """ 281 return len(games) == 0
282 283
284 -def _active_games(inactive_interval):
285 """ 286 Returns a list of all active games. In this case, an active game is a game 287 that will start within inactive_interval seconds, or has started within 288 _MAX_GAME_TIME seconds in the past. 289 """ 290 gen = _games_in_week(_cur_year, _cur_week, 291 regular=_regular, 292 postseason=False, 293 preseason=_preseason) 294 games = [] 295 for info in gen: 296 if not _game_is_active(info, inactive_interval): 297 continue 298 games.append(info) 299 return games
300 301
302 -def _games_in_week(year, week, 303 regular=True, postseason=False, preseason=False):
304 """ 305 A generator for the games matching the year/week/preseason parameters. 306 """ 307 for (y, t, w, _, _), info in nflgame.schedule.games: 308 if y != year: 309 continue 310 if w != week: 311 continue 312 if t == 'PRE' and not preseason: 313 continue 314 if t == 'REG' and not regular: 315 continue 316 yield info
317 318
319 -def _game_is_active(gameinfo, inactive_interval):
320 """ 321 Returns true if the game is active. A game is considered active if the 322 game start time is in the past and not in the completed list (which is 323 a private module level variable that is populated automatically) or if the 324 game start time is within inactive_interval seconds from starting. 325 """ 326 gametime = _game_datetime(gameinfo) 327 now = _now() 328 if gametime >= now: 329 return (gametime - now).total_seconds() <= inactive_interval 330 return gameinfo['eid'] not in _completed
331 332
333 -def _game_datetime(gameinfo):
334 hour, minute = gameinfo['time'].strip().split(':') 335 d = datetime.datetime(gameinfo['year'], gameinfo['month'], gameinfo['day'], 336 (int(hour) + 12) % 24, int(minute)) 337 return _EASTERN_TZ.localize(d).astimezone(pytz.utc)
338 339
340 -def _now():
341 return datetime.datetime.now(pytz.utc)
342 343
344 -def _update_week_number():
345 global _cur_week, _cur_year, _preseason, _regular 346 347 dom = xml.parse(urllib2.urlopen(_CUR_SCHEDULE_URL)) 348 gms = dom.getElementsByTagName('gms')[0] 349 _cur_week = int(gms.getAttribute('w')) 350 _cur_year = int(gms.getAttribute('y')) 351 _preseason = gms.getAttribute('t').strip() == 'P' 352 _regular = gms.getAttribute('t').strip() == 'R' 353 return time.time()
354