Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quick attempt at the first tier exercises #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions api/main.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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),
],
)
)

Expand Down Expand Up @@ -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?
Expand All @@ -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}",
)
Expand All @@ -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)
Expand Down
29 changes: 18 additions & 11 deletions api/schema/book.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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!
9 changes: 9 additions & 0 deletions api/schema/review.py
Original file line number Diff line number Diff line change
@@ -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