Global refactoring of the project to use poetry and implement tests,

fixing bugs, changing the handling of dto and db models, preparing to
add new functionality
This commit is contained in:
2025-06-24 13:30:35 +03:00
parent 51a6ba75c0
commit 6658d773bf
58 changed files with 2521 additions and 1008 deletions

View File

@@ -0,0 +1,14 @@
from fastapi import APIRouter
from .authors import router as authors_router
from .books import router as books_router
from .relationships import router as relationships_router
from .misc import router as misc_router
api_router = APIRouter()
# Including all routers
api_router.include_router(authors_router)
api_router.include_router(books_router)
api_router.include_router(relationships_router)
api_router.include_router(misc_router)

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 Author, AuthorBookLink, Book, AuthorWithBooks
from library_service.models.dto import (
AuthorCreate, AuthorUpdate, AuthorRead,
AuthorList, BookRead
)
router = APIRouter(prefix="/authors", tags=["authors"])
# Create an author
@router.post("/", response_model=AuthorRead)
def create_author(author: AuthorCreate, session: Session = Depends(get_session)):
db_author = Author(**author.model_dump())
session.add(db_author)
session.commit()
session.refresh(db_author)
return AuthorRead(**db_author.model_dump())
# Read authors
@router.get("/", response_model=AuthorList)
def read_authors(session: Session = Depends(get_session)):
authors = session.exec(select(Author)).all()
return AuthorList(
authors=[AuthorRead(**author.model_dump()) for author in authors],
total=len(authors)
)
# Read an author with their books
@router.get("/{author_id}", response_model=AuthorWithBooks)
def get_author(author_id: int, session: Session = Depends(get_session)):
author = session.get(Author, author_id)
if not author:
raise HTTPException(status_code=404, detail="Author not found")
books = session.exec(
select(Book)
.join(AuthorBookLink)
.where(AuthorBookLink.author_id == author_id)
).all()
book_reads = [BookRead(**book.model_dump()) for book in books]
author_data = author.model_dump()
author_data['books'] = book_reads
return AuthorWithBooks(**author_data)
# Update an author
@router.put("/{author_id}", response_model=AuthorRead)
def update_author(
author_id: int,
author: AuthorUpdate,
session: Session = Depends(get_session)
):
db_author = session.get(Author, author_id)
if not db_author:
raise HTTPException(status_code=404, detail="Author not found")
update_data = author.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_author, field, value)
session.commit()
session.refresh(db_author)
return AuthorRead(**db_author.model_dump())
# Delete an author
@router.delete("/{author_id}", response_model=AuthorRead)
def delete_author(author_id: int, session: Session = Depends(get_session)):
author = session.get(Author, author_id)
if not author:
raise HTTPException(status_code=404, detail="Author not found")
author_read = AuthorRead(**author.model_dump())
session.delete(author)
session.commit()
return author_read

View File

@@ -0,0 +1,77 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from typing import Deque, List
from library_service.settings import get_session
from library_service.models.db import Book, Author, AuthorBookLink, BookWithAuthors
from library_service.models.dto import (
BookCreate, BookUpdate, BookRead,
BookList, AuthorRead
)
router = APIRouter(prefix="/books", tags=["books"])
# Create a book
@router.post("/", response_model=Book)
def create_book(book: BookCreate, session: Session = Depends(get_session)):
db_book = Book(**book.model_dump())
session.add(db_book)
session.commit()
session.refresh(db_book)
return BookRead(**db_book.model_dump())
# Read books
@router.get("/", response_model=BookList)
def read_books(session: Session = Depends(get_session)):
books = session.exec(select(Book)).all()
return BookList(
books=[BookRead(**book.model_dump()) for book in books],
total=len(books)
)
# Read a book
@router.get("/{book_id}", response_model=BookWithAuthors)
def get_book(book_id: int, session: Session = Depends(get_session)):
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return BookWithAuthors(**book.model_dump())
# Update a book
@router.put("/{book_id}", response_model=Book)
def update_book(book_id: int, book: BookUpdate, session: Session = Depends(get_session)):
db_book = session.get(Book, book_id)
if not db_book:
raise HTTPException(status_code=404, detail="Book not found")
db_book.title = book.title or db_book.title
db_book.description = book.description or db_book.description
session.commit()
session.refresh(db_book)
return db_book
# Delete a book
@router.delete("/{book_id}", response_model=BookRead)
def delete_book(book_id: int, session: Session = Depends(get_session)):
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
book_read = BookRead(id=(book.id or 0), title=book.title, description=book.description)
session.delete(book)
session.commit()
return book_read
# Get all authors for a book
@router.get("/{book_id}/authors/", response_model=List[AuthorRead])
def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
authors = session.exec(
select(Author)
.join(AuthorBookLink)
.where(AuthorBookLink.book_id == book_id)
).all()
return [AuthorRead(**author.model_dump()) for author in authors]

View File

@@ -0,0 +1,38 @@
from fastapi import APIRouter, Request, FastAPI
from fastapi.params import Depends
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from datetime import datetime
from typing import Dict
from httpx import get
from library_service.settings import get_app
# Templates initialization
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
router = APIRouter(tags=["misc"])
# Formatted information about the application
def get_info(app) -> Dict:
return {
"status": "ok",
"app_info": {
"title": app.title,
"version": app.version,
"description": app.description,
},
"server_time": datetime.now().isoformat(),
}
# Root endpoint
@router.get("/", response_class=HTMLResponse)
async def root(request: Request, app=Depends(get_app)):
return templates.TemplateResponse(request, "index.html", get_info(app))
# API Information endpoint
@router.get("/api/info")
async def api_info(app=Depends(get_app)):
return JSONResponse(content=get_info(app))

View File

@@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends, HTTPException
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
router = APIRouter(prefix="/relationships", tags=["relations"])
# Add author to book
@router.post("/", 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:
raise HTTPException(status_code=404, detail="Author not found")
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
existing_link = session.exec(
select(AuthorBookLink)
.where(AuthorBookLink.author_id == author_id)
.where(AuthorBookLink.book_id == book_id)
).first()
if existing_link:
raise HTTPException(status_code=400, detail="Relationship already exists")
link = AuthorBookLink(author_id=author_id, book_id=book_id)
session.add(link)
session.commit()
session.refresh(link)
return link
# Remove author from book
@router.delete("/", 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)
.where(AuthorBookLink.author_id == author_id)
.where(AuthorBookLink.book_id == book_id)
).first()
if not link:
raise HTTPException(status_code=404, detail="Relationship not found")
session.delete(link)
session.commit()
return {"message": "Relationship removed successfully"}