def test_event_get(self): event = CalendarIdCreatedEvent(str(uuid.uuid4()), 1, 1) EventEvent.append_event(event) with session_scope(self.DBSession) as session: session.add(CalendarId( user_id=1, id=1)) now = datetime.datetime.now() post_data = json.dumps( dict(name='Evento', location=[44.6368, 10.5697], start_time=strftime((now + datetime.timedelta(days=1))), end_time=strftime((now + datetime.timedelta(days=1, hours=1))), recurrence_rule='MONTHLY', next_is_base=False, until=strftime(datetime.datetime(year=2018, month=2, day=1)), flex=True, flex_duration=1800)) self.client.post('/users/1/calendars/1/events', data=post_data, content_type='application/json') self.simulate_eventual_consistency() response = self.client.get('/users/1/calendars/1/events/1') self.assertEqual(response.status_code, 200)
def test_event_delete(self): event = CalendarIdCreatedEvent(str(uuid.uuid4()), 1, 1) EventEvent.append_event(event) with session_scope(self.DBSession) as session: session.add(CalendarId( user_id=1, id=1)) now = datetime.datetime.now() post_data = json.dumps( dict(name='Pranzo Dalla Nonna', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), recurrence_rule='DAILY', next_is_base=False, until=strftime(now + datetime.timedelta(days=3)), flex=True, flex_duration=1800)) self.client.post('/users/1/calendars/1/events', data=post_data, content_type='application/json') self.simulate_eventual_consistency() self.client.delete('/users/1/calendars/1/events/1') self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertIsNone(session.query(Event).first())
def add_and_publish_event(event, routing_key): """ Adds the event to the event store and publish it on the event bus. :param event: the event. :param routing_key: the routing key for topic-matching. """ session = EventStoreSession() try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. EventEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, routing_key) if not event_published: logger.error(f'Could not publish event {event.toJSON()}') session.rollback() raise SQLAlchemyError else: session.commit() logger.info(f'Added and published event {event.toJSON()}') except SQLAlchemyError: session.rollback() logger.error(f'Could not append to event store event {event.toJSON()}') raise SQLAlchemyError finally: session.close()
def test_add_event(self): with session_scope(self.EventStoreSession) as session: event = CalendarIdCreatedEvent(str(uuid.uuid4()), 1, 1) j_event = event.toJSON() EventEvent.append_event(event) q = session.query(EventEvent).first() self.assertEqual(q.event, j_event) self.assertEqual( json.loads(q.event)['type'], CALENDAR_ID_CREATED_EVENT)
def test_event_create(self): event = CalendarIdCreatedEvent(str(uuid.uuid4()), 1, 1) EventEvent.append_event(event) with session_scope(self.DBSession) as session: session.add(CalendarId(user_id=1, id=1)) now = datetime.datetime.now() post_data = json.dumps( dict(name='Pranzo Dalla Nonna', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), recurrence_rule='DAILY', next_is_base=False, until=strftime(now + datetime.timedelta(days=3)), flex=True, flex_duration=1800)) response = self.client.post('/users/1/calendars/1/events', data=post_data, content_type='application/json') self.simulate_eventual_consistency() self.assertEqual(response.status_code, 201) now = datetime.datetime.now() post_data = json.dumps( dict(name='Football Match', location=[44.6368, 10.5697], start_time=strftime(now + datetime.timedelta(days=3)), end_time=strftime(now + datetime.timedelta(days=4)), recurrence_rule='NORMAL', next_is_base=False, until=None, flex=None, flex_duration=None)) response = self.client.post('/users/1/calendars/1/events', data=post_data, content_type='application/json') self.simulate_eventual_consistency() self.assertEqual(response.status_code, 400) # the event overlaps now = datetime.datetime.now() post_data = json.dumps( dict(name='Football Match', location=[44.6368, 10.5697], start_time=strftime(now + datetime.timedelta(days=10)), end_time=strftime(now + datetime.timedelta(days=12)), recurrence_rule='NORMAL', next_is_base=False, until=None, flex=None, flex_duration=None)) response = self.client.post('/users/1/calendars/1/events', data=post_data, content_type='application/json') self.simulate_eventual_consistency() self.assertEqual(response.status_code, 201)
def handle_recurrence_delete(user_id, calendar_id, event_id, id): """ This endpoint deletes a recurrence of an event. """ aggregate_status = build_event_aggregate() if aggregate_status == -1: logger.error( f'Could not build calendar aggregate for request: {request}') return ko(jsonify(dict(error='cannot build calendar aggregate'))) if not (str(user_id) in aggregate_status.keys()): return not_found(jsonify(dict(error='invalid user'))) if not (str(calendar_id) in aggregate_status[str(user_id)]['calendars'].keys()): return not_found(jsonify(dict(error='invalid calendar'))) if not (str(event_id) in aggregate_status[str(user_id)]['calendars'][str( calendar_id)]['events'].keys()): return not_found(jsonify(dict(error='invalid event'))) if not (str(id) in aggregate_status[str(user_id)]['calendars'][str( calendar_id)]['events'][str(event_id)]['recurrences'].keys()): return not_found(jsonify(dict(error='invalid recurrence'))) data = dict() data['user_id'] = user_id data['calendar_id'] = calendar_id data['event_id'] = event_id data['id'] = id event = RecurrenceDeletedEvent(**data) session = EventStoreSession() try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. EventEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, RECURRENCE_DELETED) if not event_published: logger.error( f'Could not publish event on event bus for request: {request}') session.rollback() return ko( jsonify(dict(error='could not publish event on event bus'))) else: session.commit() logger.info( f'Deleted reccurece for user {user_id} on calendar id {calendar_id} on event id {event_id} with id {id}' ) return ok( jsonify(dict(user_id=user_id, calendar_id=calendar_id, id=id))) except SQLAlchemyError: session.rollback() logger.error(f'Could not append to event store for request: {request}') return ko(jsonify(dict(error='could not append to event store'))) finally: session.close()
def build_event_aggregate(): """ This builds the event aggregate by chronologically applying events in the event store. :return: the aggregate status. """ aggregate_status = {} event_list = [] try: results = EventEvent.get_all_events() except SQLAlchemyError: return -1 if not results: return {} for r in results: event_list.append(json.loads(r.event)) for event in event_list: user_id = str(event['event_info']['user_id']) event_type = event['type'] event_info = event['event_info'] if event_type == CALENDAR_ID_CREATED_EVENT: if user_id not in aggregate_status: aggregate_status[user_id] = \ {'calendars': {}} aggregate_status[user_id]['calendars'].update( {str(event_info['id']): { 'events': {} }}) elif event_type == CALENDAR_ID_DELETED_EVENT: aggregate_status[user_id]['calendars'].pop(str(event_info['id'])) elif event_type == USER_CALENDARS_DELETED_EVENT: try: aggregate_status.pop(user_id) except KeyError: pass # user had no calendars else: calendar_id = str(event_info['calendar_id']) event_id = str(event_info['id']) if event_type == EVENT_CREATED_EVENT or event_type == EVENT_MODIFIED_EVENT: event_info.pop('user_id') event_info.pop('calendar_id') event_info.pop('id') event_info['start_time'] = strptime(event_info['start_time']) event_info['end_time'] = strptime((event_info['end_time'])) aggregate_status[user_id]['calendars'][calendar_id][ 'events'].update( {event_id: { **event_info, 'recurrences': {} }}) start_time = event_info['start_time'] end_time = event_info['end_time'] if event_info['recurrence_rule'] == 'NORMAL': aggregate_status[user_id]['calendars'][calendar_id][ 'events'][event_id]['recurrences'].update({ '1': dict(start_time=start_time, end_time=end_time) }) else: rec_rule = event_info['recurrence_rule'] until = strptime(event_info['until']) start_occurrences, end_occurrences = generate_occurrences( RRULE[rec_rule]['name'], start_time, end_time, until) for index, (s_time, e_time) in enumerate( zip(start_occurrences, end_occurrences), 1): aggregate_status[user_id]['calendars'][calendar_id][ 'events'][event_id]['recurrences'].update({ str(index): { 'start_time': s_time, 'end_time': e_time } }) elif event_type == EVENT_DELETED_EVENT: aggregate_status[user_id]['calendars'][calendar_id][ 'events'].pop(event_id) return aggregate_status
def handle_modify(user_id, calendar_id, id): """ This endpoint modifies an event resource. """ data = request.get_json() aggregate_status = build_event_aggregate() if aggregate_status == -1: logger.error( f'Could not build calendar aggregate for request: {request}') return ko(jsonify(dict(error='cannot build calendar aggregate'))) if not (str(user_id) in aggregate_status.keys()): return not_found(jsonify(dict(error='invalid user'))) if not (str(calendar_id) in aggregate_status[str(user_id)]['calendars'].keys()): return not_found(jsonify(dict(error='invalid calendar'))) if not (str(id) in aggregate_status[str(user_id)]['calendars'][str( calendar_id)]['events'].keys()): return not_found(jsonify(dict(error='invalid event'))) aggregate_status[str(user_id)]['calendars'][str( calendar_id)]['events'].pop(str(id)) user_schedule = aggregate_status[str(user_id)]['calendars'] if validate_event(data) and event_can_be_inserted(data, user_schedule): if 'until' not in data: data['until'] = None if 'flex' not in data: data['flex'] = None data['flex_duration'] = None data['user_id'] = user_id data['calendar_id'] = calendar_id data['id'] = id event = EventModifiedEvent(**data) session = EventStoreSession() try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. EventEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, EVENT_MODIFIED) if not event_published: logger.error( f'Could not publish event on event bus for request: {request}' ) session.rollback() return ko( jsonify( dict(error='could not publish event on event bus'))) else: session.commit() logger.info( f'Modified event for user {user_id} on calendar id {calendar_id} with id {id}' ) return ok( jsonify( dict(user_id=user_id, calendar_id=calendar_id, id=id))) except SQLAlchemyError: session.rollback() logger.error( f'Could not append to event store for request: {request}') return ko(jsonify(dict(error='could not append to event store'))) finally: session.close() return bad_request(jsonify(dict(error='invalid data')))
def handle_create(user_id, calendar_id): """ This endpoint creates a new event resource """ data = request.get_json() aggregate_status = build_event_aggregate() if aggregate_status == -1: logger.error( f'Could not build calendar aggregate for request: {request}') return ko(jsonify(dict(error='cannot build calendar aggregate'))) if not (str(user_id) in aggregate_status.keys()): return not_found(jsonify(dict(error='invalid user'))) if not (str(calendar_id) in aggregate_status[str(user_id)]['calendars'].keys()): return not_found(jsonify(dict(error='invalid calendar'))) user_schedule = aggregate_status[str(user_id)]['calendars'] # checks if the request payload is formed correctly and if the event can be inserted in the user schedule # given the current aggregate status if validate_event(data) and event_can_be_inserted(data, user_schedule): calendar_events = aggregate_status[str(user_id)]['calendars'][str( calendar_id)]['events'] if not calendar_events: new_id = 1 else: new_id = int(max(calendar_events, key=int)) + 1 if 'until' not in data: data['until'] = None if 'flex' not in data: data['flex'] = None data['flex_duration'] = None data['user_id'] = user_id data['calendar_id'] = calendar_id data['id'] = new_id event = EventCreatedEvent(**data) session = EventStoreSession() try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. EventEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, EVENT_CREATED) if not event_published: logger.error( f'Could not publish event on event bus for request: {request}' ) session.rollback() return ko( jsonify( dict(error='could not publish event on event bus'))) else: session.commit() logger.info( f'Created event for user {user_id} on calendar id {calendar_id} with id {new_id}' ) return created( jsonify( dict(user_id=user_id, calendar_id=calendar_id, id=new_id))) except SQLAlchemyError: session.rollback() logger.error( f'Could not append to event store for request: {request}') return ko(jsonify(dict(error='could not append to event store'))) finally: session.close() return bad_request(jsonify(dict(error='invalid data')))
def test_aggregate_builder(self): event1 = CalendarIdCreatedEvent(uuid=str(uuid.uuid4()), user_id=1, id=1) event2 = CalendarIdCreatedEvent(uuid=str(uuid.uuid4()), user_id=1, id=2) now = datetime.datetime.now() event3 = EventCreatedEvent( user_id=1, calendar_id=1, id=1, name='Meeting', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), recurrence_rule='NORMAL', next_is_base=False, until=None, flex=None, flex_duration=None ) now = datetime.datetime.now() event4 = EventCreatedEvent( user_id=1, calendar_id=1, id=2, name='Cena Natalizia', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), next_is_base=False, recurrence_rule='DAILY', until=strftime(now + datetime.timedelta(days=3)), flex=True, flex_duration=3600 ) now = datetime.datetime.now() event5 = EventModifiedEvent( user_id=1, calendar_id=1, id=1, name='Lol finals', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), next_is_base=False, recurrence_rule='DAILY', until=strftime(now + datetime.timedelta(days=10)), flex=True, flex_duration=3600 ) now = datetime.datetime.now() event6 = EventCreatedEvent( user_id=1, calendar_id=2, id=1, name='For lab', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), recurrence_rule='NORMAL', next_is_base=False, until=None, flex=None, flex_duration=None ) event7 = CalendarIdCreatedEvent(uuid=str(uuid.uuid4()), user_id=2, id=1) now = datetime.datetime.now() event8 = EventCreatedEvent( user_id=2, calendar_id=1, id=1, name='Cinema', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), recurrence_rule='NORMAL', next_is_base=False, until=None, flex=None, flex_duration=None ) event9 = EventDeletedEvent( user_id=1, calendar_id=1, id=2 ) EventEvent.append_event(event1) EventEvent.append_event(event2) EventEvent.append_event(event3) EventEvent.append_event(event4) EventEvent.append_event(event5) EventEvent.append_event(event6) EventEvent.append_event(event7) EventEvent.append_event(event8) EventEvent.append_event(event9) aggregate_status = build_event_aggregate() self.assertEqual(len(aggregate_status['1']['calendars'].keys()), 2) self.assertEqual(aggregate_status['1']['calendars']['1']['events']['1']['name'], 'Lol finals') self.assertEqual(len(aggregate_status['1']['calendars']['1']['events']['1']['recurrences'].keys()), 11) self.assertEqual(aggregate_status['2']['calendars']['1']['events']['1']['name'], 'Cinema') self.assertTrue('2' not in aggregate_status['1']['calendars']['1']['events']) event10 = CalendarIdDeletedEvent(uuid=str(uuid.uuid4()), user_id=1, id=1) EventEvent.append_event(event10) aggregate_status = build_event_aggregate() self.assertEqual(len(aggregate_status['1']['calendars'].keys()), 1)