diff --git a/README.md b/README.md index 232065c..78cb292 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,11 @@ For run tests: | GET | `/books/{id}/authors` | Retrieve a list of authors for a specific book | **Relationships** -| Method | Endpoint | Description | -|--------|-----------------------|------------------------------------------------| -| GET | `/relationships` | Retrieve a list of all relationships | -| POST | `/relationships` | Add author-book relationship | -| DELETE | `/relationships` | Remove author-book relationship | +| Method | Endpoint | Description | +|--------|------------------------------|-----------------------------------------| +| GET | `/relationships/author-book` | Retrieve a list of all relationships | +| POST | `/relationships/author-book` | Add author-book relationship | +| DELETE | `/relationships/author-book` | Remove author-book relationship | ### **Technologies Used** diff --git a/library_service/models/db/__init__.py b/library_service/models/db/__init__.py index dd6849e..5e32583 100644 --- a/library_service/models/db/__init__.py +++ b/library_service/models/db/__init__.py @@ -1,14 +1,17 @@ from .author import Author from .book import Book +from .genre import Genre from .links import ( AuthorBookLink, GenreBookLink, AuthorWithBooks, BookWithAuthors, - GenreWithBooks, BookWithAuthorsAndGenres + GenreWithBooks, BookWithGenres, + BookWithAuthorsAndGenres ) __all__ = [ - 'Author', 'Book', + 'Author', 'Book', 'Genre', 'AuthorBookLink', 'AuthorWithBooks', 'BookWithAuthors', 'GenreBookLink', - 'GenreWithBooks', 'BookWithAuthorsAndGenres' + 'GenreWithBooks', 'BookWithGenres', + 'BookWithAuthorsAndGenres' ] diff --git a/library_service/models/db/genre.py b/library_service/models/db/genre.py index f114532..aca8baf 100644 --- a/library_service/models/db/genre.py +++ b/library_service/models/db/genre.py @@ -9,6 +9,6 @@ if TYPE_CHECKING: class Genre(GenreBase, table=True): id: Optional[int] = Field(default=None, primary_key=True, index=True) books: List["Book"] = Relationship( - back_populates="authors", + back_populates="genres", link_model=GenreBookLink ) diff --git a/library_service/routers/genres.py b/library_service/routers/genres.py new file mode 100644 index 0000000..15107ce --- /dev/null +++ b/library_service/routers/genres.py @@ -0,0 +1,80 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import Session, select + +from library_service.settings import get_session +from library_service.models.db import Genre, GenreBookLink, Book, GenreWithBooks +from library_service.models.dto import ( + GenreCreate, GenreUpdate, GenreRead, + GenreList, BookRead +) + +router = APIRouter(prefix="/genres", tags=["genres"]) + +# Create a genre +@router.post("/", response_model=GenreRead) +def create_genre(genre: GenreCreate, session: Session = Depends(get_session)): + db_genre = Genre(**genre.model_dump()) + session.add(db_genre) + session.commit() + session.refresh(db_genre) + return GenreRead(**db_genre.model_dump()) + +# Read genres +@router.get("/", response_model=GenreList) +def read_genres(session: Session = Depends(get_session)): + genres = session.exec(select(Genre)).all() + return GenreList( + genres=[GenreRead(**genre.model_dump()) for genre in genres], + total=len(genres) + ) + +# Read a genre with their books +@router.get("/{genre_id}", response_model=GenreWithBooks) +def get_genre(genre_id: int, session: Session = Depends(get_session)): + genre = session.get(Genre, genre_id) + if not genre: + raise HTTPException(status_code=404, detail="Genre not found") + + books = session.exec( + select(Book) + .join(GenreBookLink) + .where(GenreBookLink.genre_id == genre_id) + ).all() + + book_reads = [BookRead(**book.model_dump()) for book in books] + + genre_data = genre.model_dump() + genre_data['books'] = book_reads + + return GenreWithBooks(**genre_data) + +# Update a genre +@router.put("/{genre_id}", response_model=GenreRead) +def update_genre( + genre_id: int, + genre: GenreUpdate, + session: Session = Depends(get_session) +): + db_genre = session.get(Genre, genre_id) + if not db_genre: + raise HTTPException(status_code=404, detail="Genre not found") + + update_data = genre.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_genre, field, value) + + session.commit() + session.refresh(db_genre) + return GenreRead(**db_genre.model_dump()) + +# Delete a genre +@router.delete("/{genre_id}", response_model=GenreRead) +def delete_genre(genre_id: int, session: Session = Depends(get_session)): + genre = session.get(Genre, genre_id) + if not genre: + raise HTTPException(status_code=404, detail="Genre not found") + + genre_read = GenreRead(**genre.model_dump()) + session.delete(genre) + session.commit() + return genre_read diff --git a/library_service/routers/relationships.py b/library_service/routers/relationships.py index bdbf8e4..8006617 100644 --- a/library_service/routers/relationships.py +++ b/library_service/routers/relationships.py @@ -3,13 +3,13 @@ from sqlmodel import Session, select from typing import List, Dict from library_service.settings import get_session -from library_service.models.db import Book, Author, AuthorBookLink -from library_service.models.dto import AuthorRead, BookRead +from library_service.models.db import Author, Book, Genre, AuthorBookLink, GenreBookLink +from library_service.models.dto import AuthorRead, BookRead, GenreRead router = APIRouter(tags=["relations"]) # Add author to book -@router.post("/relationships", response_model=AuthorBookLink) +@router.post("/relationships/author-book", response_model=AuthorBookLink) def add_author_to_book(author_id: int, book_id: int, session: Session = Depends(get_session)): author = session.get(Author, author_id) if not author: @@ -35,7 +35,7 @@ def add_author_to_book(author_id: int, book_id: int, session: Session = Depends( return link # Remove author from book -@router.delete("/relationships", response_model=Dict[str, str]) +@router.delete("/relationships/author-book", response_model=Dict[str, str]) def remove_author_from_book(author_id: int, book_id: int, session: Session = Depends(get_session)): link = session.exec( select(AuthorBookLink) @@ -51,7 +51,7 @@ def remove_author_from_book(author_id: int, book_id: int, session: Session = Dep return {"message": "Relationship removed successfully"} # Get relationships -@router.get("/relationships", response_model=List[AuthorBookLink]) +@router.get("/relationships/author-book", response_model=List[AuthorBookLink]) def get_relationships(session: Session = Depends(get_session)): relationships = session.exec(select(AuthorBookLink)).all() return relationships diff --git a/library_service/settings.py b/library_service/settings.py index 5177490..7ae2ef8 100644 --- a/library_service/settings.py +++ b/library_service/settings.py @@ -24,6 +24,10 @@ def get_app() -> FastAPI: "name": "books", "description": "Operations with books.", }, + { + "name": "genres", + "description": "Operations with genres.", + }, { "name": "relations", "description": "Operations with relations.", diff --git a/migrations/versions/9d7a43ac5dfc_genres.py b/migrations/versions/9d7a43ac5dfc_genres.py new file mode 100644 index 0000000..3056f1a --- /dev/null +++ b/migrations/versions/9d7a43ac5dfc_genres.py @@ -0,0 +1,45 @@ +"""genres + +Revision ID: 9d7a43ac5dfc +Revises: d266fdc61e99 +Create Date: 2025-06-25 11:24:30.229418 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '9d7a43ac5dfc' +down_revision: Union[str, None] = 'd266fdc61e99' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('genre', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_genre_id'), 'genre', ['id'], unique=False) + op.create_table('genrebooklink', + sa.Column('genre_id', sa.Integer(), nullable=False), + sa.Column('book_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['book_id'], ['book.id'], ), + sa.ForeignKeyConstraint(['genre_id'], ['genre.id'], ), + sa.PrimaryKeyConstraint('genre_id', 'book_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('genrebooklink') + op.drop_index(op.f('ix_genre_id'), table_name='genre') + op.drop_table('genre') + # ### end Alembic commands ### diff --git a/tests/test_misc.py b/tests/test_misc.py index d5ddb19..2479fda 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,7 +6,8 @@ from fastapi.testclient import TestClient from sqlmodel import select, delete, Session from library_service.main import app, engine -from library_service.models.db import Author, Book, AuthorBookLink +from library_service.models.db import Author, Book, Genre +from library_service.models.db import AuthorBookLink, GenreBookLink client = TestClient(app) @@ -16,7 +17,9 @@ def setup_database(): with Session(engine) as session: original_authors = session.exec(select(Author)).all() original_books = session.exec(select(Book)).all() - original_links = session.exec(select(AuthorBookLink)).all() + original_genres = session.exec(select(Genre)).all() + original_author_book_links = session.exec(select(AuthorBookLink)).all() + original_genre_book_links = session.exec(select(GenreBookLink)).all() # Reset database alembic_cfg = Config("alembic.ini") with engine.begin() as connection: @@ -27,7 +30,9 @@ def setup_database(): with Session(engine) as session: assert len(session.exec(select(Author)).all()) == 0 assert len(session.exec(select(Book)).all()) == 0 + assert len(session.exec(select(Genre)).all()) == 0 assert len(session.exec(select(AuthorBookLink)).all()) == 0 + assert len(session.exec(select(GenreBookLink)).all()) == 0 yield # Here pytest will start testing # Restore original data from backup with Session(engine) as session: @@ -35,7 +40,9 @@ def setup_database(): session.add(author) for book in original_books: session.add(book) - for link in original_links: + for link in original_author_book_links: + session.add(link) + for link in original_genre_book_links: session.add(link) session.commit() diff --git a/tests/test_relationships.py b/tests/test_relationships.py index 03d99e2..e7870ab 100644 --- a/tests/test_relationships.py +++ b/tests/test_relationships.py @@ -10,7 +10,7 @@ from tests.test_misc import setup_database client = TestClient(app) def make_relationship(author_id, book_id): - response = client.post("/relationships", params={"author_id": author_id, "book_id": book_id}) + response = client.post("/relationships/author-book", params={"author_id": author_id, "book_id": book_id}) assert response.status_code == 200, "Invalid response status" def test_prepare_data(setup_database): @@ -28,7 +28,7 @@ def test_prepare_data(setup_database): make_relationship(2, 3) make_relationship(3, 3) - response = client.get("/relationships") + response = client.get("/relationships/author-book") assert response.status_code == 200, "Invalid response status" assert len(response.json()) == 5, "Invalid number of relationships"