mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 12:31:09 +00:00
Исправление фильтров, добавление ссылки на главную в документации
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
# Конфигурация
|
||||
USERNAME = "sys-admin"
|
||||
PASSWORD = "wTKPVqTIMqzXL2EZxYz80w"
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
|
||||
class LibraryAPI:
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url
|
||||
self.token: Optional[str] = None
|
||||
self.session = requests.Session()
|
||||
|
||||
def login(self, username: str, password: str) -> bool:
|
||||
"""Авторизация и получение токена"""
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/auth/token",
|
||||
data={"username": username, "password": password},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
self.token = response.json()["access_token"]
|
||||
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
|
||||
print(f"✓ Авторизация успешна для пользователя: {username}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Ошибка авторизации: {response.text}")
|
||||
return False
|
||||
|
||||
def register(self, username: str, email: str, password: str, full_name: str = None) -> bool:
|
||||
"""Регистрация нового пользователя"""
|
||||
data = {
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": password
|
||||
}
|
||||
if full_name:
|
||||
data["full_name"] = full_name
|
||||
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/auth/register",
|
||||
json=data
|
||||
)
|
||||
if response.status_code == 201:
|
||||
print(f"✓ Пользователь {username} зарегистрирован")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Ошибка регистрации: {response.text}")
|
||||
return False
|
||||
|
||||
def create_author(self, name: str) -> Optional[int]:
|
||||
"""Создание автора"""
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/authors/",
|
||||
json={"name": name}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
author_id = response.json()["id"]
|
||||
print(f" ✓ Автор создан: {name} (ID: {author_id})")
|
||||
return author_id
|
||||
else:
|
||||
print(f" ✗ Ошибка создания автора {name}: {response.text}")
|
||||
return None
|
||||
|
||||
def create_book(self, title: str, description: str) -> Optional[int]:
|
||||
"""Создание книги"""
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/books/",
|
||||
json={"title": title, "description": description}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
book_id = response.json()["id"]
|
||||
print(f" ✓ Книга создана: {title} (ID: {book_id})")
|
||||
return book_id
|
||||
else:
|
||||
print(f" ✗ Ошибка создания книги {title}: {response.text}")
|
||||
return None
|
||||
|
||||
def create_genre(self, name: str) -> Optional[int]:
|
||||
"""Создание жанра"""
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/genres/",
|
||||
json={"name": name}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
genre_id = response.json()["id"]
|
||||
print(f" ✓ Жанр создан: {name} (ID: {genre_id})")
|
||||
return genre_id
|
||||
else:
|
||||
print(f" ✗ Ошибка создания жанра {name}: {response.text}")
|
||||
return None
|
||||
|
||||
def link_author_book(self, author_id: int, book_id: int) -> bool:
|
||||
"""Связь автора и книги"""
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/relationships/author-book",
|
||||
params={"author_id": author_id, "book_id": book_id}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
print(f" ↔ Связь автор-книга: {author_id} ↔ {book_id}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ Ошибка связи автор-книга: {response.text}")
|
||||
return False
|
||||
|
||||
def link_genre_book(self, genre_id: int, book_id: int) -> bool:
|
||||
"""Связь жанра и книги"""
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/relationships/genre-book",
|
||||
params={"genre_id": genre_id, "book_id": book_id}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
print(f" ↔ Связь жанр-книга: {genre_id} ↔ {book_id}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ Ошибка связи жанр-книга: {response.text}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
api = LibraryAPI(BASE_URL)
|
||||
|
||||
# Авторизация
|
||||
if not api.login(USERNAME, PASSWORD):
|
||||
print("Не удалось авторизоваться. Проверьте логин и пароль.")
|
||||
return
|
||||
|
||||
# === АВТОРЫ (12 авторов) ===
|
||||
print("\n📚 Создание авторов...")
|
||||
authors_data = [
|
||||
"Лев Толстой",
|
||||
"Фёдор Достоевский",
|
||||
"Антон Чехов",
|
||||
"Александр Пушкин",
|
||||
"Михаил Булгаков",
|
||||
"Николай Гоголь",
|
||||
"Иван Тургенев",
|
||||
"Борис Пастернак",
|
||||
"Михаил Лермонтов",
|
||||
"Александр Солженицын",
|
||||
"Максим Горький",
|
||||
"Иван Бунин"
|
||||
]
|
||||
|
||||
authors = {}
|
||||
for name in authors_data:
|
||||
author_id = api.create_author(name)
|
||||
if author_id:
|
||||
authors[name] = author_id
|
||||
|
||||
# === ЖАНРЫ (8 жанров) ===
|
||||
print("\n🏷️ Создание жанров...")
|
||||
genres_data = [
|
||||
"Роман",
|
||||
"Повесть",
|
||||
"Рассказ",
|
||||
"Поэзия",
|
||||
"Драма",
|
||||
"Философская проза",
|
||||
"Историческая проза",
|
||||
"Сатира"
|
||||
]
|
||||
|
||||
genres = {}
|
||||
for name in genres_data:
|
||||
genre_id = api.create_genre(name)
|
||||
if genre_id:
|
||||
genres[name] = genre_id
|
||||
|
||||
# === КНИГИ (25 книг) ===
|
||||
print("\n📖 Создание книг...")
|
||||
books_data = [
|
||||
{
|
||||
"title": "Война и мир",
|
||||
"description": "Роман-эпопея Льва Толстого, описывающий русское общество в эпоху войн против Наполеона в 1805—1812 годах. Одно из величайших произведений мировой литературы.",
|
||||
"authors": ["Лев Толстой"],
|
||||
"genres": ["Роман", "Историческая проза"]
|
||||
},
|
||||
{
|
||||
"title": "Анна Каренина",
|
||||
"description": "Роман Льва Толстого о трагической любви замужней дамы Анны Карениной к блестящему офицеру Вронскому. История страсти, ревности и роковых решений.",
|
||||
"authors": ["Лев Толстой"],
|
||||
"genres": ["Роман", "Драма"]
|
||||
},
|
||||
{
|
||||
"title": "Преступление и наказание",
|
||||
"description": "Социально-психологический роман Фёдора Достоевского о бедном студенте Раскольникове, совершившем убийство и мучающемся угрызениями совести.",
|
||||
"authors": ["Фёдор Достоевский"],
|
||||
"genres": ["Роман", "Философская проза"]
|
||||
},
|
||||
{
|
||||
"title": "Братья Карамазовы",
|
||||
"description": "Последний роман Достоевского, история семьи Карамазовых, затрагивающая глубокие вопросы веры, свободы воли и морали.",
|
||||
"authors": ["Фёдор Достоевский"],
|
||||
"genres": ["Роман", "Философская проза", "Драма"]
|
||||
},
|
||||
{
|
||||
"title": "Идиот",
|
||||
"description": "Роман о князе Мышкине — человеке с чистой душой, который сталкивается с жестокостью и корыстью петербургского общества.",
|
||||
"authors": ["Фёдор Достоевский"],
|
||||
"genres": ["Роман", "Философская проза"]
|
||||
},
|
||||
{
|
||||
"title": "Вишнёвый сад",
|
||||
"description": "Пьеса Антона Чехова о разорении дворянского гнезда и продаже родового имения с вишнёвым садом.",
|
||||
"authors": ["Антон Чехов"],
|
||||
"genres": ["Драма"]
|
||||
},
|
||||
{
|
||||
"title": "Чайка",
|
||||
"description": "Пьеса Чехова о любви, искусстве и несбывшихся мечтах, разворачивающаяся в усадьбе на берегу озера.",
|
||||
"authors": ["Антон Чехов"],
|
||||
"genres": ["Драма"]
|
||||
},
|
||||
{
|
||||
"title": "Палата № 6",
|
||||
"description": "Повесть о враче психиатрической больницы, который начинает сомневаться в границах между нормой и безумием.",
|
||||
"authors": ["Антон Чехов"],
|
||||
"genres": ["Повесть", "Философская проза"]
|
||||
},
|
||||
{
|
||||
"title": "Евгений Онегин",
|
||||
"description": "Роман в стихах Александра Пушкина — энциклопедия русской жизни начала XIX века и история несчастной любви.",
|
||||
"authors": ["Александр Пушкин"],
|
||||
"genres": ["Роман", "Поэзия"]
|
||||
},
|
||||
{
|
||||
"title": "Капитанская дочка",
|
||||
"description": "Исторический роман Пушкина о событиях Пугачёвского восстания, любви и чести.",
|
||||
"authors": ["Александр Пушкин"],
|
||||
"genres": ["Роман", "Историческая проза"]
|
||||
},
|
||||
{
|
||||
"title": "Пиковая дама",
|
||||
"description": "Повесть о молодом офицере Германне, одержимом желанием узнать тайну трёх карт.",
|
||||
"authors": ["Александр Пушкин"],
|
||||
"genres": ["Повесть"]
|
||||
},
|
||||
{
|
||||
"title": "Мастер и Маргарита",
|
||||
"description": "Роман Михаила Булгакова о визите дьявола в Москву 1930-х годов, переплетённый с историей Понтия Пилата.",
|
||||
"authors": ["Михаил Булгаков"],
|
||||
"genres": ["Роман", "Сатира", "Философская проза"]
|
||||
},
|
||||
{
|
||||
"title": "Собачье сердце",
|
||||
"description": "Повесть-сатира о профессоре Преображенском, превратившем бродячего пса в человека.",
|
||||
"authors": ["Михаил Булгаков"],
|
||||
"genres": ["Повесть", "Сатира"]
|
||||
},
|
||||
{
|
||||
"title": "Белая гвардия",
|
||||
"description": "Роман о семье Турбиных в Киеве во время Гражданской войны 1918-1919 годов.",
|
||||
"authors": ["Михаил Булгаков"],
|
||||
"genres": ["Роман", "Историческая проза"]
|
||||
},
|
||||
{
|
||||
"title": "Мёртвые души",
|
||||
"description": "Поэма Николая Гоголя о похождениях Чичикова, скупающего «мёртвые души» крепостных крестьян.",
|
||||
"authors": ["Николай Гоголь"],
|
||||
"genres": ["Роман", "Сатира"]
|
||||
},
|
||||
{
|
||||
"title": "Ревизор",
|
||||
"description": "Комедия о чиновниках уездного города, принявших проезжего за ревизора из Петербурга.",
|
||||
"authors": ["Николай Гоголь"],
|
||||
"genres": ["Драма", "Сатира"]
|
||||
},
|
||||
{
|
||||
"title": "Шинель",
|
||||
"description": "Повесть о маленьком человеке — титулярном советнике Акакии Башмачкине и его мечте о новой шинели.",
|
||||
"authors": ["Николай Гоголь"],
|
||||
"genres": ["Повесть"]
|
||||
},
|
||||
{
|
||||
"title": "Отцы и дети",
|
||||
"description": "Роман Ивана Тургенева о конфликте поколений и нигилизме на примере Евгения Базарова.",
|
||||
"authors": ["Иван Тургенев"],
|
||||
"genres": ["Роман", "Философская проза"]
|
||||
},
|
||||
{
|
||||
"title": "Записки охотника",
|
||||
"description": "Цикл рассказов Тургенева о русской деревне и крестьянах, написанный с глубоким сочувствием к народу.",
|
||||
"authors": ["Иван Тургенев"],
|
||||
"genres": ["Рассказ"]
|
||||
},
|
||||
{
|
||||
"title": "Доктор Живаго",
|
||||
"description": "Роман Бориса Пастернака о судьбе русского интеллигента в эпоху революции и Гражданской войны.",
|
||||
"authors": ["Борис Пастернак"],
|
||||
"genres": ["Роман", "Историческая проза", "Поэзия"]
|
||||
},
|
||||
{
|
||||
"title": "Герой нашего времени",
|
||||
"description": "Роман Михаила Лермонтова о Печорине — «лишнем человеке», скучающем и разочарованном в жизни.",
|
||||
"authors": ["Михаил Лермонтов"],
|
||||
"genres": ["Роман", "Философская проза"]
|
||||
},
|
||||
{
|
||||
"title": "Архипелаг ГУЛАГ",
|
||||
"description": "Документально-художественное исследование Александра Солженицына о системе советских лагерей.",
|
||||
"authors": ["Александр Солженицын"],
|
||||
"genres": ["Историческая проза"]
|
||||
},
|
||||
{
|
||||
"title": "Один день Ивана Денисовича",
|
||||
"description": "Повесть о одном дне заключённого советского лагеря, положившая начало лагерной прозе.",
|
||||
"authors": ["Александр Солженицын"],
|
||||
"genres": ["Повесть", "Историческая проза"]
|
||||
},
|
||||
{
|
||||
"title": "На дне",
|
||||
"description": "Пьеса Максима Горького о жителях ночлежки для бездомных — людях, оказавшихся на дне жизни.",
|
||||
"authors": ["Максим Горький"],
|
||||
"genres": ["Драма", "Философская проза"]
|
||||
},
|
||||
{
|
||||
"title": "Тёмные аллеи",
|
||||
"description": "Сборник рассказов Ивана Бунина о любви — трагической, мимолётной и прекрасной.",
|
||||
"authors": ["Иван Бунин"],
|
||||
"genres": ["Рассказ"]
|
||||
}
|
||||
]
|
||||
|
||||
books = {}
|
||||
for book in books_data:
|
||||
book_id = api.create_book(book["title"], book["description"])
|
||||
if book_id:
|
||||
books[book["title"]] = {
|
||||
"id": book_id,
|
||||
"authors": book["authors"],
|
||||
"genres": book["genres"]
|
||||
}
|
||||
|
||||
# === СОЗДАНИЕ СВЯЗЕЙ ===
|
||||
print("\n🔗 Создание связей...")
|
||||
|
||||
for book_title, book_info in books.items():
|
||||
book_id = book_info["id"]
|
||||
|
||||
# Связи с авторами
|
||||
for author_name in book_info["authors"]:
|
||||
if author_name in authors:
|
||||
api.link_author_book(authors[author_name], book_id)
|
||||
|
||||
# Связи с жанрами
|
||||
for genre_name in book_info["genres"]:
|
||||
if genre_name in genres:
|
||||
api.link_genre_book(genres[genre_name], book_id)
|
||||
|
||||
# === ИТОГИ ===
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 ИТОГИ:")
|
||||
print(f" • Авторов создано: {len(authors)}")
|
||||
print(f" • Жанров создано: {len(genres)}")
|
||||
print(f" • Книг создано: {len(books)}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -7,7 +7,7 @@ from sqlmodel import Session, select, col, func
|
||||
from library_service.auth import RequireAuth
|
||||
from library_service.settings import get_session
|
||||
from library_service.models.db import Author, AuthorBookLink, Book
|
||||
from library_service.models.dto import AuthorRead, BookCreate, BookList, BookRead, BookUpdate
|
||||
from library_service.models.dto import AuthorRead, BookCreate, BookList, BookRead, BookUpdate, GenreRead
|
||||
from library_service.models.dto.combined import (
|
||||
BookWithAuthorsAndGenres,
|
||||
BookFilteredList
|
||||
@@ -17,6 +17,54 @@ from library_service.models.dto.combined import (
|
||||
router = APIRouter(prefix="/books", tags=["books"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/filter",
|
||||
response_model=BookFilteredList,
|
||||
summary="Фильтрация книг",
|
||||
description="Фильтрация списка книг по названию, авторам и жанрам с пагинацией"
|
||||
)
|
||||
def filter_books(
|
||||
session: Session = Depends(get_session),
|
||||
q: str | None = Query(None, min_length=3, max_length=50, description="Поиск"),
|
||||
author_ids: List[int] | None = Query(None, description="Список ID авторов"),
|
||||
genre_ids: List[int] | None = Query(None, description="Список ID жанров"),
|
||||
page: int = Query(1, gt=0, description="Номер страницы"),
|
||||
size: int = Query(20, gt=0, lt=101, description="Количество элементов на странице"),
|
||||
):
|
||||
"""Эндпоинт получения отфильтрованного списка книг"""
|
||||
statement = select(Book).distinct()
|
||||
|
||||
if q:
|
||||
statement = statement.where(
|
||||
(col(Book.title).ilike(f"%{q}%")) | (col(Book.description).ilike(f"%{q}%"))
|
||||
)
|
||||
|
||||
if author_ids:
|
||||
statement = statement.join(AuthorBookLink).where(AuthorBookLink.author_id.in_(author_ids))
|
||||
|
||||
if genre_ids:
|
||||
statement = statement.join(GenreBookLink).where(GenreBookLink.genre_id.in_(genre_ids))
|
||||
|
||||
total_statement = select(func.count()).select_from(statement.subquery())
|
||||
total = session.exec(total_statement).one()
|
||||
|
||||
offset = (page - 1) * size
|
||||
statement = statement.offset(offset).limit(size)
|
||||
results = session.exec(statement).all()
|
||||
|
||||
books_with_data = []
|
||||
for db_book in results:
|
||||
books_with_data.append(
|
||||
BookWithAuthorsAndGenres(
|
||||
**db_book.model_dump(),
|
||||
authors=[AuthorRead(**a.model_dump()) for a in db_book.authors],
|
||||
genres=[GenreRead(**g.model_dump()) for g in db_book.genres]
|
||||
)
|
||||
)
|
||||
|
||||
return BookFilteredList(books=books_with_data, total=total)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=Book,
|
||||
@@ -127,51 +175,3 @@ def delete_book(
|
||||
session.delete(book)
|
||||
session.commit()
|
||||
return book_read
|
||||
|
||||
|
||||
@router.get(
|
||||
"/filter",
|
||||
response_model=BookFilteredList,
|
||||
summary="Фильтрация книг",
|
||||
description="Фильтрация списка книг по названию, авторам и жанрам с пагинацией"
|
||||
)
|
||||
def filter_books(
|
||||
session: Session = Depends(get_session),
|
||||
q: str | None = Query(None, min_length=3, max_length=50, description="Поиск"),
|
||||
author_ids: List[int] | None = Query(None, description="Список ID авторов"),
|
||||
genre_ids: List[int] | None = Query(None, description="Список ID жанров"),
|
||||
page: int = Query(1, gt=0, description="Номер страницы"),
|
||||
size: int = Query(20, gt=0, lt=101, description="Количество элементов на странице"),
|
||||
):
|
||||
"""Эндпоинт получения отфильтрованного списка книг"""
|
||||
statement = select(Book).distinct()
|
||||
|
||||
if q:
|
||||
statement = statement.where(
|
||||
(col(Book.title).ilike(f"%{q}%")) | (col(Book.description).ilike(f"%{q}%"))
|
||||
)
|
||||
|
||||
if author_ids:
|
||||
statement = statement.join(AuthorBookLink).where(AuthorBookLink.author_id.in_(author_ids))
|
||||
|
||||
if genre_ids:
|
||||
statement = statement.join(GenreBookLink).where(GenreBookLink.genre_id.in_(genre_ids))
|
||||
|
||||
total_statement = select(func.count()).select_from(statement.subquery())
|
||||
total = session.exec(total_statement).one()
|
||||
|
||||
offset = (page - 1) * size
|
||||
statement = statement.offset(offset).limit(size)
|
||||
results = session.exec(statement).all()
|
||||
|
||||
books_with_data = []
|
||||
for db_book in results:
|
||||
books_with_data.append(
|
||||
BookWithAuthorsAndGenres(
|
||||
**db_book.model_dump(),
|
||||
authors=[AuthorRead(**a.model_dump()) for a in db_book.authors],
|
||||
genres=[GenreRead(**g.model_dump()) for g in db_book.genres]
|
||||
)
|
||||
)
|
||||
|
||||
return BookFilteredList(books=books_with_data, total=total)
|
||||
|
||||
@@ -22,33 +22,33 @@ def get_info(app) -> Dict:
|
||||
"app_info": {
|
||||
"title": app.title,
|
||||
"version": app.version,
|
||||
"description": app.description,
|
||||
"description": app.description.rsplit('|', 1)[0],
|
||||
},
|
||||
"server_time": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/", include_in_schema=False)
|
||||
async def root(request: Request, app=Depends(get_app)):
|
||||
async def root(request: Request, app=Depends(lambda: get_app())):
|
||||
"""Эндпоинт главной страницы"""
|
||||
return RedirectResponse("/books")
|
||||
|
||||
|
||||
@router.get("/books", include_in_schema=False)
|
||||
async def books(request: Request, app=Depends(get_app)):
|
||||
async def books(request: Request, app=Depends(lambda: get_app())):
|
||||
"""Эндпоинт страницы выбора книг"""
|
||||
return templates.TemplateResponse(request, "books.html", get_info(app))
|
||||
|
||||
|
||||
@router.get("/auth", include_in_schema=False)
|
||||
async def root(request: Request, app=Depends(get_app)):
|
||||
async def auth(request: Request, app=Depends(lambda: get_app())):
|
||||
"""Эндпоинт страницы авторизации"""
|
||||
return templates.TemplateResponse(request, "auth.html", get_info(app))
|
||||
|
||||
|
||||
|
||||
@router.get("/api", include_in_schema=False)
|
||||
async def root(request: Request, app=Depends(get_app)):
|
||||
async def api(request: Request, app=Depends(lambda: get_app())):
|
||||
"""Страница с сылками на документацию API"""
|
||||
return templates.TemplateResponse(request, "api.html", get_info(app))
|
||||
|
||||
@@ -72,6 +72,6 @@ async def favicon():
|
||||
summary="Информация о сервисе",
|
||||
description="Возвращает информацию о системе",
|
||||
)
|
||||
async def api_info(app=Depends(get_app)):
|
||||
async def api_info(app=Depends(lambda: get_app())):
|
||||
"""Эндпоинт информации об API"""
|
||||
return JSONResponse(content=get_info(app))
|
||||
|
||||
@@ -12,12 +12,12 @@ with open("pyproject.toml", 'r', encoding='utf-8') as f:
|
||||
config = load(f)
|
||||
|
||||
|
||||
def get_app(lifespan=None) -> FastAPI:
|
||||
def get_app(lifespan=None, /) -> FastAPI:
|
||||
"""Dependency для получения экземпляра FastAPI application"""
|
||||
if not hasattr(get_app, 'instance'):
|
||||
get_app.instance = FastAPI(
|
||||
title=config["tool"]["poetry"]["name"],
|
||||
description=config["tool"]["poetry"]["description"],
|
||||
description=config["tool"]["poetry"]["description"] + " | [Вернутьсяна главную](/)",
|
||||
version=config["tool"]["poetry"]["version"],
|
||||
lifespan=lifespan,
|
||||
openapi_tags=[
|
||||
|
||||
Reference in New Issue
Block a user