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