Created genre table and added links and endpoints for it.

This commit is contained in:
2025-06-25 14:50:28 +03:00
parent 83dbb1824e
commit 9604771439
9 changed files with 158 additions and 19 deletions

View File

@@ -74,11 +74,11 @@ For run tests:
| GET | `/books/{id}/authors` | Retrieve a list of authors for a specific book | | GET | `/books/{id}/authors` | Retrieve a list of authors for a specific book |
**Relationships** **Relationships**
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|-----------------------|------------------------------------------------| |--------|------------------------------|-----------------------------------------|
| GET | `/relationships` | Retrieve a list of all relationships | | GET | `/relationships/author-book` | Retrieve a list of all relationships |
| POST | `/relationships` | Add author-book relationship | | POST | `/relationships/author-book` | Add author-book relationship |
| DELETE | `/relationships` | Remove author-book relationship | | DELETE | `/relationships/author-book` | Remove author-book relationship |
### **Technologies Used** ### **Technologies Used**

View File

@@ -1,14 +1,17 @@
from .author import Author from .author import Author
from .book import Book from .book import Book
from .genre import Genre
from .links import ( from .links import (
AuthorBookLink, GenreBookLink, AuthorBookLink, GenreBookLink,
AuthorWithBooks, BookWithAuthors, AuthorWithBooks, BookWithAuthors,
GenreWithBooks, BookWithAuthorsAndGenres GenreWithBooks, BookWithGenres,
BookWithAuthorsAndGenres
) )
__all__ = [ __all__ = [
'Author', 'Book', 'Author', 'Book', 'Genre',
'AuthorBookLink', 'AuthorWithBooks', 'AuthorBookLink', 'AuthorWithBooks',
'BookWithAuthors', 'GenreBookLink', 'BookWithAuthors', 'GenreBookLink',
'GenreWithBooks', 'BookWithAuthorsAndGenres' 'GenreWithBooks', 'BookWithGenres',
'BookWithAuthorsAndGenres'
] ]

View File

@@ -9,6 +9,6 @@ if TYPE_CHECKING:
class Genre(GenreBase, table=True): class Genre(GenreBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True, index=True) id: Optional[int] = Field(default=None, primary_key=True, index=True)
books: List["Book"] = Relationship( books: List["Book"] = Relationship(
back_populates="authors", back_populates="genres",
link_model=GenreBookLink link_model=GenreBookLink
) )

View File

@@ -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

View File

@@ -3,13 +3,13 @@ from sqlmodel import Session, select
from typing import List, Dict from typing import List, Dict
from library_service.settings import get_session from library_service.settings import get_session
from library_service.models.db import Book, Author, AuthorBookLink from library_service.models.db import Author, Book, Genre, AuthorBookLink, GenreBookLink
from library_service.models.dto import AuthorRead, BookRead from library_service.models.dto import AuthorRead, BookRead, GenreRead
router = APIRouter(tags=["relations"]) router = APIRouter(tags=["relations"])
# Add author to book # 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)): def add_author_to_book(author_id: int, book_id: int, session: Session = Depends(get_session)):
author = session.get(Author, author_id) author = session.get(Author, author_id)
if not author: if not author:
@@ -35,7 +35,7 @@ def add_author_to_book(author_id: int, book_id: int, session: Session = Depends(
return link return link
# Remove author from book # 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)): def remove_author_from_book(author_id: int, book_id: int, session: Session = Depends(get_session)):
link = session.exec( link = session.exec(
select(AuthorBookLink) 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"} return {"message": "Relationship removed successfully"}
# Get relationships # 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)): def get_relationships(session: Session = Depends(get_session)):
relationships = session.exec(select(AuthorBookLink)).all() relationships = session.exec(select(AuthorBookLink)).all()
return relationships return relationships

View File

@@ -24,6 +24,10 @@ def get_app() -> FastAPI:
"name": "books", "name": "books",
"description": "Operations with books.", "description": "Operations with books.",
}, },
{
"name": "genres",
"description": "Operations with genres.",
},
{ {
"name": "relations", "name": "relations",
"description": "Operations with relations.", "description": "Operations with relations.",

View File

@@ -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 ###

View File

@@ -6,7 +6,8 @@ from fastapi.testclient import TestClient
from sqlmodel import select, delete, Session from sqlmodel import select, delete, Session
from library_service.main import app, engine 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) client = TestClient(app)
@@ -16,7 +17,9 @@ def setup_database():
with Session(engine) as session: with Session(engine) as session:
original_authors = session.exec(select(Author)).all() original_authors = session.exec(select(Author)).all()
original_books = session.exec(select(Book)).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 # Reset database
alembic_cfg = Config("alembic.ini") alembic_cfg = Config("alembic.ini")
with engine.begin() as connection: with engine.begin() as connection:
@@ -27,7 +30,9 @@ def setup_database():
with Session(engine) as session: with Session(engine) as session:
assert len(session.exec(select(Author)).all()) == 0 assert len(session.exec(select(Author)).all()) == 0
assert len(session.exec(select(Book)).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(AuthorBookLink)).all()) == 0
assert len(session.exec(select(GenreBookLink)).all()) == 0
yield # Here pytest will start testing yield # Here pytest will start testing
# Restore original data from backup # Restore original data from backup
with Session(engine) as session: with Session(engine) as session:
@@ -35,7 +40,9 @@ def setup_database():
session.add(author) session.add(author)
for book in original_books: for book in original_books:
session.add(book) 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.add(link)
session.commit() session.commit()

View File

@@ -10,7 +10,7 @@ from tests.test_misc import setup_database
client = TestClient(app) client = TestClient(app)
def make_relationship(author_id, book_id): 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" assert response.status_code == 200, "Invalid response status"
def test_prepare_data(setup_database): def test_prepare_data(setup_database):
@@ -28,7 +28,7 @@ def test_prepare_data(setup_database):
make_relationship(2, 3) make_relationship(2, 3)
make_relationship(3, 3) make_relationship(3, 3)
response = client.get("/relationships") response = client.get("/relationships/author-book")
assert response.status_code == 200, "Invalid response status" assert response.status_code == 200, "Invalid response status"
assert len(response.json()) == 5, "Invalid number of relationships" assert len(response.json()) == 5, "Invalid number of relationships"