from typing import List, Dict, Optional, Union, Any from datetime import datetime import re from sqlalchemy.orm import Session from models.book import Book from schemas.book import BookCreate, BookUpdate def validate_isbn(isbn: str) -> bool: """ Validate ISBN format (supports both ISBN-10 and ISBN-13). Args: isbn: ISBN string to validate Returns: bool: True if valid ISBN format, False otherwise """ isbn = isbn.replace("-", "").replace(" ", "") if len(isbn) == 10: # ISBN-10 validation if not isbn[:-1].isdigit() and isbn[-1].lower() != 'x': return False total = 0 for i in range(9): total += int(isbn[i]) * (10 - i) check = 11 - (total % 11) return str(check) == isbn[-1] or (check == 10 and isbn[-1].lower() == 'x') elif len(isbn) == 13: # ISBN-13 validation if not isbn.isdigit(): return False total = 0 for i in range(12): if i % 2 == 0: total += int(isbn[i]) else: total += int(isbn[i]) * 3 check = 10 - (total % 10) if check == 10: check = 0 return str(check) == isbn[-1] return False def search_books( db: Session, title: Optional[str] = None, author: Optional[str] = None, genre: Optional[str] = None, publisher: Optional[str] = None, available_only: bool = False ) -> List[Book]: """ Search books with multiple optional filters. Args: db: Database session title: Optional title to search for author: Optional author to search for genre: Optional genre to filter by publisher: Optional publisher to filter by available_only: If True, return only available books Returns: List of books matching the search criteria """ query = db.query(Book) if title: query = query.filter(Book.title.ilike(f"%{title}%")) if author: query = query.filter(Book.author.ilike(f"%{author}%")) if genre: query = query.filter(Book.genre == genre) if publisher: query = query.filter(Book.publisher == publisher) if available_only: query = query.filter(Book.is_available == True) return query.all() def get_book_stats(db: Session) -> Dict[str, Any]: """ Get statistical information about books in the database. Args: db: Database session Returns: Dictionary containing various statistics about books """ total_books = db.query(Book).count() available_books = db.query(Book).filter(Book.is_available == True).count() genres = db.query(Book.genre).distinct().all() languages = db.query(Book.language).distinct().all() avg_pages = db.query(func.avg(Book.page_count)).scalar() return { "total_books": total_books, "available_books": available_books, "total_genres": len(genres), "genres": [g[0] for g in genres], "languages": [l[0] for l in languages], "average_page_count": round(avg_pages if avg_pages else 0, 2) } def validate_book_data(book_data: Union[BookCreate, BookUpdate]) -> Dict[str, str]: """ Validate book data before creation or update. Args: book_data: Book data to validate Returns: Dictionary of validation errors, empty if validation passes """ errors = {} if hasattr(book_data, 'isbn') and book_data.isbn: if not validate_isbn(book_data.isbn): errors['isbn'] = "Invalid ISBN format" if hasattr(book_data, 'publication_year'): current_year = datetime.now().year if book_data.publication_year > current_year: errors['publication_year'] = "Publication year cannot be in the future" if hasattr(book_data, 'page_count'): if book_data.page_count <= 0: errors['page_count'] = "Page count must be positive" return errors def get_similar_books(db: Session, book_id: int, limit: int = 5) -> List[Book]: """ Find similar books based on genre and author. Args: db: Database session book_id: ID of the reference book limit: Maximum number of similar books to return Returns: List of similar books """ reference_book = db.query(Book).filter(Book.id == book_id).first() if not reference_book: return [] similar_books = db.query(Book).filter( Book.id != book_id, Book.genre == reference_book.genre, Book.author == reference_book.author ).limit(limit).all() if len(similar_books) < limit: # If not enough books by same author, add books from same genre additional_books = db.query(Book).filter( Book.id != book_id, Book.genre == reference_book.genre, Book.author != reference_book.author ).limit(limit - len(similar_books)).all() similar_books.extend(additional_books) return similar_books