import logging from collections import namedtuple from ...util import Heap from ...types import Timestamp from . import defaults from .event import Event, unpack_events logger = logging.getLogger("mw.lib.sessions.cache") Session = namedtuple("Session", ["user", "events"]) """ Represents a user session (a cluster over events for a user). This class behaves like :class:`collections.namedtuple`. Note that the datatypes of `events`, is not specified since those types will depend on the revision data provided during revert detection. :Members: **user** A hashable user identifier : `hashable` **events** A list of event data : list( `mixed` ) """ class Cache: """ A cache of recent user session. Since sessions expire once activities stop for at least `cutoff` seconds, this class manages a cache of *active* sessions. :Parameters: cutoff : int Maximum amount of time in seconds between session events :Example: >>> from mw.lib import sessions >>> >>> cache = sessions.Cache(cutoff=3600) >>> >>> list(cache.process("Willy on wheels", 100000, {'rev_id': 1})) [] >>> list(cache.process("Walter", 100001, {'rev_id': 2})) [] >>> list(cache.process("Willy on wheels", 100001, {'rev_id': 3})) [] >>> list(cache.process("Walter", 100035, {'rev_id': 4})) [] >>> list(cache.process("Willy on wheels", 103602, {'rev_id': 5})) [Session(user='Willy on wheels', events=[{'rev_id': 1}, {'rev_id': 3}])] >>> list(cache.get_active_sessions()) [Session(user='Walter', events=[{'rev_id': 2}, {'rev_id': 4}]), Session(user='Willy on wheels', events=[{'rev_id': 5}])] """ def __init__(self, cutoff=defaults.CUTOFF): self.cutoff = int(cutoff) self.recently_active = Heap() self.active_users = {} def process(self, user, timestamp, data=None): """ Processes a user event. :Parameters: user : `hashable` A hashable value to identify a user (`int` or `str` are OK) timestamp : :class:`mw.Timestamp` The timestamp of the event data : `mixed` Event meta data :Returns: A generator of :class:`~mw.lib.sessions.Session` expired after processing the user event. """ event = Event(user, Timestamp(timestamp), data) for user, events in self._clear_expired(event.timestamp): yield Session(user, unpack_events(events)) # Apply revision if event.user in self.active_users: events = self.active_users[event.user] else: events = [] self.active_users[event.user] = events self.recently_active.push((event.timestamp, events)) events.append(event) def get_active_sessions(self): """ Retrieves the active, unexpired sessions. :Returns: A generator of :class:`~mw.lib.sessions.Session` """ for last_timestamp, events in self.recently_active: yield Session(events[-1].user, unpack_events(events)) def _clear_expired(self, timestamp): # Cull old sessions while (len(self.recently_active) > 0 and timestamp - self.recently_active.peek()[0] >= self.cutoff): _, events = self.recently_active.pop() if timestamp - events[-1].timestamp >= self.cutoff: del self.active_users[events[-1].user] yield events[-1].user, events else: self.recently_active.push((events[-1].timestamp, events)) def __repr__(self): return "%s(%s)".format(self.__class__.__name__, repr(self.cutoff))