def update_book_genre(genre_id: Union[str, uuid4], book_genre_id: Union[str, uuid4]) -> Optional[dict]: """Updates a book_genre by a given id. Args: genre_id: The FK to the genre table. book_genre_id: The PK of a book_genre. Returns: An updated book_genre with the given id else None. Raises: InvalidParamException: If any of the given params are None. UniqueEntityException: If a book_id/genre_id match already exists in the table. ModifyingPrimaryException: If attempting to modify to replace the book's primary genre. """ validate_params(func='update_book_genre', params={ 'genre_id': genre_id, 'book_genre_id': book_genre_id }) book_genre = data.get_book_genre_by_id(book_genre_id=book_genre_id) validate_entity_is_unique( func=data.get_book_genres_by_book_id_and_genre_id, book_id=book_genre.get('book_id'), genre_id=genre_id) validate_primary_genre( book_id=data.get_book_genre_by_id(book_genre_id).get('book', {}).get('id'), genre_id=genre_id, method='PATCH') return data.update_book_genre(genre_id=genre_id, book_genre_id=book_genre_id)
def get_user_book_by_id( user_id: Union[str, uuid4], book_id: Union[str, uuid4] ) -> Optional[dict]: """Gets a user book by a given ID. Args: user_id: The unique user ID associated with the JWT authorized in the request. book_id: The unique ID associated with the book being retrieved. Returns: A book populated with aggregated information else None. Raises: InvalidParamException: If the given user ID or book ID are None. UnauthorizedAccessException: If the user ID associated with the book does not match the user ID pulled from the request's JWT. """ log.info( f'Attempting to retrieve aggregated book data for book with id "{book_id}" for user with ' f'id "{user_id}".' ) validate_params(func='get_user_book_by_id', params={'user_id': user_id, 'book_id': book_id}) book = security.validate_user_book(user_id=user_id, book_id=book_id) return populate_genres_for_user_book(user_book=book)
def create_book_genre( book_id: Union[str, uuid4], genre_id: Union[str, uuid4], book_genre_id: Optional[Union[str, uuid4]] = None) -> Optional[dict]: """Creates a new book_genre. Args: book_id: The FK to the books table. genre_id: The FK to the genres table. book_genre_id: The PK to assign to the new book_genre. Returns: A newly created book_genre else None. Raises: InvalidParamException: If any of the given params are None. UniqueEntityException: If a book_id/genre_id match already exists in the table. MultiplePrimaryException: If the given book id already has a primary genre. """ validate_params(func='create_book_genre', params={ 'book_id': book_id, 'genre_id': genre_id }) validate_entity_is_unique( func=data.get_book_genres_by_book_id_and_genre_id, book_id=book_id, genre_id=genre_id) validate_primary_genre(book_id=book_id, genre_id=genre_id, method='POST') return data.create_book_genre(book_id=book_id, genre_id=genre_id, book_genre_id=book_genre_id)
def delete_user_book_by_id( user_id: Union[str, uuid4], book_id: Union[str, uuid4] ) -> Optional[dict]: """Deletes the book by the given ID. Args: user_id: The unique ID of the user pulled off of the authorized JWT. book_id: The unique ID of the book to be deleted. Returns: The deleted book else None. Raises: InvalidParamException: If any of the required parameters are None. UnauthorizedAccessException: If the user ID associated with the book does not match the user ID pulled from the request's JWT. """ log.info(f'Deleting book with id "{book_id}".') validate_params( func='delete_user_book_by_id', params={'user_id': user_id, 'book_id': book_id} ) security.validate_user_book(user_id=user_id, book_id=book_id) book_genres = book_genres_service.get_book_genres_by_book_id(book_id=book_id) genre_ids = [book_genre.get('genre', {}).get('id') for book_genre in book_genres] for book_genre in book_genres: book_genres_service.delete_book_genre_by_id(book_genre_id=book_genre.get('id')) for genre_id in genre_ids: if not book_genres_service.get_book_genres_by_genre_id(genre_id=genre_id): genres_service.delete_genre_by_id(genre_id=genre_id) return books_service.delete_book_by_id(book_id=book_id)
def update_genre(user_id: Union[str, uuid4], name: str, display_name: str, genre_id: Union[str, uuid4]) -> Optional[dict]: """Updates a genre by a given id. Args: user_id: The FK to the users table. name: The name to modify in the genre with the given id. display_name: The display_name to modify in the genre with the given id. genre_id: The PK of a genre. Returns: An updated genre with the given id else None. Raises: InvalidParamException: If any of the given params are None. UniqueEntityException: If a matching genre is found by the display name and/or user_id. """ validate_params(func='update_genre', params={ 'name': name, 'display_name': display_name, 'genre_id': genre_id }) validate_entity_is_unique(func=data.get_genre_by_display_name_and_user_id, display_name=display_name, user_id=user_id) return data.update_genre(name=name, display_name=display_name, genre_id=genre_id)
def get_book_genres_by_genre_id(genre_id: Optional[Union[str, uuid4]]) -> list: """Gets book_genres from the table filtered by given params. Args: genre_id: The FK to the genres table. Returns: A list of book_genres filtered by any given params. """ validate_params(func='get_book_genres_by_genre_id', params={'genre_id': genre_id}) return data.get_book_genres_by_genre_id(genre_id=genre_id)
def create_secondary_book_genre( user_id: Union[str, uuid4], book_id: Union[str, uuid4], secondary_genre_name: str ) -> Optional[dict]: """Creates a new secondary genre tied to the given book ID. Args: user_id: The unique ID of the user pulled off of the authorized JWT. book_id: The unique ID of the book to tie the new secondary genre to. secondary_genre_name: The unique name of the genre to tie to the book. Returns: The updated secondary book genre else None. Raises: InvalidParamException: If any of the required parameters are None. UnauthorizedAccessException: If the user ID associated with the book does not match the user ID pulled from the request's JWT. """ log.info( f'Tying new secondary genre "{secondary_genre_name}" to book with id "{book_id}"' ) validate_params( func='create_secondary_book_genre', params={ 'user_id': user_id, 'book_id': book_id, 'secondary_genre_name': secondary_genre_name } ) security.validate_user_book(user_id=user_id, book_id=book_id) existing_genre = genres_service.get_genre_by_display_name_and_user_id( display_name=secondary_genre_name, user_id=user_id, ) if not existing_genre: log.info(f'Creating new genre "{secondary_genre_name}" tied to user id: "{user_id}"') new_genre = genres_service.create_genre( user_id=user_id, display_name=secondary_genre_name, name=secondary_genre_name ) new_book_genre = book_genres_service.create_book_genre( book_id=book_id, genre_id=existing_genre.get('id') if existing_genre else new_genre.get('id') ) return book_genres_service.get_book_genre_by_id(new_book_genre.get('id'))
def get_book_by_id(book_id: Union[str, uuid4]) -> Optional[dict]: """Gets a book from the table by a given id. Args: book_id: The PK of a book. Returns: A book from the table by a given id else None. Raises: InvalidParamException: If the given book_id is None. """ validate_params(func='get_book_by_id', params={'book_id': book_id}) return data.get_book_by_id(book_id=book_id)
def delete_book_genres(book_id: Union[str, uuid4]) -> Optional[list]: """Deletes book_genres from the table using the given params. Args: book_id: The FK to the book table. Returns: A list of book_genres deleted using the given params. Raises: InvalidParamException: If any of the given params are None. """ validate_params(func='delete_book_genres', params={'book_id': book_id}) return data.delete_book_genres(book_id=book_id)
def get_session_by_user(user_id: Union[str, uuid4]) -> Optional[dict]: """Gets a session using the given user_id. Args: user_id: The primary key associated with a user entity in the dim_users table. Returns: A session with the given user_id else None. Raises: InvalidParamException: If the user_id is None. """ validate_params(func='get_session_by_user', params={'user_id': user_id}) return data.get_session_by_user(user_id=user_id)
def delete_genres(user_id: Union[str, uuid4]) -> Optional[list]: """Deletes genres from the table using the given params. Args: user_id: The ID of the user to delete genres by. Returns: A list of genres deleted using the given params. Raises: InvalidParamException: If any of the given params are None. """ validate_params(func='delete_genres', params={'user_id': user_id}) return data.delete_genres_by_user_id(user_id=user_id)
def get_genre_by_id(genre_id: Union[str, uuid4]) -> Optional[dict]: """Gets a genre from the table by a given id. Args: genre_id: The PK of a genre. Returns: A genre from the table by a given id else None. Raises: InvalidParamException: If the given genre_id is None. """ validate_params(func='get_genre_by_id', params={'genre_id': genre_id}) return data.get_genre_by_id(genre_id=genre_id)
def get_session_by_token(token: str) -> Optional[dict]: """Gets a session using the given token. Args: token: An encoded JSON web token associated with a session in the fct_sessions table. Returns: A session with the given token else None. Raises: InvalidParamException: If the token is None. """ validate_params(func='get_session_by_token', params={'token': token}) return data.get_session_by_token(token=token)
def create_session(user_id: Union[str, uuid4], token: str) -> Optional[dict]: """Creates a new session in the fct_sessions table. Args: user_id: The primary key associated with a user entity in the dim_users table. token: An encoded JSON web token associated with the given user entity. Returns: A newly created session else None. Raises: InvalidParamException: If the user_id or token is None. """ validate_params(func='create_session', params={'user_id': user_id, 'token': token}) return data.create_session(user_id=user_id, token=token)
def delete_book_genre_by_id( book_genre_id: Union[str, uuid4]) -> Optional[dict]: """Deletes a book_genre from the table by the given id. Args: book_genre_id: The PK of a book_genre. Returns: A deleted book_genre with the given id else None. Raises: InvalidParamException: If the given book_genre_id is None. """ validate_params(func='delete_book_genre_by_id', params={'book_genre_id': book_genre_id}) return data.delete_book_genre_by_id(book_genre_id=book_genre_id)
def authorize_user( email: str, password: str, name: Optional[str] = None, ) -> Optional[dict]: """Authorizes a user with the users service using the given params. Args: email: The unique email associated with a user entity in the dim_users table. password: The plain string password associated with a user entity in the dim_users table. name: The full name of a new user. Returns: A dict containing an access token and auth results else None. Raises: InvalidParamException: If email or password is None. """ validate_params(func='authorize_user', params={'email': email, 'password': password}) user = ( create_user(name=name, email=email, password=password) if name else get_user_by_email(email=email) ) if not user: log.debug( f'Failed to GET user by email "{email}" from Users Service.' if not name else f'Failed to POST new user with name "{name}" and email "{email}".' ) return None is_user_validated = ( validate.validate_password(password=password, hashed=user.get('password')) if not name else True ) if not is_user_validated: log.debug( f'Failed to validate given plain text password "{password}" with the entity ' f'associated with the given email "{email}".' ) return None access_token, auth_results = encode.encode_json_web_token(user=user) log.info(user) create_session(user_id=user.get('id'), token=access_token) return {'access_token': access_token, 'auth_results': auth_results}
def create_book( user_id: Union[str, uuid4], author: str, title: str, image_key: Optional[str] = None, synopsis: Optional[str] = None, book_id: Optional[Union[str, uuid4]] = None ) -> Optional[dict]: """Creates a new book. Args: user_id: The FK to the users table. author: The author to associate with the new book. image_key: The image_key to associate with the new book. synopsis: The synopsis to associate with the new book. title: The title to associate with the new book. book_id: The PK to assign to the new book. Returns: A newly created book else None. Raises: InvalidParamException: If any of the given params are None. UniqueEntityException: If a title/user_id match already exists in the table. """ validate_params( func='create_book', params={'user_id': user_id, 'author': author, 'title': title} ) validate_entity_is_unique( func=data.get_book_by_title_and_user_id, title=title, user_id=user_id ) return data.create_book( book_status_id=INITIAL_BOOK_STATUS_ID, user_id=user_id, author=author, image_key=image_key, synopsis=synopsis, timestamp=datetime.utcnow().replace(tzinfo=pytz.utc), title=title, book_id=book_id )
def update_user_book_by_id( user_id: Union[str, uuid4], book_id: Union[str, uuid4], title: Optional[str], author: Optional[str], synopsis: Optional[str], image_key: Optional[str], book_status_id: Optional[Union[str, uuid4]], ) -> Optional[dict]: """Updates a user's book using the given parameters. Args: secondary_genre_ids: [description] user_id: The unique ID of the user creating the book. book_id: The unique ID associate with the book to update. title: The updated title of the new book. author: The updated name of the person writing the book. synopsis: An updated short summary of the book. image_key: The S3 object_key associated with the user's book cover. book_status_id: The updated status for the book. Returns: A book populated with the updated aggregated information else None. Raises: InvalidParamException: If any of the required parameters are None. UnauthorizedAccessException: If the user ID associated with the book does not match the user ID pulled from the request's JWT. """ log.info(f'Updating book with id "{book_id}".') validate_params(func='update_user_book_by_id', params={'user_id': user_id, 'book_id': book_id}) security.validate_user_book(user_id=user_id, book_id=book_id) updated_book = books_service.update_book( book_id=book_id, user_id=user_id, title=title, author=author, synopsis=synopsis, image_key=image_key, book_status_id=book_status_id ) return populate_genres_for_user_book(user_book=updated_book)
def delete_secondary_book_genre( user_id: Union[str, uuid4], book_id: Union[str, uuid4], book_genre_id: Union[str, uuid4] ) -> Optional[dict]: """Deletes the secondary genre from the given book. Args: user_id: The unique ID of the user pulled off of the authorized JWT. book_id: The unique ID of the book to tie the new secondary genre to. book_genre_id: The unique ID of the book genre to delete from the book. Returns: The deleted book genre else None. Raises: InvalidParamException: If any of the required parameters are None. UnauthorizedAccessException: If the user ID associated with the book does not match the user ID pulled from the request's JWT. """ log.info( f'Deleting secondary book genre with id "{book_genre_id}" from book with id "{book_id}".' ) validate_params( func='delete_secondary_book_genre', params={ 'user_id': user_id, 'book_id': book_id, 'book_genre_id': book_genre_id } ) security.validate_user_book(user_id=user_id, book_id=book_id) book_genre = book_genres_service.get_book_genre_by_id(book_genre_id=book_genre_id) deleted_book_genre = book_genres_service.delete_book_genre_by_id( book_genre_id=book_genre_id ) genre_id = book_genre.get('genre').get('id') if not book_genres_service.get_book_genres_by_genre_id(genre_id=genre_id): genres_service.delete_genre_by_id(genre_id=genre_id) return deleted_book_genre
def get_user_books(user_id: Union[str, uuid4]) -> list: """Gets all of the books associated with the user ID authorized using the given JWT. Args: user_id: The unique user ID associated with the JWT authorized in the request. Returns: A list of all books associated with the given user ID else an empty list. Raises: InvalidParamException: If the user ID is None. """ log.info(f'Retrieving aggregated book data for user with id "{user_id}".') validate_params(func='get_user_books', params={'user_id': user_id}) return [ populate_genres_for_user_book(user_book=user_book) for user_book in books_service.get_books(user_id=user_id) ]
def update_book( book_id: Union[str, uuid4], user_id: Union[str, uuid4], title: Optional[str] = None, author: Optional[str] = None, synopsis: Optional[str] = None, image_key: Optional[str] = None, book_status_id: Optional[Union[str, uuid4]] = None ) -> Optional[dict]: """Updates a book by a given id. Args: book_id: The PK of a book. title: The title to modify in the book with the given id. author: The author to modify in the book with the given id. synopsis: The synopsis to modify in the book with the given id. image_key: The image_key to modify in the book with the given id. book_status_id: The FK to the book_status table. Returns: An updated book with the given id else None. Raises: InvalidParamException: If any of the given params are None. UniqueEntityException: If a title/user_id match already exists in the table. """ validate_params(func='update_book', params={'book_id': book_id}) if title: validate_entity_is_unique( func=data.get_book_by_title_and_user_id, title=title, user_id=user_id ) return data.update_book( book_id=book_id, title=title, author=author, synopsis=synopsis, image_key=image_key, book_status_id=book_status_id )
def delete_user_books(user_id: Union[str, uuid4]) -> list: """Deletes all of the books associated with the user_id pulled from the authorized JWT. Args: user_id: The unique ID of the user pulled off of the authorized JWT. Returns: A list of all of the books that were deleted. Raises: InvalidParamException: If the given user ID is None. """ log.info(f'Deleting all books for user with id "{user_id}".') validate_params(func='delete_user_books', params={'user_id': user_id}) user_books = books_service.get_books(user_id) for user_book in user_books: book_genres = book_genres_service.get_book_genres_by_book_id(book_id=user_book.get('id')) for book_genre in book_genres: book_genres_service.delete_book_genre_by_id(book_genre_id=book_genre.get('id')) genres_service.delete_genres(user_id=user_id) return books_service.delete_books(user_id=user_id)
def authenticate_user( email: str, password: str, json_web_token: str ) -> Optional[dict]: """Authenticates a user's json web token with the given parameters. Args: email: The unique email associated with a user entity in the dim_users table. password: The plain string password associated with a user entity in the dim_users table. json_web_token: The token pulled from the request. Returns: A dict containing the decoded json web token. Raises: InvalidParamException: If any of the given params is None. """ validate_params( func='authenticate_user', params={'email': email, 'password': password, 'json_web_token': json_web_token} ) user = get_user_by_email(email=email) if not user: log.debug(f'Failed to GET user by email "{email}" from Users Service.') return None if not password == user.get('password'): log.debug( f'Failed to validate given plain text password "{password}" with the entity ' f'associated with the given email "{email}".' ) return None return decode.decode_json_web_token( json_web_token=json_web_token, secret=user.get('password'), )
def refresh_authorization(email: str) -> Optional[dict]: """Refreshes the authenication token for a user session with the given email. Args: email: The unique email associated with a session entity in the fct_sessions table. Returns: A dict containing an access token and auth results else None. Raises: InvalidParamException: If the given email is None. """ validate_params(func='refresh_authorization', params={'email': email}) user = get_user_by_email(email=email) if not user: log.debug(f'Failed to GET user by email "{email}" from Users Service.') return None access_token, auth_results = encode.encode_json_web_token(user=user) create_session(user_id=user.get('id'), token=access_token) return {'access_token': access_token, 'auth_results': auth_results}
def create_genre( user_id: Union[str, uuid4], display_name: str, name: str, genre_id: Optional[Union[str, uuid4]] = None) -> Optional[dict]: """Creates a new genre. Args: user_id: The FK to the users table. display_name: The display_name to associate with the new genre. name: The name to associate with the new genre. genre_id: The PK to assign to the new genre. Returns: A newly created genre else None. Raises: InvalidParamException: If any of the given params are None. UniqueEntityException: If a matching genre is found by the display name and/or user_id. """ validate_params(func='create_genre', params={ 'user_id': user_id, 'display_name': display_name, 'name': name }) validate_entity_is_unique(func=data.get_genre_by_display_name_and_user_id, display_name=display_name, user_id=user_id) return data.create_genre(user_id=user_id, bucket_name=None, display_name=display_name, is_primary=False, name=name, genre_id=genre_id)
def update_user_book_genre_by_id( user_id: Union[str, uuid4], book_id: Union[str, uuid4], genre_name: str, book_genre_id: Union[str, uuid4] ) -> Optional[dict]: """Updates a book genre with the given ID using the params passed in the body of the request. Args: user_id: The unique ID of the user pulled off of the authorized JWT. book_id: The unique ID of the book associated with the genre being updated. genre_name: The unique name of the new genre to associate with the given book genre. book_genre_id: The unique ID of the book genre to update. Returns: An updated book genre else None. Raises: InvalidParamException: If any of the given parameters are None. UnauthorizedAccessException: If the user ID associated with the book does not match the user ID pulled from the request's JWT. """ log.info( f'Updating book genre with id "{book_genre_id}" to be associated with "{genre_name}".' ) validate_params( func='update_user_book_genre_by_id', params={ 'user_id': user_id, 'book_id': book_id, 'genre_name': genre_name, 'book_genre_id': book_genre_id } ) security.validate_user_book(user_id=user_id, book_id=book_id) original_book_genre = book_genres_service.get_book_genre_by_id(book_genre_id) original_genre = genres_service.get_genre_by_id( genre_id=original_book_genre.get('genre', {}).get('id') ) original_genre_id = original_genre.get('id') existing_genre = genres_service.get_genre_by_display_name_and_user_id( display_name=genre_name, user_id=user_id, ) if not existing_genre: log.info(f'Creating new genre "{genre_name}" tied to user id: "{user_id}"') new_genre = genres_service.create_genre( user_id=user_id, display_name=genre_name, name=genre_name ) updated_book_genre = book_genres_service.update_book_genre( genre_id=existing_genre.get('id') if existing_genre else new_genre.get('id'), book_genre_id=book_genre_id ) if not book_genres_service.get_book_genres_by_genre_id(genre_id=original_genre_id): genres_service.delete_genre_by_id(genre_id=original_genre_id) return book_genres_service.get_book_genre_by_id(updated_book_genre.get('id'))
def create_user_book( user_id: Union[str, uuid4], author: str, title: str, primary_genre_id: Union[str, uuid4], image_key: Optional[str], synopsis: Optional[str], secondary_genre_names: Optional[list], book_id: Optional[Union[str, uuid4]] ) -> Optional[dict]: """Creates a new book associated with the user using the given body parameters. Args: user_id: The unique ID of the user creating the book. author: The name of the person writing the book. title: The title of the new book. primary_genre_id: The unique ID of the primary genre for the book. image_key: The S3 object_key associated with the user's book cover. synopsis: A short summary of the book. secondary_genre_names: A list of names of secondary genres to associate with the new book. book_id: Optional unique ID to associate with the new book. Returns: A dict containing aggregated information for the newly created book else None. Raises: InvalidParamException: If any of the required parameters are None. """ log.info(f'Creating new book "{title}" for user with id "{user_id}".') validate_params( func='create_user_book', params={ 'user_id': user_id, 'author': author, 'title': title, 'primary_genre_id': primary_genre_id } ) new_book = books_service.create_book( user_id=user_id, author=author, title=title, image_key=image_key, synopsis=synopsis, book_id=book_id ) primary_book_genre = book_genres_service.create_book_genre( book_id=new_book.get('id'), genre_id=primary_genre_id ) secondary_book_genres = [] for secondary_genre_name in secondary_genre_names: existing_genre = genres_service.get_genre_by_display_name_and_user_id( display_name=secondary_genre_name, user_id=user_id, ) if not existing_genre: log.info(f'Creating new genre "{secondary_genre_name}" tied to user id: "{user_id}"') new_genre = genres_service.create_genre( user_id=user_id, display_name=secondary_genre_name, name=secondary_genre_name ) log.info(f'Tying genre "{secondary_genre_name}" to new book "{new_book.get("title")}"') secondary_book_genres.append( book_genres_service.create_book_genre( book_id=new_book.get('id'), genre_id=existing_genre.get('id') if existing_genre else new_genre.get('id') ) ) new_book.update({ 'primary_genre': ( book_genres_service.get_book_genre_by_id(primary_book_genre.get('id')) ), 'secondary_genres': [ book_genres_service.get_book_genre_by_id(secondary_book_genre.get('id')) for secondary_book_genre in secondary_book_genres ] }) return new_book