diff --git a/.gitignore b/.gitignore index 7ed07d08..70d06276 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ coverage.xml local_settings.py db.sqlite3 db.sqlite3-journal +library.db # Flask stuff: instance/ @@ -128,3 +129,5 @@ dmypy.json # Pyre type checker .pyre/ .idea/ + +.vscode/ \ No newline at end of file diff --git a/crud.py b/crud.py new file mode 100644 index 00000000..9010227b --- /dev/null +++ b/crud.py @@ -0,0 +1,82 @@ +from sqlalchemy.orm import Session +import models +from fastapi import HTTPException +import schemas + + +def get_book_by_id(book_id: int, db: Session): + return db.query(models.Book).filter(models.Book.id == book_id).first() + + +def get_book_by_title(title: str, db: Session): + return db.query(models.Book).filter(models.Book.title == title).first() + + +def create_book(author_id: int, book: schemas.BookCreate, db: Session): + if not get_author_by_id(author_id, db): + raise HTTPException(status_code=404, detail="Author not found") + db_book = models.Book(**book.model_dump(), author_id=author_id) + db.add(db_book) + db.commit() + db.refresh(db_book) + return db_book + + +def get_author_by_id(author_id: int, db: Session): + return db.query(models.Author).filter(models.Author.id == author_id).first() + + +def get_author_by_name(name: str, db: Session): + return db.query(models.Author).filter(models.Author.name == name).first() + + +def create_author(author: schemas.AuthorCreate, db: Session): + if get_author_by_name(author.name, db): + raise HTTPException( + status_code=400, detail="Author with this name already exists" + ) + db_author = models.Author(**author.model_dump()) + db.add(db_author) + db.commit() + db.refresh(db_author) + return db_author + + +def get_all_books(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.Book).offset(skip).limit(limit).all() + + +def get_books_by_author(author_id: int, db: Session, skip: int = 0, limit: int = 100): + return ( + db.query(models.Book) + .filter(models.Book.author_id == author_id) + .offset(skip) + .limit(limit) + .all() + ) + + +def get_all_authors(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.Author).offset(skip).limit(limit).all() + + +def get_authors_with_books(db: Session): + return db.query(models.Author).filter(models.Author.books.any()).all() + + +def delete_book(book_id: int, db: Session): + book = get_book_by_id(book_id, db) + if not book: + raise HTTPException(status_code=404, detail="Book not found") + db.delete(book) + db.commit() + return {"detail": "Book deleted"} + + +def delete_author(author_id: int, db: Session): + author = get_author_by_id(author_id, db) + if not author: + raise HTTPException(status_code=404, detail="Author not found") + db.delete(author) + db.commit() + return {"detail": "Author deleted"} diff --git a/database.py b/database.py new file mode 100644 index 00000000..be27e8d6 --- /dev/null +++ b/database.py @@ -0,0 +1,14 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + + +SQLALCHEMY_DATABASE_URL = "sqlite:///./library.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/main.py b/main.py new file mode 100644 index 00000000..3741c1cf --- /dev/null +++ b/main.py @@ -0,0 +1,74 @@ +from sqlalchemy.orm import Session +from fastapi import FastAPI, Depends, HTTPException + +from database import SessionLocal, engine +import models +import schemas +import crud + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.post("/authors", response_model=schemas.AuthorResponse) +def create_author(author: schemas.AuthorCreate, db: Session = Depends(get_db)): + return crud.create_author(author, db) + + +@app.get("/authors", response_model=list[schemas.AuthorResponse]) +def read_authors(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + return crud.get_all_authors(db, skip=skip, limit=limit) + + +@app.get("/authors/{author_id}", response_model=schemas.AuthorResponse) +def read_author(author_id: int, db: Session = Depends(get_db)): + db_author = crud.get_author_by_id(author_id, db) + if db_author is None: + raise HTTPException(status_code=404, detail="Author not found") + return db_author + + +@app.delete("/authors/{author_id}") +def delete_author(author_id: int, db: Session = Depends(get_db)): + return crud.delete_author(author_id, db) + + +@app.post("/authors/{author_id}/books", response_model=schemas.BookResponse) +def create_book( + book: schemas.BookCreate, author_id: int, db: Session = Depends(get_db) +): + return crud.create_book(author_id, book, db) + + +@app.get("/books", response_model=list[schemas.BookResponse]) +def read_books( + db: Session = Depends(get_db), + author_id: int | None = None, + skip: int = 0, + limit: int = 100, +): + if author_id is not None: + return crud.get_books_by_author(author_id, db, skip=skip, limit=limit) + return crud.get_all_books(db, skip=skip, limit=limit) + + +@app.get("/books/{book_id}", response_model=schemas.BookResponse) +def read_book(book_id: int, db: Session = Depends(get_db)): + db_book = crud.get_book_by_id(book_id, db) + if db_book is None: + raise HTTPException(status_code=404, detail="Book not found") + return db_book + + +@app.delete("/books/{book_id}") +def delete_book(book_id: int, db: Session = Depends(get_db)): + return crud.delete_book(book_id, db) diff --git a/models.py b/models.py new file mode 100644 index 00000000..d07df71b --- /dev/null +++ b/models.py @@ -0,0 +1,24 @@ +from sqlalchemy import String, ForeignKey, Date +from sqlalchemy.orm import relationship, Mapped, mapped_column +from datetime import date +from database import Base + + +class Author(Base): + __tablename__ = "authors" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(255), unique=True) + bio: Mapped[str] = mapped_column(String) + books: Mapped[list["Book"]] = relationship("Book", back_populates="author") + + +class Book(Base): + __tablename__ = "books" + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(255)) + summary: Mapped[str] = mapped_column(String) + publication_date: Mapped[date] = mapped_column(Date) + author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"), index=True) + author: Mapped["Author"] = relationship(back_populates="books") diff --git a/schemas.py b/schemas.py new file mode 100644 index 00000000..56009528 --- /dev/null +++ b/schemas.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, ConfigDict +from datetime import date + + +class BookBase(BaseModel): + title: str + summary: str + publication_date: date + + +class BookCreate(BookBase): + pass + + +class BookResponse(BookBase): + id: int + author_id: int + + model_config = ConfigDict(from_attributes=True) + + +class AuthorBase(BaseModel): + name: str + bio: str + + +class AuthorCreate(AuthorBase): + pass + + +class AuthorResponse(AuthorBase): + id: int + books: list[BookResponse] = [] + + model_config = ConfigDict(from_attributes=True)