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_gp_modify(self): event = GlobalPreferencesCreatedEvent( str(uuid.uuid4()), 1, dict(vehicles=[ 'bus', 'subway', 'train', 'tram', 'car', 'walking', 'bike', 'taxi', 'enjoy', 'mobike' ])) publish_event(self.app.sending_channel, event, PREFERENCES_CREATED) self.simulate_eventual_consistency() event = CalendarCreatedEvent( user_id=1, id=1, name='Home', description='', base=[35.1324, 36.1234], color=[243, 250, 152], active=True, carbon=True, preferences=[dict(name='bus', time=None, mileage=10)]) publish_event(self.app.sending_channel, event, CALENDAR_CREATED) self.simulate_eventual_consistency() event = GlobalPreferencesModifiedEvent(1, dict(vehicles=['mobike'])) publish_event(self.app.sending_channel, event, PREFERENCES_MODIFIED) 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().preferences, [])
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 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 handle_delete(user_id): """ Delete user endpoint. It deletes a user resource. """ aggregate_status = build_user_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'))) event = UserDeletedEvent(id=user_id) session = EventStoreSession() try: UserEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, USER_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 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()
def test_messages(self): event = UserCreatedEvent(1, '*****@*****.**', hash_pass('test_password'), 'test_first_name', 'test_last_name') publish_event(self.app.sending_channel, event, 'user.created') event = UserModifiedEvent(1, '*****@*****.**', hash_pass('test_password'), 'test_first_name', 'test_last_name') publish_event(self.app.sending_channel, event, USER_MODIFIED) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual( session.query(User).filter(User.id == 1).first().email, '*****@*****.**') event = UserDeletedEvent(1) publish_event(self.app.sending_channel, event, USER_DELETED) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertIsNone(session.query(User).filter(User.id == 1).first())
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 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 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 handle_create(): """ This is the create user endpoint. It creates a new user resource. """ data = request.get_json() aggregate_status = build_user_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 validate_request_payload(data): # checks the data in the payload is formed correctly email = data['email'] password = data['password'] first_name = data['first_name'] last_name = data['last_name'] logger.info("Validated request payload") if validate_user_data(email, password, first_name, last_name): logger.info("Validated user data") # checks if the account can be created given the current aggregate status if is_unique_email(email, aggregate_status): logger.info("Email is indeed unique") if aggregate_status: new_id = int(max(aggregate_status, key=int)) + 1 else: new_id = 1 data['id'] = new_id data['password'] = hash_pass(password) event = UserCreatedEvent(**data) session = EventStoreSession() logger.info(data) try: # both of these are wrapped inside a unique try catch because this must be an atomic operation. UserEvent.append_event(event) event_published = publish_event(get_sending_channel(), event, USER_CREATED) # create a new consumer inside kong kong_request = requests.post(KONG_CONSUMER_API, data={'username': email}) if not event_published or not kong_request.status_code == 201: 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 user with id: {new_id}') logger.info(f"Added new consumer to kong with username {email}") return created(jsonify(dict(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'))) except Exception as e: logger.error(e) finally: session.close() return bad_request(jsonify(dict(error='invalid data')))
def test_events(self): event = CalendarIdCreatedEvent(str(uuid.uuid4()), 1, 1) publish_event(self.app.sending_channel, event, CALENDAR_ID_CREATED) self.simulate_eventual_consistency() now = datetime.datetime.now() event = EventCreatedEvent( user_id=1, calendar_id=1, id=1, name='Pranzo Dalla Nonna', 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 ) publish_event(self.app.sending_channel, event, EVENT_CREATED) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual(len(session.query(Recurrence).all()), 4) event = 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 ) publish_event(self.app.sending_channel, event, EVENT_MODIFIED) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual(len(session.query(Event).all()), 1) self.assertEqual(session.query(Event).first().name, 'Lol finals') self.assertEqual(len(session.query(Recurrence).all()), 11) event = EventDeletedEvent( user_id=1, calendar_id=1, id=1 ) publish_event(self.app.sending_channel, event, EVENT_DELETED) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertIsNone(session.query(Event).first()) self.assertIsNone(session.query(Recurrence).first()) event = EventCreatedEvent( user_id=1, calendar_id=1, id=1, name='Pranzo Dalla Nonna', location=[44.6368, 10.5697], start_time=strftime(now), end_time=strftime(now + datetime.timedelta(hours=1)), next_is_base=False, recurrence_rule='NORMAL', until=None, flex=None, flex_duration=None ) publish_event(self.app.sending_channel, event, EVENT_CREATED) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertEqual(len(session.query(Recurrence).all()), 1) event = CalendarIdDeletedEvent(str(uuid.uuid4()), 1, 1) publish_event(self.app.sending_channel, event, CALENDAR_ID_DELETED) self.simulate_eventual_consistency() with session_scope(self.DBSession) as session: self.assertIsNone(session.query(Event).first()) self.assertIsNone(session.query(Recurrence).first())