Package turbogears :: Package visit :: Module api

Source Code for Module turbogears.visit.api

  1  # -*- coding: UTF-8 -*- 
  2  """Base API of the TurboGears Visit Framework.""" 
  3   
  4  __all__ = [ 
  5      'BaseVisitManager', 
  6      'Visit', 
  7      'VisitFilter', 
  8      'create_extension_model', 
  9      'current', 
 10      'enable_visit_plugin', 
 11      'set_current', 
 12      'start_extension', 
 13      'shutdown_extension', 
 14  ] 
 15   
 16   
 17  import logging 
 18  try: 
 19      from hashlib import sha1 
 20  except ImportError: 
 21      from sha import new as sha1 
 22  import threading 
 23  import time 
 24   
 25  from random import random 
 26  from datetime import timedelta, datetime 
 27   
 28  import cherrypy 
 29  import pkg_resources 
 30   
 31  from cherrypy.filters.basefilter import BaseFilter 
 32  from turbogears import config 
 33  from turbogears.util import load_class 
 34   
 35  log = logging.getLogger("turbogears.visit") 
 36   
 37  # Global VisitManager 
 38  _manager = None 
 39   
 40  # Global list of plugins for the Visit Tracking framework 
 41  _plugins = list() 
42 43 # Accessor functions for getting and setting the current visit information. 44 -def current():
45 """Retrieve the current visit record from the cherrypy request.""" 46 return getattr(cherrypy.request, "tg_visit", None)
47
48 -def set_current(visit):
49 """Set the current visit record on the cherrypy request being processed.""" 50 cherrypy.request.tg_visit = visit
51
52 -def _create_visit_manager(timeout):
53 """Create a VisitManager based on the plugin specified in the config file.""" 54 plugin_name = config.get("visit.manager", "sqlalchemy") 55 plugins = pkg_resources.iter_entry_points( 56 "turbogears.visit.manager", plugin_name) 57 log.debug("Loading visit manager from plugin: %s", plugin_name) 58 provider_class = None 59 for entrypoint in plugins: 60 try: 61 provider_class = entrypoint.load() 62 break 63 except ImportError, e: 64 log.error("Error loading visit plugin '%s': %s", entrypoint, e) 65 66 if not provider_class and '.' in plugin_name: 67 try: 68 provider_class = load_class(plugin_name) 69 except ImportError, e: 70 log.error("Error loading visit class '%s': %s", plugin_name, e) 71 if not provider_class: 72 raise RuntimeError("VisitManager plugin missing: %s" % plugin_name) 73 return provider_class(timeout)
74
75 76 # Interface for the TurboGears extension 77 78 -def start_extension():
79 global _manager 80 81 # Bail out if the application hasn't enabled this extension 82 if not config.get("visit.on", False): 83 return 84 85 # Bail out if this extension is already running 86 if _manager: 87 log.warning("Visit manager already running.") 88 return 89 90 # How long may the visit be idle before a new visit ID is assigned? 91 # The default is 20 minutes. 92 timeout = timedelta(minutes=config.get("visit.timeout", 20)) 93 log.info("Visit Tracking starting (timeout = %i sec).", timeout.seconds) 94 # Create the thread that manages updating the visits 95 _manager = _create_visit_manager(timeout) 96 97 visit_filter = VisitFilter() 98 # Install Filter into the root filter chain 99 if not hasattr(cherrypy.root, "_cp_filters"): 100 cherrypy.root._cp_filters = list() 101 if not visit_filter in cherrypy.root._cp_filters: 102 cherrypy.root._cp_filters.append(visit_filter)
103
104 -def shutdown_extension():
105 # Bail out if this extension is not running. 106 global _manager 107 if not _manager: 108 return 109 log.info("Visit Tracking shutting down.") 110 _manager.shutdown() 111 _manager = None
112
113 -def create_extension_model():
114 """Create the data model of the VisitManager if one exists.""" 115 if _manager: 116 _manager.create_model()
117
118 -def enable_visit_plugin(plugin):
119 """Register a visit tracking plugin. 120 121 These plugins will be called for each request. 122 123 """ 124 _plugins.append(plugin)
125
126 127 -class Visit(object):
128 """Basic container for visit related data.""" 129
130 - def __init__(self, key, is_new):
131 self.key = key 132 self.is_new = is_new
133
134 135 -class VisitFilter(BaseFilter):
136 """A filter that automatically tracks visitors.""" 137
138 - def __init__(self):
139 get = config.get 140 # Where to look for the session key in the request and in which order 141 self.source = [s.strip().lower() for s in 142 get("visit.source", "cookie").split(',')] 143 if set(self.source).difference(('cookie', 'form')): 144 log.warning("Unsupported 'visit.source' '%s' in configuration.") 145 # Get the name to use for the identity cookie. 146 self.cookie_name = get("visit.cookie.name", "tg-visit") 147 # and the name of the request param. MUST NOT contain dashes or dots, 148 # otherwise the NestedVariablesFilter will choke on it. 149 self.visit_key_param = get("visit.form.name", "tg_visit") 150 # TODO: The path should probably default to whatever 151 # the root is masquerading as in the event of a 152 # virtual path filter. 153 self.cookie_path = get("visit.cookie.path", "/") 154 # The secure bit should be set for HTTPS only sites 155 self.cookie_secure = get("visit.cookie.secure", False) 156 # By default, I don't specify the cookie domain. 157 self.cookie_domain = get("visit.cookie.domain", None) 158 assert self.cookie_domain != "localhost", "localhost" \ 159 " is not a valid value for visit.cookie.domain. Try None instead." 160 # Use max age only if the cookie shall explicitly be permanent 161 self.cookie_max_age = get("visit.cookie.permanent", 162 False) and int(get("visit.timeout", "20")) * 60 or None 163 log.info("Visit filter initialized")
164
165 - def before_main(self):
166 """Check whether submitted request belongs to an existing visit.""" 167 if not config.get("visit.on", True): 168 set_current(None) 169 return 170 visit = current() 171 if not visit: 172 visit_key = None 173 for source in self.source: 174 if source == 'cookie': 175 visit_key = cherrypy.request.simple_cookie.get( 176 self.cookie_name) 177 if visit_key: 178 visit_key = visit_key.value 179 log.debug("Retrieved visit key '%s' from cookie '%s'.", 180 visit_key, self.cookie_name) 181 elif source == 'form': 182 visit_key = cherrypy.request.params.pop( 183 self.visit_key_param, None) 184 log.debug( 185 "Retrieved visit key '%s' from request param '%s'.", 186 visit_key, self.visit_key_param) 187 if visit_key: 188 visit = _manager.visit_for_key(visit_key) 189 break 190 if visit: 191 log.debug("Using visit from request with key: %s", visit_key) 192 else: 193 visit_key = self._generate_key() 194 visit = _manager.new_visit_with_key(visit_key) 195 log.debug("Created new visit with key: %s", visit_key) 196 self.send_cookie(visit_key) 197 set_current(visit) 198 # Inform all the plugins that a request has been made for the current 199 # visit. This gives plugins the opportunity to track click-path or 200 # retrieve the visitor's identity. 201 try: 202 for plugin in _plugins: 203 plugin.record_request(visit) 204 except cherrypy.InternalRedirect, e: 205 # Can't allow an InternalRedirect here because CherryPy is dumb, 206 # instead change cherrypy.request.object_path to the url desired. 207 cherrypy.request.object_path = e.path
208 209 @staticmethod
210 - def _generate_key():
211 """Return a (pseudo)random hash based on seed.""" 212 # Adding remoteHost and remotePort doesn't make this any more secure, 213 # but it makes people feel secure... It's not like I check to make 214 # certain you're actually making requests from that host and port. So 215 # it's basically more noise. 216 key_string = '%s%s%s%s' % (random(), datetime.now(), 217 cherrypy.request.remote_host, cherrypy.request.remote_port) 218 return sha1(key_string).hexdigest()
219 229
250
251 252 -class BaseVisitManager(threading.Thread):
253
254 - def __init__(self, timeout):
255 super(BaseVisitManager, self).__init__(name="VisitManager") 256 self.timeout = timeout 257 self.queue = dict() 258 self.lock = threading.Lock() 259 self._shutdown = threading.Event() 260 self.interval = config.get('visit.interval', 30) # seconds 261 # We must create the visit model before the manager thread is started. 262 self.create_model() 263 self.setDaemon(True) 264 self.start()
265
266 - def create_model(self):
267 pass
268
269 - def new_visit_with_key(self, visit_key):
270 """Return a new Visit object with the given key.""" 271 raise NotImplementedError
272
273 - def visit_for_key(self, visit_key):
274 """Return the visit for this key. 275 276 Return None if the visit doesn't exist or has expired. 277 278 """ 279 raise NotImplementedError
280
281 - def update_queued_visits(self, queue):
282 """Extend the expiration of the queued visits.""" 283 raise NotImplementedError
284
285 - def update_visit(self, visit_key, expiry):
286 try: 287 self.lock.acquire() 288 self.queue[visit_key] = expiry 289 finally: 290 self.lock.release()
291
292 - def shutdown(self, timeout=None):
293 try: 294 self.lock.acquire() 295 self._shutdown.set() 296 self.join(timeout) 297 finally: 298 self.lock.release() 299 if self.isAlive(): 300 log.error("Visit Manager thread failed to shutdown.")
301
302 - def run(self):
303 while not self._shutdown.isSet(): 304 self.lock.acquire() 305 if self._shutdown.isSet(): 306 self.lock.release() 307 continue 308 queue = None 309 try: 310 # make a copy of the queue and empty the original 311 if self.queue: 312 queue = self.queue.copy() 313 self.queue.clear() 314 if queue is not None: 315 self.update_queued_visits(queue) 316 finally: 317 self.lock.release() 318 self._shutdown.wait(self.interval)
319