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. CalendarEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, routing_key) if not event_published: # message cannot be routed 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 as e: session.rollback() logger.error(f'Could not append to event store event {event.toJSON()}') raise e finally: session.close()
def test_calendar_delete(self): default_vehicles = ['bus', 'subway', 'train', 'tram', 'car', 'walking', 'bike', 'taxi', 'enjoy', 'mobike'] default_preferences = dict(vehicles=default_vehicles, personal_vehicles=[]) with session_scope(self.DBSession) as session: session.add(GlobalPreferences( user_id=1, preferences=default_preferences)) CalendarEvent.append_event(GlobalPreferencesCreatedEvent(str(uuid.uuid4()), 1, default_preferences)) post_data = json.dumps(dict(name='Home', description='Home sweet home', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[dict( name='bus', time=['19:00', '20:30'], mileage=10 )])) self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.simulate_eventual_consistency() self.client.delete('/users/1/calendars/1') self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertIsNone(session.query(Calendar).filter(Calendar.user_id == 1, Calendar.id == 1).first())
def test_global_preferences_modify(self): default_vehicles = ['bus', 'subway', 'train', 'tram', 'car', 'walking', 'bike', 'taxi', 'enjoy', 'mobike'] default_preferences = dict(vehicles=default_vehicles, personal_vehicles=[]) with session_scope(self.DBSession) as session: session.add(GlobalPreferences( user_id=1, preferences=default_preferences)) CalendarEvent.append_event(GlobalPreferencesCreatedEvent(str(uuid.uuid4()), 1, default_preferences)) new_preferences = json.dumps(dict( vehicles=['bus', 'subway'], personal_vehicles=[ dict( name="tesla", type='car', location=(13, 14), active=True ) ]) ) response = self.client.put('/users/1/preferences', data=new_preferences, content_type='application/json') self.assertTrue(response.status_code, 200) self.assertEqual(json.loads(response.data)['id'], 1) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual( session.query(GlobalPreferences).filter(GlobalPreferences.user_id == 1).first().preferences, json.loads(new_preferences)) response = self.client.put('/users/10/preferences', data=new_preferences, content_type='application/json') self.assertTrue(response.status_code, 404)
def test_all_calendars_get(self): default_vehicles = [ 'bus', 'subway', 'train', 'tram', 'car', 'walking', 'bike', 'taxi', 'enjoy', 'mobike' ] default_preferences = dict(vehicles=default_vehicles, personal_vehicles=[]) with session_scope(self.DBSession) as session: session.add( GlobalPreferences(user_id=1, preferences=default_preferences)) CalendarEvent.append_event( GlobalPreferencesCreatedEvent(str(uuid.uuid4()), 1, default_preferences)) post_data = json.dumps( dict(name='Home', description='Home sweet home', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[ dict(name='bus', time=['19:00', '20:30'], mileage=10) ])) self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.simulate_eventual_consistency() post_data = json.dumps( dict(name='Job', description='', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[ dict(name='bus', time=['19:00', '20:30'], mileage=10) ])) self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.simulate_eventual_consistency() response = self.client.get('/users/1/calendars') self.assertEqual( json.loads(response.data)['calendars'][0]['name'], 'Home') self.assertEqual( json.loads(response.data)['calendars'][1]['name'], 'Job') response = self.client.get('/users/128/calendars') self.assertEqual(response.status_code, 404)
def test_add_event(self): with session_scope(self.EventStoreSession) as session: event = GlobalPreferencesCreatedEvent(str(uuid.uuid4()), 1, dict( vehicles=['bus'], personal_vehicles=[dict( name='tesla', type='car', location=(44.700546, 8.035837), active=True )])) j_event = event.toJSON() CalendarEvent.append_event(event) q = session.query(CalendarEvent).first() self.assertEqual(q.event, j_event) self.assertEqual(json.loads(q.event)['type'], PREFERENCES_CREATED_EVENT)
def handle_delete(user_id, id): """ Delete calendar endpoint. It deletes a calendar. """ aggregate_status = build_calendar_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(id) in aggregate_status[str(user_id)]['calendars'].keys()): return not_found(jsonify(dict(error='invalid calendar'))) event = CalendarDeletedEvent(user_id=user_id, id=id) session = EventStoreSession() try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. CalendarEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, CALENDAR_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 calendar for user {user_id} with id: {id}') return ok(jsonify(dict(user_id=user_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_calendar_aggregate(): """ This builds the calendar aggregate by chronologically applying events in the event store. :return: the aggregate status. """ aggregate_status = {} event_list = [] try: results = CalendarEvent.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 == PREFERENCES_CREATED_EVENT: aggregate_status[user_id] = event_info elif event_type == PREFERENCES_MODIFIED_EVENT: aggregate_status[user_id]['preferences'] = event_info[ 'preferences'] global_preferences = aggregate_status[user_id]['preferences'] if 'calendars' in aggregate_status[user_id]: for calendar in aggregate_status[user_id]['calendars'].values( ): calendar[ 'preferences'] = make_calendar_preferences_consistent( global_preferences, calendar['preferences']) elif event_type == PREFERENCES_DELETED_EVENT: aggregate_status.pop(user_id) else: if 'calendars' not in aggregate_status[user_id].keys(): aggregate_status[user_id].update(dict(calendars=dict())) id = str(event['event_info']['id']) event_info.pop('user_id', None) event_info.pop('id', None) if event_type == CALENDAR_CREATED_EVENT or event_type == CALENDAR_MODIFIED_EVENT: aggregate_status[user_id]['calendars'][id] = event_info elif event_type == CALENDAR_DELETED_EVENT: aggregate_status[user_id]['calendars'].pop(id) return aggregate_status
def handle_preferences(user_id): """ Global preferences modify endpoint. It modifies user global preferences. """ aggregate_status = build_calendar_aggregate() if aggregate_status == -1: logger.error(f'Could not build user aggregate for request: {request}') return ko(jsonify(dict(error='cannot build user aggregate'))) if not (str(user_id) in aggregate_status.keys()): return not_found(jsonify(dict(error='invalid user'))) data = request.get_json() if validate_global_preferences(data): # checks the request payload is correctly formed event = GlobalPreferencesModifiedEvent(user_id=user_id, preferences=data) session = EventStoreSession() try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. CalendarEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, PREFERENCES_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 global preferences of user with id: {user_id}') return ok(jsonify(dict(id=user_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_calendar_get(self): default_vehicles = [ 'bus', 'subway', 'train', 'tram', 'car', 'walking', 'bike', 'taxi', 'enjoy', 'mobike' ] default_preferences = dict(vehicles=default_vehicles, personal_vehicles=dict()) with session_scope(self.DBSession) as session: session.add( GlobalPreferences(user_id=1, preferences=default_preferences)) CalendarEvent.append_event( GlobalPreferencesCreatedEvent(str(uuid.uuid4()), 1, default_preferences)) response = self.client.get('/users/4/preferences') self.assertEqual(response.status_code, 404) response = self.client.get('/users/1/preferences') self.assertEqual(response.status_code, 200) self.assertEqual( json.loads(response.data)['vehicles'], default_vehicles)
def test_calendar_create(self): default_vehicles = [ 'bus', 'subway', 'train', 'tram', 'car', 'walking', 'bike', 'taxi', 'enjoy', 'mobike' ] default_preferences = dict(vehicles=default_vehicles, personal_vehicles=[]) with session_scope(self.DBSession) as session: session.add( GlobalPreferences(user_id=1, preferences=default_preferences)) CalendarEvent.append_event( GlobalPreferencesCreatedEvent(str(uuid.uuid4()), 1, default_preferences)) post_data = json.dumps( dict(name='Home', description='', base=[44.6381, 10.5726], color=[243, 250, 0], active=False, carbon=False, preferences=[dict(name='bus', time=None, mileage=None)])) response = self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.assertEqual(response.status_code, 201) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual( session.query(Calendar).filter(Calendar.user_id == 1, Calendar.id == 1).first().name, 'Home') post_data = json.dumps( dict(name='Job', description='Job sweet job', base=[50.20, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[ dict(name='bus', time=['19:00', '20:30'], mileage=10) ])) response = self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.assertEqual(response.status_code, 201) self.assertEqual(json.loads(response.data)['id'], 2) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual( session.query(Calendar).filter(Calendar.user_id == 1, Calendar.id == 2).first().name, 'Job') post_data = json.dumps( dict(name='Home', description='second home', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[ dict(name='bus', time=['19:00', '20:30'], mileage=10) ])) response = self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.assertEqual(response.status_code, 400) # name already present post_data = json.dumps( dict(name='Home', description='second home', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[ dict(name='bus', time=['19:00', '20:30'], mileage=10), dict(name='tesla', time=None, mileage=None) ])) response = self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.assertEqual(response.status_code, 400) # tesla not in global preferences
def handle_modify(user_id, id): """ Calendar modify endpoint. It modifies a calendar resource. """ data = request.get_json() aggregate_status = build_calendar_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(id) in aggregate_status[str(user_id)]['calendars'].keys()): return not_found(jsonify(dict(error='invalid calendar'))) user_global_preferences = aggregate_status[str(user_id)]['preferences'] # validate the request payload and check if the preferences are applicable given the global one # in the current aggregate status if validate_calendar_data(data) and validate_calendar_preferences( data['preferences'], user_global_preferences): user_calendars = aggregate_status[str(user_id)]['calendars'] user_calendars.pop(str(id)) # pop for preserving idempotency if data['name'] in ((value['name'] for value in user_calendars.values())): return bad_request(jsonify(dict(error='invalid data'))) data['user_id'] = user_id data['id'] = id event = CalendarModifiedEvent(**data) session = EventStoreSession() try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. CalendarEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, CALENDAR_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 calendar for user {user_id} with id: {id}') return ok(jsonify(dict(user_id=user_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 test_calendar_modify(self): default_vehicles = ['bus', 'subway', 'train', 'tram', 'car', 'walking', 'bike', 'taxi', 'enjoy', 'mobike'] default_preferences = dict(vehicles=default_vehicles, personal_vehicles=[]) with session_scope(self.DBSession) as session: session.add(GlobalPreferences( user_id=1, preferences=default_preferences)) CalendarEvent.append_event(GlobalPreferencesCreatedEvent(str(uuid.uuid4()), 1, default_preferences)) post_data = json.dumps(dict(name='Home', description='Home sweet home', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[dict( name='bus', time=['19:00', '20:30'], mileage=10 )])) self.client.post('/users/1/calendars', data=post_data, content_type='application/json') self.simulate_eventual_consistency() put_data = json.dumps(dict(name='Test', description='Test sweet Test', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[dict( name='bus', time=['19:00', '20:30'], mileage=10 )])) response = self.client.put('/users/1/calendars/1', data=put_data, content_type='application/json') self.assertEqual(response.status_code, 200) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual( session.query(Calendar).filter(Calendar.user_id == 1, Calendar.id == 1).first().name, 'Test' ) put_data = json.dumps(dict(name='Test', description='Test sweet Test', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[ dict( name='bus', time=['19:00', '20:30'], mileage=10 ), dict( name='tesla', time=['19:00', '20:30'], mileage=100 )] )) response = self.client.put('/users/1/calendars/1', data=put_data, content_type='application/json') self.assertEqual(response.status_code, 400) # there is no tesla in global preferences response = self.client.put('/users/1/calendars/3', data=put_data, content_type='application/json') self.assertEqual(response.status_code, 404) self.assertEqual(json.loads(response.data)['error'], 'invalid calendar')
def test_build_aggregate(self): event1 = GlobalPreferencesCreatedEvent( str(uuid.uuid4()), 1, dict(vehicles=['bus'], personal_vehicles=[ dict(name='tesla', type='car', location=(44.700546, 8.035837), active=True) ])) event2 = CalendarCreatedEvent( user_id=1, id=1, name='Home', description='Home sweet home', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[dict(name='bus', time=['19:00', '20:30'], mileage=5)]) event3 = CalendarCreatedEvent( user_id=1, id=2, name='Job', description='Job sweet job', base=[20, 20], color=[243, 250, 152], active=True, carbon=True, preferences=[dict(name='bus', time=['19:00', '20:30'], mileage=5)]) event4 = CalendarModifiedEvent(user_id=1, id=1, name='Home', description='Home not so sweet home', base=[50, 50], color=[243, 250, 152], active=True, carbon=True, preferences=[ dict(name='bus', time=['19:00', '20:30'], mileage=5), dict(name='tesla', time=['19:00', '20:30'], mileage=200) ]) event5 = CalendarDeletedEvent(user_id=1, id=2) event6 = GlobalPreferencesModifiedEvent( 1, dict(personal_vehicles=[ dict(name='tesla', type='car', location=(44.700546, 8.035837), active=True) ])) CalendarEvent.append_event(event1) CalendarEvent.append_event(event2) CalendarEvent.append_event(event3) CalendarEvent.append_event(event4) CalendarEvent.append_event(event5) CalendarEvent.append_event(event6) aggregate_status = build_calendar_aggregate() self.assertEqual( aggregate_status['1']['calendars']['1']['description'], 'Home not so sweet home') self.assertFalse('2' in aggregate_status['1']['calendars']) self.assertTrue('bus' not in [ vehicle['name'] for vehicle in aggregate_status['1']['calendars'] ['1']['preferences'] ])