Source code for pymt.timeline

"""Execute events along a timeline.

Examples
--------
>>> timeline = Timeline()
>>> timeline.add_recurring_event('event 1', 1.)
>>> timeline.add_recurring_event('event 2', .3)
>>> timeline.pop()
'event 2'
>>> timeline.pop()
'event 2'
>>> timeline.pop()
'event 2'
>>> timeline.pop()
'event 1'

>>> timeline.time
1.0

>>> for event in timeline.iter_until(2.0): print(event)
event 2
event 2
event 2
event 1

>>> timeline.pop()
'event 2'

When events occur at the same time, events are popped in as first-in, first-out.

>>> timeline = Timeline([('event 1', 1.), ('event 2', .5)])
>>> for event in timeline.iter_until(1.0): print(event)
event 2
event 1
event 2

>>> timeline = Timeline([('event 2', .5), ('event 1', 1.)])
>>> timeline.pop_until(1.05)
['event 2', 'event 1', 'event 2']

>>> timeline.time
1.05

>>> for event in timeline.iter_until(1.05): print(event)
>>> timeline.time
1.05
>>> for event in timeline.iter_until(1.06): print(event)
>>> timeline.time
1.06

The event key can be any object, even a `tuple`.

>>> timeline = Timeline([(('event', 2), .5), (('event', 0), 1.)])
>>> for event in timeline.iter_until(1.05): print(event)
('event', 2)
('event', 0)
('event', 2)

Events do not have to be recurring.

>>> timeline = Timeline([(('event', 2), .5), (('event', 0), 1.)])
>>> timeline.add_one_time_event('one-timer', .1)
>>> for event in timeline.iter_until(1.05): print(event)
one-timer
('event', 2)
('event', 0)
('event', 2)
"""
import bisect


[docs]class Timeline: """Create a timeline of events. Parameters ---------- events : dict-like Events as event-object/repeat interval pairs. start : float, optional Start time for the timeline. Examples -------- Create a timeline with two recurring events. Events can be any old object. In this case they are two strings. >>> timeline = Timeline([('hello', 1.), ('world', 1.5)], start=3.) >>> sorted(timeline.events) # The events are returned as a set. ['hello', 'world'] The first events will not occur at the starting time. >>> timeline.time 3.0 >>> timeline.next_event 'hello' >>> timeline.time_of_next_event 4.0 To advance the timeline forward to the next event, use the `pop` method. >>> timeline.pop() 'hello' >>> timeline.time 4.0 >>> timeline.pop() 'world' >>> timeline.time 4.5 The timeline keeps track of objects, without making copies. The objects don't even need to be hashable. >>> hello = ['hello', 'world'] >>> timeline = Timeline([(hello, 1.)]) >>> event = timeline.pop() >>> event is hello True >>> event.append('!') >>> hello ['hello', 'world', '!'] """ def __init__(self, events=None, start=0.0): events = events or {} self._time = float(start) self._events = [] self._times = [] self._intervals = [] self.add_recurring_events(events) @property def time(self): """Current time along the timeline. Examples -------- >>> timeline = Timeline(start=0) >>> timeline.time 0.0 >>> timeline = Timeline(start=2) >>> timeline.time 2.0 """ return self._time @property def next_event(self): """Next event object. Return the next event object but don't advance the timeline forward in time. Examples -------- >>> timeline = Timeline([('an event', 1.)]) >>> timeline.next_event 'an event' >>> timeline.time 0.0 """ try: return self._events[0] except IndexError: raise IndexError("empty timeline") @property def time_of_next_event(self): """Time when the next event will happen. Examples -------- >>> timeline = Timeline([('an event', 1.)]) >>> timeline.time_of_next_event 1.0 """ try: return self._times[0] except IndexError: raise IndexError("empty timeline") @property def events(self): """All of the event objects in the timeline. Examples -------- >>> timeline = Timeline([('an event', 1.), ('another event', 1.)]) >>> events = timeline.events >>> isinstance(events, set) True >>> sorted(events) ['an event', 'another event'] """ return set(self._events)
[docs] def add_recurring_events(self, events): """Add a series of recurring events to the timeline. Adds recurring events to the timeline. *events* is a list where each element is tuple that gives the event object followed by the event recurrence interval. Parameters ---------- events : dict-like Events object/interval pairs to add to the timeline. Examples -------- >>> timeline = Timeline() >>> len(timeline.events) 0 >>> timeline.add_recurring_events([('hello', 1.), ('world', 1.)]) >>> sorted(timeline.events) ['hello', 'world'] Events pop as first-in, first-out. >>> timeline.pop() 'hello' >>> timeline.pop() 'world' The same event can be added multiple times. >>> timeline = Timeline() >>> timeline.add_recurring_events([('hello', 1.), ('world', 1.), ... ('hello', 1.)]) >>> sorted(timeline.events) ['hello', 'world'] >>> timeline.pop_until(2.) ['hello', 'world', 'hello', 'hello', 'world', 'hello'] """ try: event_items = events.items() except AttributeError: event_items = events for event, interval in event_items: self.add_recurring_event(event, interval)
[docs] def add_recurring_event(self, event, interval): """Add a recurring event to the timeline. Adds a single recurring event to the timeline. *event* is the event object, and *interval* is recurrence interval. Parameters ---------- event : event-like Event to add to the timeline. interval : float Recurrence interval of the event. Examples -------- >>> timeline = Timeline() >>> timeline.add_recurring_event('say hello', 1.) >>> timeline.events == {'say hello'} True """ self._insert_event(event, self.time + interval, interval)
[docs] def add_one_time_event(self, event, time): """Add an event that will only happen once. Parameters ---------- event : event-like Event to add to the timeline. time : float Time for the event to execute. Examples -------- >>> timeline = Timeline() >>> timeline.add_one_time_event('say hello', 1.) >>> timeline.time_of_next_event 1.0 >>> timeline.pop() 'say hello' >>> timeline.time_of_next_event # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): IndexError: empty timeline """ self._insert_event(event, time, None)
def _insert_event(self, event, time, interval): index = bisect.bisect_right(self._times, time) self._times.insert(index, time) self._events.insert(index, event) self._intervals.insert(index, interval)
[docs] def pop(self): """Pop the next event from the timeline. Examples -------- >>> timeline = Timeline(dict(hello=1.)) >>> timeline.pop() 'hello' """ try: (event, time, interval) = ( self._events.pop(0), self._times.pop(0), self._intervals.pop(0), ) except IndexError: raise IndexError("pop from empty timeline") try: self._insert_event(event, time + interval, interval) except TypeError: pass self._time = time return event
[docs] def iter_until(self, stop): """Iterate the timeline until a given time. Iterate the timeline until *stop*, popping events along the way. Parameters ---------- stop : float Time to iterate until. Returns ------- event The next event object as the timeline advances to *stop*. """ if stop < self.time: raise ValueError("stop time is less than current time") while self.time_of_next_event <= stop: yield self.pop() self._time = stop
[docs] def pop_until(self, stop): """Advance the timeline, popping events along the way. Advance the timeline to *stop*, popping events along the way. Returns a list of the event objects that were popped to advance the timeline to *stop*. Parameters ---------- stop : float Time to iterate until. Returns ------- list The events popped to get to the stop time. Examples -------- >>> timeline = Timeline([('a', 1.), ('b', 1.), ('c', 1.)]) >>> timeline.pop_until(2.) ['a', 'b', 'c', 'a', 'b', 'c'] """ events = [] for event in self.iter_until(stop): events.append(event) return events