diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..9804300a --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +inline-quotes = " +ignore = E203, E266, W503, ANN002, ANN003, ANN101, ANN102, ANN401, N807, N818, VNE001, F401 +max-line-length = 119 +max-complexity = 18 +select = B,C,E,F,W,T4,B9,ANN,Q0,N8,VNE +exclude = .venv, __pycache__, migrations, alembic \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7ed07d08..c82bb5be 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ __pycache__/ # C extensions *.so - # Distribution / packaging .Python build/ diff --git a/crud.py b/crud.py new file mode 100644 index 00000000..891613f6 --- /dev/null +++ b/crud.py @@ -0,0 +1,82 @@ +from fastapi import HTTPException +from sqlalchemy import select +from sqlalchemy.orm import Session +from typing import Sequence +import schemas +from db import models + + +def get_all_books( + db: Session, + author_id: int | None = None, + skip: int = 0, + limit: int = 10, +) -> Sequence[models.Book]: + + query = select(models.Book) + + if author_id is not None: + query = query.where(models.Book.author_id == author_id) + + query = query.offset(skip).limit(limit) + + return db.scalars(query).all() + + +def get_author_by_id( + db: Session, + author_id: int +) -> models.Author | None: + author = db.execute( + select(models.Author).where(models.Author.id == author_id) + ).scalar_one_or_none() + if not author: + raise HTTPException(status_code=404, detail="Author not found") + return author + + +def create_book( + db: Session, + book: schemas.BookCreateSchema +) -> models.Book: + author = db.execute( + select(models.Author).where(models.Author.id == book.author_id) + ).scalar_one_or_none() + + if not author: + raise HTTPException(status_code=404, detail="Author not found") + db_book = models.Book( + title=book.title, + summary=book.summary, + publication_date=book.publication_date, + author_id=book.author_id + ) + db.add(db_book) + db.commit() + db.refresh(db_book) + + return db_book + + +def create_author( + db: Session, + author: schemas.AuthorSchemaBase +) -> models.Author: + + db_author = models.Author( + name=author.name, + bio=author.bio, + ) + db.add(db_author) + db.commit() + db.refresh(db_author) + + return db_author + + +def get_authors_list(db, skip: int, limit: int) -> list[models.Author]: + return db.scalars( + select(models.Author) + .offset(skip) + .limit(limit) + ).all() diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/db/database.py b/db/database.py new file mode 100644 index 00000000..f3863814 --- /dev/null +++ b/db/database.py @@ -0,0 +1,12 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./db.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/db/models.py b/db/models.py new file mode 100644 index 00000000..54c4ca1c --- /dev/null +++ b/db/models.py @@ -0,0 +1,31 @@ +import datetime +from sqlalchemy import String, Date, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship + +from db.database import Base + + +class Author(Base): + __tablename__ = "authors" + + id: Mapped[int] = mapped_column(primary_key=True, index=True) + name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) + bio: Mapped[str] = mapped_column(String(512), nullable=False) + books: Mapped[list["Book"]] = relationship( + "Book", back_populates="author" + ) + + +class Book(Base): + __tablename__ = "books" + + id: Mapped[int] = mapped_column(primary_key=True, index=True) + title: Mapped[str] = mapped_column(String(255), nullable=False) + summary: Mapped[str] = mapped_column(String(255), nullable=False) + publication_date: Mapped[datetime.date] = mapped_column(Date, nullable=False) + author_id: Mapped[int] = mapped_column( + ForeignKey("authors.id"), nullable=False + ) + author: Mapped["Author"] = relationship( + "Author", back_populates="books" + ) diff --git a/main.py b/main.py new file mode 100644 index 00000000..ade388b6 --- /dev/null +++ b/main.py @@ -0,0 +1,72 @@ +from typing import Annotated, Generator +from fastapi import Depends +from sqlalchemy.orm import Session +import crud +import schemas +from db.database import SessionLocal +from fastapi import FastAPI +from db.database import engine +from db.models import Base + + +app = FastAPI() +Base.metadata.create_all(bind=engine) + + +def get_db() -> Generator[Session, None, None]: + db = SessionLocal() + + try: + yield db + finally: + db.close() + + +@app.get("/books/", response_model=list[schemas.BookListSchema]) +def read_cheese_types( + db: Annotated[Session, Depends(get_db)], + author_id: int | None = None, + skip: int | None = None, + limit: int | None = None, +): + return crud.get_all_books( + db=db, + author_id=author_id, + skip=skip, + limit=limit + ) + + +@app.post("/books/", response_model=schemas.BookSchemaBase) +def create_book_route( + book: schemas.BookCreateSchema, + db: Session = Depends(get_db) +): + return crud.create_book(db=db, book=book) + + +@app.get("/authors/", response_model=list[schemas.AuthorListSchema]) +def get_authors( + db: Annotated[Session, Depends(get_db)], + skip: int | None = None, + limit: int | None = None, +): + return crud.get_authors_list(db=db, skip=skip, limit=limit) + + +@app.post("/authors/", response_model=schemas.AuthorSchema) +def create_author_route( + author: schemas.AuthorSchemaBase, + db: Session = Depends(get_db) +): + return crud.create_author(db=db, author=author) + + +@app.get("/authors/{author_id}/", response_model=schemas.AuthorListSchema) +def get_authors_by_author_id( + db: Annotated[Session, Depends(get_db)], + author_id: int, +): + return crud.get_author_by_id( + db=db, author_id=author_id + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..d78fa26c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi +sqlalchemy +alembic +uvicorn \ No newline at end of file diff --git a/schemas.py b/schemas.py new file mode 100644 index 00000000..6d3c75df --- /dev/null +++ b/schemas.py @@ -0,0 +1,40 @@ +import datetime +from pydantic import BaseModel + + +class BookSchemaBase(BaseModel): + title: str + summary: str + publication_date: datetime.date + author_id: int + + class Config: + from_attributes = True + + +class BookCreateSchema(BookSchemaBase): + pass + + +class BookListSchema(BookSchemaBase): + id: int + + +class AuthorSchemaBase(BaseModel): + name: str + bio: str + + class Config: + from_attributes = True + + +class AuthorSchema(AuthorSchemaBase): + id: int + + class Config: + from_attributes = True + + +class AuthorListSchema(AuthorSchemaBase): + id: int + books: list[BookListSchema] = []