def chain_events(base_event_name, events_to_chain, now, time_group, system='default'): """ Chain additional events with a base set of events. Note: ``OR`` operators will apply only to their direct predecessors (i.e., ``A && B && C || D`` will be handled as ``A && B && (C || D)``, and ``A && B || C && D`` will be handled as ``A && (B || C) && D``). :param str base_event_name: Name of event to chain additional events to/with :param list events_to_chain: List of additional event names to chain (e.g., ``[{'name': 'user:logged_in', 'op': 'and'}]``) :param datetime now: Time point at which to get event data :param str time_group: Time scale by which to group results; can be `days`, `weeks`, `months`, `years` :param str system: Which bitmapist should be used :returns: Bitmapist events collection """ fn_get_events = _events_fn(time_group) base_event = fn_get_events(base_event_name, now, system) if not base_event.has_events_marked(): return '' if events_to_chain: chain_events = [] # for idx, event_to_chain in enumerate(events_to_chain): for event_to_chain in events_to_chain: event_name = event_to_chain.get('name') chain_event = fn_get_events(event_name, now, system) chain_events.append(chain_event) # Each OR should operate only on its immediate predecessor, e.g., # `A && B && C || D` should be handled as ~ `A && B && (C || D)`, # and # `A && B || C && D` should be handled as ~ `A && (B || C) && D`. op_or_indices = [idx for idx, e in enumerate(events_to_chain) if e['op'] == 'or'] # Work backwards; least impact on operator combos + list indexing for idx in reversed(op_or_indices): # If first of events to chain, OR will just operate on base event if idx > 0: prev_event = chain_events[idx - 1] or_event = chain_events.pop(idx) # OR events should not be re-chained below events_to_chain.pop(idx) chain_events[idx - 1] = BitOpOr(prev_event, or_event) for idx, name_and_op in enumerate(events_to_chain): if name_and_op.get('op') == 'or': base_event = BitOpOr(base_event, chain_events[idx]) else: base_event = BitOpAnd(base_event, chain_events[idx]) return base_event
def test_bit_operations(): delete_all_events() now = datetime.utcnow() last_month = datetime.utcnow() - timedelta(days=30) # 123 has been active for two months mark_event('active', 123, now=now) mark_event('active', 123, now=last_month) # 224 has only been active last_month mark_event('active', 224, now=last_month) # Assert basic premises assert MonthEvents('active', last_month.year, last_month.month).get_count() == 2 assert MonthEvents('active', now.year, now.month).get_count() == 1 # Try out with bit AND operation active_2_months = BitOpAnd( MonthEvents('active', last_month.year, last_month.month), MonthEvents('active', now.year, now.month) ) assert active_2_months.get_count() == 1 assert 123 in active_2_months assert 224 not in active_2_months # Try out with bit OR operation assert BitOpOr( MonthEvents('active', last_month.year, last_month.month), MonthEvents('active', now.year, now.month) ).get_count() == 2 # Try out with a different system active_2_months = BitOpAnd( 'default_copy', MonthEvents('active', last_month.year, last_month.month), MonthEvents('active', now.year, now.month), ) assert active_2_months.get_count() == 1 assert active_2_months.system == 'default_copy' # Try nested operations active_2_months = BitOpAnd( BitOpAnd( MonthEvents('active', last_month.year, last_month.month), MonthEvents('active', now.year, now.month) ), MonthEvents('active', now.year, now.month) ) assert 123 in active_2_months assert 224 not in active_2_months