From 2ec1ead8d96d7b774b1663c46c082bdec86785e5 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 18 Apr 2024 19:30:39 -0400 Subject: [PATCH 1/2] Did first two simple exercises --- api/main.py | 15 ++++++++++++++- api/schema/book.py | 13 +++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/api/main.py b/api/main.py index 96b62f4..c368e2d 100644 --- a/api/main.py +++ b/api/main.py @@ -1,5 +1,5 @@ from fastapi import FastAPI, HTTPException, status -from schema.book import Book, BookCreate +from schema.book import Book, BookCreate, Genre app = FastAPI() @@ -20,6 +20,7 @@ def get_next_book_id() -> int: title="Shogun", author="James Clavell", publication_year=1975, + genre=Genre.historical_fiction, rating=10, id=get_next_book_id(), ) @@ -51,6 +52,17 @@ 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? @@ -76,6 +88,7 @@ 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 else: diff --git a/api/schema/book.py b/api/schema/book.py index ed660a6..6524820 100644 --- a/api/schema/book.py +++ b/api/schema/book.py @@ -4,16 +4,20 @@ from pydantic import BaseModel +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 - # 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 - class BookCreate(BookBase): pass @@ -27,6 +31,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, ) From 711bb5fefd105740244df7aeb4013d8b485e7a65 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 18 Apr 2024 19:52:19 -0400 Subject: [PATCH 2/2] Add/delete reviews - not my best work ;) --- api/main.py | 57 +++++++++++++++++++++++++++++++++++++------- api/schema/book.py | 18 +++++++------- api/schema/review.py | 9 +++++++ 3 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 api/schema/review.py diff --git a/api/main.py b/api/main.py index c368e2d..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, Genre +from schema.review import Review app = FastAPI() @@ -17,12 +18,15 @@ def get_next_book_id() -> int: books.append( Book( + id=get_next_book_id(), title="Shogun", author="James Clavell", publication_year=1975, genre=Genre.historical_fiction, - rating=10, - id=get_next_book_id(), + reviews=[ + Review(submitter="Noah", rating=4), + Review(submitter="Asta", rating=5), + ], ) ) @@ -52,18 +56,17 @@ async def get_book(book_id: int) -> Book: ) return ret -@app.get( - "/books/byauthor/{author_last_name}" -) + +@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}" -) + +@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? @@ -80,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}", ) @@ -90,7 +129,7 @@ async def update_book(book: BookCreate, book_id: int) -> Book: 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 6524820..99ed01d 100644 --- a/api/schema/book.py +++ b/api/schema/book.py @@ -1,7 +1,9 @@ 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): @@ -16,7 +18,12 @@ class BookBase(BaseModel): author: str genre: Optional[Genre] publication_year: Optional[int] = None - rating: Optional[int] = None + reviews: list[Review] = [] + + @computed_field + @property + def average_rating(self) -> float: + return round(mean(review.rating for review in self.reviews), 2) class BookCreate(BookBase): @@ -33,10 +40,5 @@ def from_base(base: BookBase, id: int): 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