diff --git a/data.py b/data.py new file mode 100644 index 0000000..c8675a5 --- /dev/null +++ b/data.py @@ -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() diff --git a/library_service/routers/books.py b/library_service/routers/books.py index 80c9d6d..a86ffe4 100644 --- a/library_service/routers/books.py +++ b/library_service/routers/books.py @@ -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) diff --git a/library_service/routers/misc.py b/library_service/routers/misc.py index 0b94be8..b1b96fc 100644 --- a/library_service/routers/misc.py +++ b/library_service/routers/misc.py @@ -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)) diff --git a/library_service/settings.py b/library_service/settings.py index 22e08fc..b2f8d8a 100644 --- a/library_service/settings.py +++ b/library_service/settings.py @@ -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=[