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