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