mirror of
https://github.com/jlengrand/bugsink.git
synced 2026-03-10 08:01:17 +00:00
event listeners
This commit is contained in:
@@ -9,12 +9,22 @@ MAX_HOURS = 24
|
||||
MAX_DAYS = 30
|
||||
MAX_MONTHS = 12
|
||||
MAX_YEARS = 5
|
||||
MAX_TOTALS = 1
|
||||
|
||||
|
||||
FOO_MIN = 1000, 1, 1, 0, 0
|
||||
FOO_MAX = 3000, 12, "?", 23, 59
|
||||
|
||||
|
||||
# TL for "tuple length", the length of the tuples for a given time-period
|
||||
TL_TOTAL = 0
|
||||
TL_YEAR = 1
|
||||
TL_MONTH = 2
|
||||
TL_DAY = 3
|
||||
TL_HOUR = 4
|
||||
TL_MINUTE = 5
|
||||
|
||||
|
||||
def apply_n(f, n, v):
|
||||
for i in range(n):
|
||||
v = f(v)
|
||||
@@ -43,35 +53,78 @@ def _prev_tup(tup):
|
||||
|
||||
|
||||
def _inc(d, tup, n, max_age):
|
||||
new_period = False
|
||||
|
||||
if tup not in d:
|
||||
# evict
|
||||
min_tup = apply_n(_prev_tup, max_age, tup)
|
||||
d = {k: v for k, v in d.items() if d >= min_tup}
|
||||
if len(d) > 0:
|
||||
new_period = True
|
||||
min_tup = apply_n(_prev_tup, max_age - 1, tup)
|
||||
for k, v in list(d.items()):
|
||||
if k < min_tup:
|
||||
del d[k]
|
||||
|
||||
# default
|
||||
d[tup] = 0
|
||||
|
||||
# inc
|
||||
d[tup] += n
|
||||
return new_period
|
||||
|
||||
|
||||
class PeriodCounter(object):
|
||||
|
||||
def __init__(self):
|
||||
self.total = 0
|
||||
self.years = {}
|
||||
self.months = {}
|
||||
self.days = {}
|
||||
self.hours = {}
|
||||
self.minutes = {}
|
||||
self.counts = {tuple_length: {} for tuple_length in range(TL_MINUTE + 1)}
|
||||
self.event_listeners = {tuple_length: {} for tuple_length in range(TL_MINUTE + 1)}
|
||||
|
||||
def inc(self, datetime_utc, n=1):
|
||||
tup = datetime_utc.timetuple()
|
||||
|
||||
self.total += n # self.forevers, ()
|
||||
for tl, mx in enumerate([MAX_TOTALS, MAX_YEARS, MAX_MONTHS, MAX_DAYS, MAX_HOURS, MAX_MINUTES]):
|
||||
new_period = _inc(self.counts[tl], tup[:tl], n, mx)
|
||||
|
||||
_inc(self.years, tup[:1], n, MAX_YEARS)
|
||||
_inc(self.months, tup[:2], n, MAX_MONTHS)
|
||||
_inc(self.days, tup[:3], n, MAX_DAYS)
|
||||
_inc(self.hours, tup[:4], n, MAX_HOURS)
|
||||
_inc(self.minutes, tup[:5], n, MAX_MINUTES)
|
||||
event_listeners_for_tl = self.event_listeners[tl]
|
||||
for ((how_many_periods, gte_threshold), (wbt, wbf, is_true)) in list(event_listeners_for_tl.items()):
|
||||
if is_true:
|
||||
if not new_period:
|
||||
continue # no new period means: never becomes false, because no old period becomes irrelevant
|
||||
|
||||
if not self._get_event_state(tup[:tl], tl, how_many_periods, gte_threshold):
|
||||
event_listeners_for_tl[(how_many_periods, gte_threshold)] = (wbt, wbf, False)
|
||||
wbf()
|
||||
|
||||
else:
|
||||
if self._get_event_state(tup[:tl], tl, how_many_periods, gte_threshold):
|
||||
event_listeners_for_tl[(how_many_periods, gte_threshold)] = (wbt, wbf, True)
|
||||
wbt()
|
||||
|
||||
def add_event_listener(self, period_name, how_many_periods, gte_threshold, when_becomes_true, when_becomes_false,
|
||||
event_state=None, tup=None):
|
||||
|
||||
if len([arg for arg in [event_state, tup] if arg is None]) != 1:
|
||||
# either be explicit, or let us deduce
|
||||
raise ValueError("Provide exactly one of (event_state, tup)")
|
||||
|
||||
tl = self._tl_for_period(period_name)
|
||||
if event_state is None:
|
||||
event_state = self._get_event_state(tup, tl, how_many_periods, gte_threshold)
|
||||
|
||||
self.event_listeners[tl][(how_many_periods, gte_threshold)] = \
|
||||
(when_becomes_true, when_becomes_false, event_state)
|
||||
|
||||
def _tl_for_period(self, period_name):
|
||||
return {
|
||||
"total": 0,
|
||||
"year": 1,
|
||||
"month": 2,
|
||||
"day": 3,
|
||||
"hour": 4,
|
||||
"minute": 5,
|
||||
}[period_name]
|
||||
|
||||
def _get_event_state(self, tup, tl, how_many_periods, gte_threshold):
|
||||
min_tup = apply_n(_prev_tup, how_many_periods - 1, tup)
|
||||
d = self.counts[tl]
|
||||
total = sum([v for k, v in d.items() if k >= min_tup])
|
||||
|
||||
return total >= gte_threshold
|
||||
|
||||
@@ -11,6 +11,14 @@ def apply_n(f, n, v):
|
||||
return v
|
||||
|
||||
|
||||
class callback(object):
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
def __call__(self):
|
||||
self.calls += 1
|
||||
|
||||
|
||||
class PeriodCounterTestCase(TestCase):
|
||||
|
||||
def test_prev_tup(self):
|
||||
@@ -42,3 +50,54 @@ class PeriodCounterTestCase(TestCase):
|
||||
datetime_utc = datetime.now(timezone.utc) # basically I just want to write this down somewhere
|
||||
pc = PeriodCounter()
|
||||
pc.inc(datetime_utc)
|
||||
|
||||
def test_event_listeners_for_total(self):
|
||||
timepoint = datetime(2020, 1, 1, 10, 15, tzinfo=timezone.utc)
|
||||
|
||||
pc = PeriodCounter()
|
||||
wbt = callback()
|
||||
wbf = callback()
|
||||
pc.add_event_listener("total", 1, 2, wbt, wbf, event_state=False)
|
||||
|
||||
# first inc: should not yet trigger
|
||||
pc.inc(timepoint)
|
||||
self.assertEquals(0, wbt.calls)
|
||||
|
||||
# second inc: should trigger (threshold of 2)
|
||||
pc.inc(timepoint)
|
||||
self.assertEquals(1, wbt.calls)
|
||||
|
||||
# third inc: should not trigger again
|
||||
pc.inc(timepoint)
|
||||
self.assertEquals(1, wbt.calls)
|
||||
|
||||
def test_event_listeners_for_year(self):
|
||||
tp_2020 = datetime(2020, 1, 1, 10, 15, tzinfo=timezone.utc)
|
||||
tp_2021 = datetime(2021, 1, 1, 10, 15, tzinfo=timezone.utc)
|
||||
tp_2022 = datetime(2022, 1, 1, 10, 15, tzinfo=timezone.utc)
|
||||
|
||||
pc = PeriodCounter()
|
||||
wbt = callback()
|
||||
wbf = callback()
|
||||
pc.add_event_listener("year", 2, 3, wbt, wbf, event_state=False)
|
||||
|
||||
pc.inc(tp_2020)
|
||||
self.assertEquals(0, wbt.calls)
|
||||
|
||||
pc.inc(tp_2020)
|
||||
self.assertEquals(0, wbt.calls)
|
||||
|
||||
# 3rd in total: become True
|
||||
pc.inc(tp_2021)
|
||||
self.assertEquals(1, wbt.calls)
|
||||
|
||||
# into a new year, total == 2: become false
|
||||
self.assertEquals(0, wbf.calls)
|
||||
pc.inc(tp_2022)
|
||||
self.assertEquals(1, wbf.calls)
|
||||
self.assertEquals(1, wbt.calls) # unchanged
|
||||
|
||||
# 3rd in (new) total: become True again
|
||||
pc.inc(tp_2022)
|
||||
self.assertEquals(2, wbt.calls)
|
||||
self.assertEquals(1, wbf.calls) # unchanged
|
||||
|
||||
Reference in New Issue
Block a user