diff --git a/.env b/.env index 610aa73..5685a36 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ POSTGRES_USER = "postgres" -POSTGRES_PASSWORD = "password" -POSTGRES_DB = "mydatabase" +POSTGRES_PASSWORD = "postgres" +POSTGRES_DB = "postgres" POSTGRES_SERVER = "db" diff --git a/README.md b/README.md index 17e156e..210fc0b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # LibraryAPI Это проект приложения на FastAPI - современном веб фреймворке для создания API на Python. Я использую Pydantic для валидации данных, SQLModel для взаимодействия с базой данных, Alembic для управления миграциями, PostgreSQL как систему базы данных и Docker Compose для легкого развертывания. @@ -52,42 +53,48 @@ ### **Эндпоинты API** **Авторы** -| Метод | Эндпоинты | Описание | -|--------|-----------------------|---------------------------------------------| -| POST | `/authors` | Создать нового автора | -| GET | `/authors` | Получить список всех авторов | -| GET | `/authors/{id}` | Получить конкретного автора по ID с книгами | -| PUT | `/authors/{id}` | Обновить конкретного автора по ID | -| DELETE | `/authors/{id}` | Удалить конкретного автора по ID | -| GET | `/authors/{id}/books` | Получить список книг для конкретного автора | +| Метод | Эндпоинты | Описание | +|--------|-----------------------|---------------------------------| +| POST | `/authors` | Создать нового автора | +| GET | `/authors` | Получить список всех авторов | +| GET | `/authors/{id}` | Получить автора по ID с книгами | +| PUT | `/authors/{id}` | Обновить автора по ID | +| DELETE | `/authors/{id}` | Удалить автора по ID | **Книги** -| Метод | Эндпоинты | Описание | -|--------|-----------------------|----------------------------------------------| -| POST | `/books` | Создать новую книгу | -| GET | `/books` | Получить список всех книг | -| GET | `/book/{id}` | Получить конкретную книгу по ID с авторами | -| PUT | `/books/{id}` | Обновить конкретную книгу по ID | -| DELETE | `/books/{id}` | Удалить конкретную книгу по ID | -| GET | `/books/{id}/authors` | Получить список авторов для конкретной книги | +| Метод | Эндпоинты | Описание | +|--------|-----------------------|---------------------------------| +| POST | `/books` | Создать новую книгу | +| GET | `/books` | Получить список всех книг | +| GET | `/book/{id}` | Получить книгу по ID с авторами | +| PUT | `/books/{id}` | Обновить книгу по ID | +| DELETE | `/books/{id}` | Удалить книгу по ID | **Жанры** -| Метод | Эндпоинты | Описание | -|--------|-----------------------|----------------------------------------------| -| POST | `/genres` | Создать новый жанр | -| GET | `/genres` | Получить список всех жанров | -| GET | `/genres/{id}` | Получить конкретный жанр по ID | -| PUT | `/genres/{id}` | Обновить конкретный жанр по ID | -| DELETE | `/genres/{id}` | Удалить конкретный жанр по ID | -| GET | `/books/{id}/genres` | Получить список жанров для конкретной книги | +| Метод | Эндпоинты | Описание | +|--------|-----------------------|---------------------------------| +| POST | `/genres` | Создать новый жанр | +| GET | `/genres` | Получить список всех жанров | +| GET | `/genres/{id}` | Получить жанр по ID | +| PUT | `/genres/{id}` | Обновить жанр по ID | +| DELETE | `/genres/{id}` | Удалить жанр по ID | **Связи** -| Метод | Эндпоинты | Описание | -|--------|------------------------------|-----------------------------------------| -| GET | `/relationships/author-book` | Получить список всех связей автор-книга | -| POST | `/relationships/author-book` | Добавить связь автор-книга | -| DELETE | `/relationships/author-book` | Удалить связь автор-книга | +| Метод | Эндпоинты | Описание | +|--------|------------------------------|-----------------------------------| +| GET | `/authors/{id}/books` | Получить список книг для автора | +| GET | `/books/{id}/authors` | Получить список авторов для книги | +| POST | `/relationships/author-book` | Связать автор-книга | +| DELETE | `/relationships/author-book` | Разделить автор-книга | +| GET | `/genres/{id}/books` | Получить список книг для жанра | +| GET | `/books/{id}/genres` | Получить список жанров для книги | +| POST | `/relationships/genre-book` | Связать автор-книга | +| DELETE | `/relationships/genre-book` | Разделить автор-книга | +**Другие** +| Метод | Эндпоинты | Описание | +|--------|-------------|-------------------------------| +| GET | `/api/info` | Получить информацию о сервисе | ### **Используемые технологии** diff --git a/docker-compose.yml b/docker-compose.yml index 842ac10..44cb0e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,17 +2,17 @@ services: db: container_name: db image: postgres:17 - expose: - - 5432 - volumes: - - ./data/db:/var/lib/postgresql/data + ports: + - 5432:5432 + # volumes: + # - ./data/db:/var/lib/postgresql/data env_file: - ./.env api: container_name: api build: . - command: bash -c "alembic upgrade head && uvicorn library_service.main:app --reload --host 0.0.0.0 --port 8000" + command: bash -c "alembic upgrade head && uvicorn library_service.main:app --host 0.0.0.0 --port 8000 --reload" volumes: - .:/code ports: @@ -26,5 +26,3 @@ services: command: bash -c "pytest tests" volumes: - .:/code - depends_on: - - db diff --git a/library_service/api.py b/library_service/api.py deleted file mode 100644 index 7d3b3c8..0000000 --- a/library_service/api.py +++ /dev/null @@ -1,6 +0,0 @@ -from fastapi import APIRouter -import asyncpg - -router = APIRouter( - prefix='/devices' -) diff --git a/library_service/favicon.svg b/library_service/favicon.svg new file mode 100644 index 0000000..99e7bd4 --- /dev/null +++ b/library_service/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/library_service/models/db/__init__.py b/library_service/models/db/__init__.py index 5e32583..aeb3699 100644 --- a/library_service/models/db/__init__.py +++ b/library_service/models/db/__init__.py @@ -2,16 +2,24 @@ from .author import Author from .book import Book from .genre import Genre from .links import ( - AuthorBookLink, GenreBookLink, - AuthorWithBooks, BookWithAuthors, - GenreWithBooks, BookWithGenres, - BookWithAuthorsAndGenres + AuthorBookLink, + GenreBookLink, + AuthorWithBooks, + BookWithAuthors, + GenreWithBooks, + BookWithGenres, + BookWithAuthorsAndGenres, ) __all__ = [ - 'Author', 'Book', 'Genre', - 'AuthorBookLink', 'AuthorWithBooks', - 'BookWithAuthors', 'GenreBookLink', - 'GenreWithBooks', 'BookWithGenres', - 'BookWithAuthorsAndGenres' + "Author", + "Book", + "Genre", + "AuthorBookLink", + "AuthorWithBooks", + "BookWithAuthors", + "GenreBookLink", + "GenreWithBooks", + "BookWithGenres", + "BookWithAuthorsAndGenres", ] diff --git a/library_service/models/db/author.py b/library_service/models/db/author.py index 6c54b0c..ea07fbc 100644 --- a/library_service/models/db/author.py +++ b/library_service/models/db/author.py @@ -6,9 +6,9 @@ from .links import AuthorBookLink if TYPE_CHECKING: from .book import Book + class Author(AuthorBase, table=True): id: Optional[int] = Field(default=None, primary_key=True, index=True) books: List["Book"] = Relationship( - back_populates="authors", - link_model=AuthorBookLink + back_populates="authors", link_model=AuthorBookLink ) diff --git a/library_service/models/db/book.py b/library_service/models/db/book.py index ead3968..cf09158 100644 --- a/library_service/models/db/book.py +++ b/library_service/models/db/book.py @@ -7,13 +7,12 @@ if TYPE_CHECKING: from .author import Author from .genre import Genre + class Book(BookBase, table=True): id: Optional[int] = Field(default=None, primary_key=True, index=True) authors: List["Author"] = Relationship( - back_populates="books", - link_model=AuthorBookLink + back_populates="books", link_model=AuthorBookLink ) genres: List["Genre"] = Relationship( - back_populates="books", - link_model=GenreBookLink + back_populates="books", link_model=GenreBookLink ) diff --git a/library_service/models/db/genre.py b/library_service/models/db/genre.py index aca8baf..120beeb 100644 --- a/library_service/models/db/genre.py +++ b/library_service/models/db/genre.py @@ -6,9 +6,9 @@ from .links import GenreBookLink if TYPE_CHECKING: from .book import Book + class Genre(GenreBase, table=True): id: Optional[int] = Field(default=None, primary_key=True, index=True) books: List["Book"] = Relationship( - back_populates="genres", - link_model=GenreBookLink + back_populates="genres", link_model=GenreBookLink ) diff --git a/library_service/models/db/links.py b/library_service/models/db/links.py index 2ac7037..78f5553 100644 --- a/library_service/models/db/links.py +++ b/library_service/models/db/links.py @@ -5,26 +5,35 @@ from library_service.models.dto.author import AuthorRead from library_service.models.dto.book import BookRead from library_service.models.dto.genre import GenreRead + class AuthorBookLink(SQLModel, table=True): - author_id: int | None = Field(default=None, foreign_key="author.id", primary_key=True) + author_id: int | None = Field( + default=None, foreign_key="author.id", primary_key=True + ) book_id: int | None = Field(default=None, foreign_key="book.id", primary_key=True) + class GenreBookLink(SQLModel, table=True): genre_id: int | None = Field(default=None, foreign_key="genre.id", primary_key=True) book_id: int | None = Field(default=None, foreign_key="book.id", primary_key=True) + class AuthorWithBooks(AuthorRead): books: List[BookRead] = Field(default_factory=list) + class BookWithAuthors(BookRead): authors: List[AuthorRead] = Field(default_factory=list) + class BookWithGenres(BookRead): genres: List[GenreRead] = Field(default_factory=list) + class GenreWithBooks(GenreRead): books: List[BookRead] = Field(default_factory=list) + class BookWithAuthorsAndGenres(BookRead): authors: List[AuthorRead] = Field(default_factory=list) genres: List[GenreRead] = Field(default_factory=list) diff --git a/library_service/models/dto/__init__.py b/library_service/models/dto/__init__.py index ef60ea5..e47f418 100644 --- a/library_service/models/dto/__init__.py +++ b/library_service/models/dto/__init__.py @@ -1,19 +1,22 @@ -from .author import ( - AuthorBase, AuthorCreate, AuthorUpdate, - AuthorRead, AuthorList -) -from .book import ( - BookBase, BookCreate, BookUpdate, - BookRead, BookList -) +from .author import AuthorBase, AuthorCreate, AuthorUpdate, AuthorRead, AuthorList +from .book import BookBase, BookCreate, BookUpdate, BookRead, BookList -from .genre import ( - GenreBase, GenreCreate, GenreUpdate, - GenreRead, GenreList -) +from .genre import GenreBase, GenreCreate, GenreUpdate, GenreRead, GenreList __all__ = [ - 'AuthorBase', 'AuthorCreate', 'AuthorUpdate', 'AuthorRead', 'AuthorList', - 'BookBase', 'BookCreate', 'BookUpdate', 'BookRead', 'BookList', - 'GenreBase', 'GenreCreate', 'GenreUpdate', 'GenreRead', 'GenreList', + "AuthorBase", + "AuthorCreate", + "AuthorUpdate", + "AuthorRead", + "AuthorList", + "BookBase", + "BookCreate", + "BookUpdate", + "BookRead", + "BookList", + "GenreBase", + "GenreCreate", + "GenreUpdate", + "GenreRead", + "GenreList", ] diff --git a/library_service/models/dto/author.py b/library_service/models/dto/author.py index a3b2e32..59ca6e0 100644 --- a/library_service/models/dto/author.py +++ b/library_service/models/dto/author.py @@ -2,24 +2,27 @@ from sqlmodel import SQLModel from pydantic import ConfigDict from typing import Optional, List + class AuthorBase(SQLModel): name: str - model_config = ConfigDict( #pyright: ignore - json_schema_extra={ - "example": {"name": "author_name"} - } + model_config = ConfigDict( # pyright: ignore + json_schema_extra={"example": {"name": "author_name"}} ) + class AuthorCreate(AuthorBase): pass + class AuthorUpdate(SQLModel): name: Optional[str] = None + class AuthorRead(AuthorBase): id: int + class AuthorList(SQLModel): authors: List[AuthorRead] total: int diff --git a/library_service/models/dto/book.py b/library_service/models/dto/book.py index 4ea9eef..5667749 100644 --- a/library_service/models/dto/book.py +++ b/library_service/models/dto/book.py @@ -2,29 +2,31 @@ from sqlmodel import SQLModel from pydantic import ConfigDict from typing import Optional, List + class BookBase(SQLModel): title: str description: str - model_config = ConfigDict( #pyright: ignore + model_config = ConfigDict( # pyright: ignore json_schema_extra={ - "example": { - "title": "book_title", - "description": "book_description" - } + "example": {"title": "book_title", "description": "book_description"} } ) + class BookCreate(BookBase): pass + class BookUpdate(SQLModel): title: Optional[str] = None description: Optional[str] = None + class BookRead(BookBase): id: int + class BookList(SQLModel): books: List[BookRead] total: int diff --git a/library_service/models/dto/genre.py b/library_service/models/dto/genre.py index e589cce..48856d2 100644 --- a/library_service/models/dto/genre.py +++ b/library_service/models/dto/genre.py @@ -2,24 +2,27 @@ from sqlmodel import SQLModel from pydantic import ConfigDict from typing import Optional, List + class GenreBase(SQLModel): name: str - model_config = ConfigDict( #pyright: ignore - json_schema_extra={ - "example": {"name": "genre_name"} - } + model_config = ConfigDict( # pyright: ignore + json_schema_extra={"example": {"name": "genre_name"}} ) + class GenreCreate(GenreBase): pass + class GenreUpdate(SQLModel): name: Optional[str] = None + class GenreRead(GenreBase): id: int + class GenreList(SQLModel): genres: List[GenreRead] total: int diff --git a/library_service/routers/authors.py b/library_service/routers/authors.py index c47a033..6eef937 100644 --- a/library_service/routers/authors.py +++ b/library_service/routers/authors.py @@ -1,17 +1,27 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Path, 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 + AuthorCreate, + AuthorUpdate, + AuthorRead, + AuthorList, + BookRead, ) + router = APIRouter(prefix="/authors", tags=["authors"]) + # Create an author -@router.post("/", response_model=AuthorRead) +@router.post( + "/", + response_model=AuthorRead, + summary="Создать автора", + description="Добавляет автора в систему", +) def create_author(author: AuthorCreate, session: Session = Depends(get_session)): db_author = Author(**author.model_dump()) session.add(db_author) @@ -19,41 +29,60 @@ def create_author(author: AuthorCreate, session: Session = Depends(get_session)) session.refresh(db_author) return AuthorRead(**db_author.model_dump()) + # Read authors -@router.get("/", response_model=AuthorList) +@router.get( + "/", + response_model=AuthorList, + summary="Получить список авторов", + description="Возвращает список всех авторов в системе", +) 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) + 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)): +@router.get( + "/{author_id}", + response_model=AuthorWithBooks, + summary="Получить информацию об авторе", + description="Возвращает информацию об авторе и его книгах", +) +def get_author( + author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0), + 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) + 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 + author_data["books"] = book_reads return AuthorWithBooks(**author_data) + # Update an author -@router.put("/{author_id}", response_model=AuthorRead) +@router.put( + "/{author_id}", + response_model=AuthorRead, + summary="Обновить информацию об авторе", + description="Обновляет информацию об авторе в системе", +) def update_author( - author_id: int, author: AuthorUpdate, - session: Session = Depends(get_session) + author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0), + session: Session = Depends(get_session), ): db_author = session.get(Author, author_id) if not db_author: @@ -67,9 +96,18 @@ def update_author( 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)): +@router.delete( + "/{author_id}", + response_model=AuthorRead, + summary="Удалить автора", + description="Удаляет автора из системы", +) +def delete_author( + author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0), + session: Session = Depends(get_session), +): author = session.get(Author, author_id) if not author: raise HTTPException(status_code=404, detail="Author not found") diff --git a/library_service/routers/books.py b/library_service/routers/books.py index 8c6202a..c8df20f 100644 --- a/library_service/routers/books.py +++ b/library_service/routers/books.py @@ -1,17 +1,28 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Path, Depends, HTTPException from sqlmodel import Session, select +from library_service.models.db.links import BookWithAuthorsAndGenres from library_service.settings import get_session from library_service.models.db import Author, Book, BookWithAuthors, AuthorBookLink from library_service.models.dto import ( - AuthorRead, BookList, BookRead, - BookCreate, BookUpdate + AuthorRead, + BookList, + BookRead, + BookCreate, + BookUpdate, ) + router = APIRouter(prefix="/books", tags=["books"]) + # Create a book -@router.post("/", response_model=Book) +@router.post( + "/", + response_model=Book, + summary="Создать книгу", + description="Добавляет книгу в систему", +) def create_book(book: BookCreate, session: Session = Depends(get_session)): db_book = Book(**book.model_dump()) session.add(db_book) @@ -19,38 +30,67 @@ def create_book(book: BookCreate, session: Session = Depends(get_session)): session.refresh(db_book) return BookRead(**db_book.model_dump()) + # Read books -@router.get("/", response_model=BookList) +@router.get( + "/", + response_model=BookList, + summary="Получить список книг", + description="Возвращает список всех книг в системе", +) 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) + books=[BookRead(**book.model_dump()) for book in books], total=len(books) ) -# Read a book with their authors -@router.get("/{book_id}", response_model=BookWithAuthors) -def get_book(book_id: int, session: Session = Depends(get_session)): + +# Read a book with their authors and genres +@router.get( + "/{book_id}", + response_model=BookWithAuthorsAndGenres, + summary="Получить информацию о книге", + description="Возвращает информацию о книге, её авторах и жанрах", +) +def get_book( + book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0), + 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) + select(Author).join(AuthorBookLink).where(AuthorBookLink.book_id == book_id) ).all() author_reads = [AuthorRead(**author.model_dump()) for author in authors] + genres = session.exec( + select(Genre).join(GenreBookLink).where(GenreBookLink.book_id == book_id) + ).all() + + genre_reads = [GenreRead(**genre.model_dump()) for genre in genres] + book_data = book.model_dump() - book_data['authors'] = author_reads + book_data["authors"] = author_reads + book_data["genres"] = genre_reads return BookWithAuthors(**book_data) + # Update a book -@router.put("/{book_id}", response_model=Book) -def update_book(book_id: int, book: BookUpdate, session: Session = Depends(get_session)): +@router.put( + "/{book_id}", + response_model=Book, + summary="Обновить информацию о книге", + description="Обновляет информацию о книге в системе", +) +def update_book( + book: BookUpdate, + book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0), + 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") @@ -61,13 +101,24 @@ def update_book(book_id: int, book: BookUpdate, session: Session = Depends(get_s 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)): +@router.delete( + "/{book_id}", + response_model=BookRead, + summary="Удалить книгу", + description="Удаляет книгу их системы", +) +def delete_book( + book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0), + 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) + book_read = BookRead( + id=(book.id or 0), title=book.title, description=book.description + ) session.delete(book) session.commit() return book_read diff --git a/library_service/routers/genres.py b/library_service/routers/genres.py index 15107ce..e1e352b 100644 --- a/library_service/routers/genres.py +++ b/library_service/routers/genres.py @@ -1,17 +1,27 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Path, 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 + GenreCreate, + GenreUpdate, + GenreRead, + GenreList, + BookRead, ) + router = APIRouter(prefix="/genres", tags=["genres"]) + # Create a genre -@router.post("/", response_model=GenreRead) +@router.post( + "/", + response_model=GenreRead, + summary="Создать жанр", + description="Добавляет жанр книг в систему", +) def create_genre(genre: GenreCreate, session: Session = Depends(get_session)): db_genre = Genre(**genre.model_dump()) session.add(db_genre) @@ -19,41 +29,59 @@ def create_genre(genre: GenreCreate, session: Session = Depends(get_session)): session.refresh(db_genre) return GenreRead(**db_genre.model_dump()) + # Read genres -@router.get("/", response_model=GenreList) +@router.get( + "/", + response_model=GenreList, + summary="Получить список жанров", + description="Возвращает список всех жанров в системе", +) 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) + 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)): +@router.get( + "/{genre_id}", + response_model=GenreWithBooks, + summary="Получить информацию о жанре", + description="Возвращает информацию о жанре и книгах с ним", +) +def get_genre( + genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0), + 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) + 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 + genre_data["books"] = book_reads return GenreWithBooks(**genre_data) + # Update a genre -@router.put("/{genre_id}", response_model=GenreRead) +@router.put( + "/{genre_id}", + response_model=GenreRead, + summary="Обновляет информацию о жанре", + description="Обновляет информацию о жанре в системе", +) def update_genre( - genre_id: int, genre: GenreUpdate, - session: Session = Depends(get_session) + genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0), + session: Session = Depends(get_session), ): db_genre = session.get(Genre, genre_id) if not db_genre: @@ -67,9 +95,18 @@ def update_genre( 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)): +@router.delete( + "/{genre_id}", + response_model=GenreRead, + summary="Удалить жанр", + description="Удаляет автора из системы", +) +def delete_genre( + genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0), + session: Session = Depends(get_session), +): genre = session.get(Genre, genre_id) if not genre: raise HTTPException(status_code=404, detail="Genre not found") diff --git a/library_service/routers/misc.py b/library_service/routers/misc.py index a351281..0a5a9f1 100644 --- a/library_service/routers/misc.py +++ b/library_service/routers/misc.py @@ -1,13 +1,11 @@ -from fastapi import APIRouter, Request, FastAPI +from fastapi import APIRouter, Path, Request, FastAPI from fastapi.params import Depends -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse 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 # Загрузка шаблонов @@ -30,12 +28,28 @@ def get_info(app) -> Dict: # Эндпоинт главной страницы -@router.get("/", response_class=HTMLResponse) +@router.get("/", include_in_schema=False) async def root(request: Request, app=Depends(get_app)): return templates.TemplateResponse(request, "index.html", get_info(app)) +# Редирект иконки вкладки +@router.get("/favicon.ico", include_in_schema=False) +def redirect_favicon(): + return RedirectResponse("/favicon.svg") + + +# Эндпоинт иконки вкладки +@router.get("/favicon.svg", include_in_schema=False) +async def favicon(): + return FileResponse("library_service/favicon.svg", media_type="image/svg+xml") + + # Эндпоинт информации об API -@router.get("/api/info") +@router.get( + "/api/info", + summary="Информация о сервисе", + description="Возвращает информацию о системе", +) async def api_info(app=Depends(get_app)): return JSONResponse(content=get_info(app)) diff --git a/library_service/routers/relationships.py b/library_service/routers/relationships.py index 3288450..4e808de 100644 --- a/library_service/routers/relationships.py +++ b/library_service/routers/relationships.py @@ -8,9 +8,17 @@ from library_service.models.dto import AuthorRead, BookRead, GenreRead router = APIRouter(tags=["relations"]) + # Add author to book -@router.post("/relationships/author-book", response_model=AuthorBookLink) -def add_author_to_book(author_id: int, book_id: int, session: Session = Depends(get_session)): +@router.post( + "/relationships/author-book", + response_model=AuthorBookLink, + summary="Связать автора и книгу", + description="Добавляет связь между автором и книгой в систему", +) +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") @@ -34,9 +42,17 @@ def add_author_to_book(author_id: int, book_id: int, session: Session = Depends( session.refresh(link) return link + # Remove author from book -@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)): +@router.delete( + "/relationships/author-book", + response_model=Dict[str, str], + summary="Разделить автора и книгу", + description="Удаляет связь между автором и книгой в системе", +) +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) @@ -50,15 +66,55 @@ def remove_author_from_book(author_id: int, book_id: int, session: Session = Dep session.commit() return {"message": "Relationship removed successfully"} -# Get relationships -@router.get("/relationships/genre-book", response_model=List[GenreBookLink]) -def get_relationships(session: Session = Depends(get_session)): - relationships = session.exec(select(GenreBookLink)).all() - return relationships -# Add author to book -@router.post("/relationships/genre-book", response_model=GenreBookLink) -def add_genre_to_book(genre_id: int, book_id: int, session: Session = Depends(get_session)): +# Get author's books +@router.get( + "/authors/{author_id}/books/", + response_model=List[BookRead], + summary="Получить книги, написанные автором", + description="Возвращает все книги в системе, написанные автором", +) +def get_books_for_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() + + return [BookRead(**book.model_dump()) for book in books] + + +# Get book's authors +@router.get( + "/books/{book_id}/authors/", + response_model=List[AuthorRead], + summary="Получить авторов книги", + description="Возвращает всех авторов книги в системе", +) +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] + + +# Add genre to book +@router.post( + "/relationships/genre-book", + response_model=GenreBookLink, + summary="Связать книгу и жанр", + description="Добавляет связь между книгой и жанром в систему", +) +def add_genre_to_book( + genre_id: int, book_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") @@ -82,9 +138,17 @@ def add_genre_to_book(genre_id: int, book_id: int, session: Session = Depends(ge session.refresh(link) return link + # Remove author from book -@router.delete("/relationships/genre-book", response_model=Dict[str, str]) -def remove_genre_from_book(genre_id: int, book_id: int, session: Session = Depends(get_session)): +@router.delete( + "/relationships/genre-book", + response_model=Dict[str, str], + summary="Разделить жанр и книгу", + description="Удаляет связь между жанром и книгой в системе", +) +def remove_genre_from_book( + genre_id: int, book_id: int, session: Session = Depends(get_session) +): link = session.exec( select(GenreBookLink) .where(GenreBookLink.genre_id == genre_id) @@ -98,38 +162,40 @@ def remove_genre_from_book(genre_id: int, book_id: int, session: Session = Depen session.commit() return {"message": "Relationship removed successfully"} -# Get relationships -@router.get("/relationships/genre-book", response_model=List[GenreBookLink]) -def get__genre_relationships(session: Session = Depends(get_session)): - relationships = session.exec(select(GenreBookLink)).all() - return relationships -# Get author's books -@router.get("/authors/{author_id}/books/", response_model=List[BookRead]) -def get_books_for_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") +# Get genre's books +@router.get( + "/genres/{author_id}/books/", + response_model=List[BookRead], + summary="Получить книги, написанные в жанре", + description="Возвращает все книги в системе в этом жанре", +) +def get_books_for_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(AuthorBookLink) - .where(AuthorBookLink.author_id == author_id) + select(Book).join(GenreBookLink).where(GenreBookLink.author_id == genre_id) ).all() return [BookRead(**book.model_dump()) for book in books] -# Get book's authors -@router.get("/books/{book_id}/authors/", response_model=List[AuthorRead]) + +# Get book's genres +@router.get( + "/books/{book_id}/genres/", + response_model=List[GenreRead], + summary="Получить жанры книги", + description="Возвращает все жанры книги в системе", +) 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) + genres = session.exec( + select(Genre).join(GenreBookLink).where(GenreBookLink.book_id == book_id) ).all() - return [AuthorRead(**author.model_dump()) for author in authors] + return [GenreRead(**author.model_dump()) for genre in genres] diff --git a/library_service/settings.py b/library_service/settings.py index 7ae2ef8..7994cef 100644 --- a/library_service/settings.py +++ b/library_service/settings.py @@ -9,6 +9,7 @@ load_dotenv() with open("pyproject.toml") as f: config = load(f) + # Dependency to get the FastAPI application instance def get_app() -> FastAPI: return FastAPI( @@ -35,10 +36,11 @@ def get_app() -> FastAPI: { "name": "misc", "description": "Miscellaneous operations.", - } - ] + }, + ], ) + USER = os.getenv("POSTGRES_USER") PASSWORD = os.getenv("POSTGRES_PASSWORD") DATABASE = os.getenv("POSTGRES_DB") @@ -50,6 +52,7 @@ if not USER or not PASSWORD or not DATABASE or not HOST: POSTGRES_DATABASE_URL = f"postgresql://{USER}:{PASSWORD}@{HOST}:5432/{DATABASE}" engine = create_engine(POSTGRES_DATABASE_URL, echo=True, future=True) + # Dependency to get a database session def get_session(): with Session(engine) as session: diff --git a/library_service/templates/index.html b/library_service/templates/index.html index 4c37657..a1307fa 100644 --- a/library_service/templates/index.html +++ b/library_service/templates/index.html @@ -43,6 +43,7 @@
+Description: {{ app_info.description }}
Version: {{ app_info.version }}
@@ -51,6 +52,9 @@