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 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 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')))