mirror of
https://github.com/wowlikon/LibraryAPI.git
synced 2025-12-11 21:30:46 +00:00
Created genre table and added links and endpoints for it.
This commit is contained in:
10
README.md
10
README.md
@@ -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**
|
||||||
|
|||||||
@@ -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'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
80
library_service/routers/genres.py
Normal file
80
library_service/routers/genres.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
45
migrations/versions/9d7a43ac5dfc_genres.py
Normal file
45
migrations/versions/9d7a43ac5dfc_genres.py
Normal 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 ###
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user