diff --git a/api/main.py b/api/main.py index 96b62f4..ac4d5e9 100644 --- a/api/main.py +++ b/api/main.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, HTTPException, status -from schema.book import Book, BookCreate +from schema.book import Book, BookCreate, Genre +from schema.review import Review app = FastAPI() @@ -17,11 +18,15 @@ def get_next_book_id() -> int: books.append( Book( + id=get_next_book_id(), title="Shogun", author="James Clavell", publication_year=1975, - rating=10, - id=get_next_book_id(), + genre=Genre.historical_fiction, + reviews=[ + Review(submitter="Noah", rating=4), + Review(submitter="Asta", rating=5), + ], ) ) @@ -52,6 +57,16 @@ async def get_book(book_id: int) -> Book: return ret +@app.get("/books/byauthor/{author_last_name}") +async def get_books_by_author(author_last_name: str) -> list[Book]: + return (book for book in books if author_last_name in book.author) + + +@app.get("/books/byyear/{publication_year}") +async def get_books_by_year(publication_year: int) -> list[Book]: + return (book for book in books if book.publication_year == publication_year) + + # TODO Add endpoint(s) for GET-ing books by author/publication year here # What should the endpoint(s) be named? # Will it/they return one or potentially several books? @@ -68,6 +83,42 @@ async def create_book(book: BookCreate) -> Book: return new_book +# TODO make "find book by ID or raise exception" logic reusable +# TODO enforce review submitter name uniqueness +@app.post( + "/books/review/{book_id}", + status_code=status.HTTP_201_CREATED, +) +async def review_book(book_id: int, review: Review) -> Book: + book_to_review = next((book for book in books if book.id == book_id), None) + if not book_to_review: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Book with ID {book_id} does not exist", + ) + book_to_review.reviews.append(review) + return book_to_review + + +# TODO raise exception when review by submitter does not exist +# TODO raise separate exception if no reviews exist +@app.delete("/books/review/{book_id}/{submitter}") +async def delete_review(book_id: int, submitter: str) -> Review: + book_to_review = next((book for book in books if book.id == book_id), None) + if not book_to_review: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Book with ID {book_id} does not exist", + ) + review_to_delete = next( + (review for review in book_to_review.reviews if review.submitter == submitter), + None, + ) + if review_to_delete: + book_to_review.reviews.remove(review_to_delete) + return review_to_delete + + @app.put( "/books/{book_id}", ) @@ -76,8 +127,9 @@ async def update_book(book: BookCreate, book_id: int) -> Book: if book_to_update: book_to_update.title = book.title book_to_update.author = book.author + book_to_update.genre = book.genre book_to_update.publication_year = book.publication_year - book_to_update.rating = book.rating + book_to_update.reviews = book.reviews else: book_to_update = Book.from_base(book, book_id) books.append(book_to_update) diff --git a/api/schema/book.py b/api/schema/book.py index ed660a6..99ed01d 100644 --- a/api/schema/book.py +++ b/api/schema/book.py @@ -1,18 +1,29 @@ from enum import Enum from typing import Optional +from schema.review import Review +from statistics import mean -from pydantic import BaseModel +from pydantic import BaseModel, computed_field + + +class Genre(str, Enum): + scifi = "Science Fiction" + biography = "Biography" + fantasy = "Fantasy" + historical_fiction = "Historical Fiction" class BookBase(BaseModel): title: str author: str + genre: Optional[Genre] publication_year: Optional[int] = None - rating: Optional[int] = None + reviews: list[Review] = [] - # TODO - # Add a 'genre' field here. You'll need to add it in a few other places as well! - # Bonus: try implementing genre as an enum rather than a string + @computed_field + @property + def average_rating(self) -> float: + return round(mean(review.rating for review in self.reviews), 2) class BookCreate(BookBase): @@ -27,11 +38,7 @@ def from_base(base: BookBase, id: int): id=id, title=base.title, author=base.author, + genre=base.genre, publication_year=base.publication_year, - rating=base.rating, + reviews=base.reviews, ) - - -# TODO You may need to create new class(es) inheriting BaseModel in order to implement the 'multiple ratings' feature. -# BaseModel is from Pydantic - see docs at https://docs.pydantic.dev/latest/concepts/models/ -# Think carefully about how to structure the classes and endpoints for this feature! diff --git a/api/schema/review.py b/api/schema/review.py new file mode 100644 index 0000000..5268c8d --- /dev/null +++ b/api/schema/review.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +# Very simple schema for Review +# No constraints on rating int +# Assume unique submitter name +class Review(BaseModel): + submitter: str + rating: int