mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Добавление авторизации и фронтэнда
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
|
POSTGRES_HOST = "localhost"
|
||||||
|
POSTGRES_PORT = "5432"
|
||||||
POSTGRES_USER = "postgres"
|
POSTGRES_USER = "postgres"
|
||||||
POSTGRES_PASSWORD = "postgres"
|
POSTGRES_PASSWORD = "postgres"
|
||||||
POSTGRES_DB = "postgres"
|
POSTGRES_DB = "lib"
|
||||||
POSTGRES_SERVER = "db"
|
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@ services:
|
|||||||
- .:/code
|
- .:/code
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
depends_on:
|
# depends_on:
|
||||||
- db
|
# - db
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
container_name: tests
|
container_name: tests
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
"""Модуль авторизации и аутентификации"""
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from library_service.models.db import Role, User
|
||||||
|
from library_service.models.dto import TokenData
|
||||||
|
from library_service.settings import get_session
|
||||||
|
|
||||||
|
|
||||||
|
# Конфигурация из переменных окружения
|
||||||
|
ALGORITHM = os.getenv("ALGORITHM", "HS256")
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
|
||||||
|
REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "7"))
|
||||||
|
|
||||||
|
# Хэширование паролей
|
||||||
|
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||||
|
|
||||||
|
# OAuth2 схема
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""Проверка пароль по его хешу."""
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
"""Хэширование пароля."""
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
||||||
|
"""Создание JWT access токена."""
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
|
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
to_encode.update({"exp": expire, "type": "access"})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def create_refresh_token(data: dict) -> str:
|
||||||
|
"""Создание JWT refresh токена."""
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
||||||
|
to_encode.update({"exp": expire, "type": "refresh"})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def decode_token(token: str) -> TokenData:
|
||||||
|
"""Декодирование и проверка JWT токенов."""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
user_id: int = payload.get("user_id")
|
||||||
|
if username is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
return TokenData(username=username, user_id=user_id)
|
||||||
|
except JWTError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_user(session: Session, username: str, password: str) -> User | None:
|
||||||
|
"""Аутентификация пользователя по имени пользователя и паролю."""
|
||||||
|
statement = select(User).where(User.username == username)
|
||||||
|
user = session.exec(statement).first()
|
||||||
|
if not user or not verify_password(password, user.hashed_password):
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user(
|
||||||
|
token: Annotated[str, Depends(oauth2_scheme)],
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
) -> User:
|
||||||
|
"""Получить текущего авторизованного пользователя."""
|
||||||
|
token_data = decode_token(token)
|
||||||
|
|
||||||
|
user = session.get(User, token_data.user_id)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="User not found",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_active_user(
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)],
|
||||||
|
) -> User:
|
||||||
|
"""Получить текущего активного пользователя."""
|
||||||
|
if not current_user.is_active:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
def require_role(role_name: str):
|
||||||
|
"""Dependency, требующая выполнения определенной роли."""
|
||||||
|
|
||||||
|
def role_checker(current_user: User = Depends(get_current_active_user)) -> User:
|
||||||
|
user_roles = [role.name for role in current_user.roles]
|
||||||
|
if role_name not in user_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=f"Role '{role_name}' required",
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
return role_checker
|
||||||
|
|
||||||
|
|
||||||
|
# Создание dependencies
|
||||||
|
RequireAuth = Annotated[User, Depends(get_current_active_user)]
|
||||||
|
RequireAdmin = Annotated[User, Depends(require_role("admin"))]
|
||||||
|
RequireModerator = Annotated[User, Depends(require_role("moderator"))]
|
||||||
|
|
||||||
|
|
||||||
|
def seed_roles(session: Session) -> dict[str, Role]:
|
||||||
|
"""Создаёт роли по умолчанию, если их нет."""
|
||||||
|
default_roles = [
|
||||||
|
{"name": "admin", "description": "Администратор системы"},
|
||||||
|
{"name": "moderator", "description": "Модератор"},
|
||||||
|
{"name": "user", "description": "Обычный пользователь"},
|
||||||
|
]
|
||||||
|
|
||||||
|
roles = {}
|
||||||
|
for role_data in default_roles:
|
||||||
|
existing = session.exec(
|
||||||
|
select(Role).where(Role.name == role_data["name"])
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
roles[role_data["name"]] = existing
|
||||||
|
else:
|
||||||
|
role = Role(**role_data)
|
||||||
|
session.add(role)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(role)
|
||||||
|
roles[role_data["name"]] = role
|
||||||
|
print(f"[+] Created role: {role_data['name']}")
|
||||||
|
|
||||||
|
return roles
|
||||||
|
|
||||||
|
|
||||||
|
def seed_admin(session: Session, admin_role: Role) -> User | None:
|
||||||
|
"""Создаёт администратора по умолчанию, если нет ни одного."""
|
||||||
|
existing_admins = session.exec(
|
||||||
|
select(User).join(User.roles).where(Role.name == "admin")
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if existing_admins:
|
||||||
|
print(f"[*] Admin already exists: {existing_admins[0].username}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
admin_username = os.getenv("DEFAULT_ADMIN_USERNAME", "admin")
|
||||||
|
admin_email = os.getenv("DEFAULT_ADMIN_EMAIL", "admin@example.com")
|
||||||
|
admin_password = os.getenv("DEFAULT_ADMIN_PASSWORD")
|
||||||
|
|
||||||
|
if not admin_password:
|
||||||
|
import secrets
|
||||||
|
admin_password = secrets.token_urlsafe(16)
|
||||||
|
print(f"[!] Generated admin password: {admin_password}")
|
||||||
|
print("[!] Please save this password and set DEFAULT_ADMIN_PASSWORD env var")
|
||||||
|
|
||||||
|
admin_user = User(
|
||||||
|
username=admin_username,
|
||||||
|
email=admin_email,
|
||||||
|
full_name="Системный администратор",
|
||||||
|
hashed_password=get_password_hash(admin_password),
|
||||||
|
is_active=True,
|
||||||
|
is_verified=True,
|
||||||
|
)
|
||||||
|
admin_user.roles.append(admin_role)
|
||||||
|
|
||||||
|
session.add(admin_user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(admin_user)
|
||||||
|
|
||||||
|
print(f"[+] Created admin user: {admin_username}")
|
||||||
|
return admin_user
|
||||||
|
|
||||||
|
|
||||||
|
def run_seeds(session: Session) -> None:
|
||||||
|
"""Запускаем создание ролей и администратора."""
|
||||||
|
roles = seed_roles(session)
|
||||||
|
seed_admin(session, roles["admin"])
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect x="10" y="10" width="80" height="80" rx="4" ry="4" fill="#fff" stroke="#000" stroke-width="2"/><rect x="20" y="15" width="60" height="70" rx="10" ry="10"/><rect x="20" y="15" width="60" height="66" rx="10" ry="10" fill="#fff"/><rect x="20" y="15" width="60" height="62" rx="10" ry="10"/><rect x="20" y="15" width="60" height="60" rx="10" ry="10" fill="#fff"/><rect x="20" y="15" width="60" height="56" rx="10" ry="10"/><rect x="20" y="15" width="60" height="54" rx="10" ry="10" fill="#fff"/><rect x="20" y="15" width="60" height="50" rx="10" ry="10"/><rect x="20" y="15" width="60" height="48" rx="10" ry="10" fill="#fff"/><rect x="20" y="15" width="60" height="44" rx="10" ry="10"/><rect x="22" y="21" width="2" height="58" rx="10" ry="10" stroke="#000" stroke-width="4"/><rect x="22" y="55" width="4" height="26" rx="2" ry="15"/><text x="50" y="40" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" stroke="#fff" stroke-width=".5" fill="none" font-size="20">『LiB』</text></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,12 +1,12 @@
|
|||||||
|
"""Основной модуль"""
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from alembic import command
|
from alembic import command
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from toml import load
|
|
||||||
|
|
||||||
from .settings import engine, get_app
|
|
||||||
from .routers import api_router
|
from .routers import api_router
|
||||||
from .routers.misc import get_info
|
from .settings import engine, get_app
|
||||||
|
|
||||||
app = get_app()
|
app = get_app()
|
||||||
alembic_cfg = Config("alembic.ini")
|
alembic_cfg = Config("alembic.ini")
|
||||||
@@ -14,6 +14,7 @@ alembic_cfg = Config("alembic.ini")
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
"""Жизененый цикл сервиса"""
|
||||||
print("[+] Initializing...")
|
print("[+] Initializing...")
|
||||||
|
|
||||||
# Настройка базы данных
|
# Настройка базы данных
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from .dto import *
|
"""Модуль моделей"""
|
||||||
from .db import *
|
from .db import *
|
||||||
|
from .dto import *
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
|
"""Модуль моделей для базы данных"""
|
||||||
from .author import Author
|
from .author import Author
|
||||||
from .book import Book
|
from .book import Book
|
||||||
from .genre import Genre
|
from .genre import Genre
|
||||||
|
from .role import Role
|
||||||
|
from .user import User
|
||||||
from .links import (
|
from .links import (
|
||||||
AuthorBookLink,
|
AuthorBookLink,
|
||||||
GenreBookLink,
|
GenreBookLink,
|
||||||
AuthorWithBooks,
|
UserRoleLink
|
||||||
BookWithAuthors,
|
|
||||||
GenreWithBooks,
|
|
||||||
BookWithGenres,
|
|
||||||
BookWithAuthorsAndGenres,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Author",
|
"Author",
|
||||||
"Book",
|
"Book",
|
||||||
"Genre",
|
"Genre",
|
||||||
|
"Role",
|
||||||
|
"User",
|
||||||
"AuthorBookLink",
|
"AuthorBookLink",
|
||||||
"AuthorWithBooks",
|
|
||||||
"BookWithAuthors",
|
|
||||||
"GenreBookLink",
|
"GenreBookLink",
|
||||||
"GenreWithBooks",
|
"UserRoleLink",
|
||||||
"BookWithGenres",
|
|
||||||
"BookWithAuthorsAndGenres",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
from typing import List, Optional, TYPE_CHECKING
|
"""Модуль DB-моделей авторов"""
|
||||||
from sqlmodel import SQLModel, Field, Relationship
|
from typing import TYPE_CHECKING, List
|
||||||
from ..dto.author import AuthorBase
|
|
||||||
from .links import AuthorBookLink
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from library_service.models.dto.author import AuthorBase
|
||||||
|
from library_service.models.db.links import AuthorBookLink
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .book import Book
|
from .book import Book
|
||||||
|
|
||||||
|
|
||||||
class Author(AuthorBase, table=True):
|
class Author(AuthorBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True, index=True)
|
"""Модель автора в базе данных"""
|
||||||
|
id: int | None = Field(default=None, primary_key=True, index=True)
|
||||||
books: List["Book"] = Relationship(
|
books: List["Book"] = Relationship(
|
||||||
back_populates="authors", link_model=AuthorBookLink
|
back_populates="authors", link_model=AuthorBookLink
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from typing import List, Optional, TYPE_CHECKING
|
"""Модуль DB-моделей книг"""
|
||||||
from sqlmodel import SQLModel, Field, Relationship
|
from typing import TYPE_CHECKING, List
|
||||||
from ..dto.book import BookBase
|
|
||||||
from .links import AuthorBookLink, GenreBookLink
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from library_service.models.dto.book import BookBase
|
||||||
|
from library_service.models.db.links import AuthorBookLink, GenreBookLink
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .author import Author
|
from .author import Author
|
||||||
@@ -9,7 +12,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class Book(BookBase, table=True):
|
class Book(BookBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True, index=True)
|
"""Модель книги в базе данных"""
|
||||||
|
id: int | None = Field(default=None, primary_key=True, index=True)
|
||||||
authors: List["Author"] = Relationship(
|
authors: List["Author"] = Relationship(
|
||||||
back_populates="books", link_model=AuthorBookLink
|
back_populates="books", link_model=AuthorBookLink
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
from typing import List, Optional, TYPE_CHECKING
|
"""Модуль DB-моделей жанров"""
|
||||||
from sqlmodel import SQLModel, Field, Relationship
|
from typing import TYPE_CHECKING, List
|
||||||
from ..dto.genre import GenreBase
|
|
||||||
from .links import GenreBookLink
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from library_service.models.dto.genre import GenreBase
|
||||||
|
from library_service.models.db.links import GenreBookLink
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .book import Book
|
from .book import Book
|
||||||
|
|
||||||
|
|
||||||
class Genre(GenreBase, table=True):
|
class Genre(GenreBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True, index=True)
|
"""Модель жанра в базе данных"""
|
||||||
|
id: int | None = Field(default=None, primary_key=True, index=True)
|
||||||
books: List["Book"] = Relationship(
|
books: List["Book"] = Relationship(
|
||||||
back_populates="genres", link_model=GenreBookLink
|
back_populates="genres", link_model=GenreBookLink
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
|
"""Модуль связей между сущностями в БД"""
|
||||||
from sqlmodel import SQLModel, Field
|
from sqlmodel import SQLModel, Field
|
||||||
from typing import List
|
|
||||||
|
|
||||||
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):
|
class AuthorBookLink(SQLModel, table=True):
|
||||||
|
"""Модель связи автора и книги"""
|
||||||
author_id: int | None = Field(
|
author_id: int | None = Field(
|
||||||
default=None, foreign_key="author.id", primary_key=True
|
default=None, foreign_key="author.id", primary_key=True
|
||||||
)
|
)
|
||||||
@@ -14,26 +11,14 @@ class AuthorBookLink(SQLModel, table=True):
|
|||||||
|
|
||||||
|
|
||||||
class GenreBookLink(SQLModel, table=True):
|
class GenreBookLink(SQLModel, table=True):
|
||||||
|
"""Модель связи жанра и книги"""
|
||||||
genre_id: int | None = Field(default=None, foreign_key="genre.id", primary_key=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)
|
book_id: int | None = Field(default=None, foreign_key="book.id", primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
class AuthorWithBooks(AuthorRead):
|
class UserRoleLink(SQLModel, table=True):
|
||||||
books: List[BookRead] = Field(default_factory=list)
|
"""Модель связи роли и пользователя"""
|
||||||
|
__tablename__ = "user_roles"
|
||||||
|
|
||||||
|
user_id: int | None = Field(default=None, foreign_key="users.id", primary_key=True)
|
||||||
class BookWithAuthors(BookRead):
|
role_id: int | None = Field(default=None, foreign_key="roles.id", primary_key=True)
|
||||||
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)
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Модуль DB-моделей ролей"""
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from library_service.models.dto.role import RoleBase
|
||||||
|
from library_service.models.db.links import UserRoleLink
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Role(RoleBase, table=True):
|
||||||
|
"""Модель роли в базе данных"""
|
||||||
|
__tablename__ = "roles"
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True, index=True)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
users: List["User"] = Relationship(back_populates="roles", link_model=UserRoleLink)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Модуль DB-моделей пользователей"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from library_service.models.dto.user import UserBase
|
||||||
|
from library_service.models.db.links import UserRoleLink
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .role import Role
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserBase, table=True):
|
||||||
|
"""Модель пользователя в базе данных"""
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True, index=True)
|
||||||
|
hashed_password: str = Field(nullable=False)
|
||||||
|
is_active: bool = Field(default=True)
|
||||||
|
is_verified: bool = Field(default=False)
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
updated_at: datetime | None = Field(
|
||||||
|
default=None, sa_column_kwargs={"onupdate": datetime.utcnow}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
roles: List["Role"] = Relationship(back_populates="users", link_model=UserRoleLink)
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
from .author import AuthorBase, AuthorCreate, AuthorUpdate, AuthorRead, AuthorList
|
"""Модуль DTO-моделей"""
|
||||||
from .book import BookBase, BookCreate, BookUpdate, BookRead, BookList
|
from .author import AuthorBase, AuthorCreate, AuthorList, AuthorRead, AuthorUpdate
|
||||||
|
from .genre import GenreBase, GenreCreate, GenreList, GenreRead, GenreUpdate
|
||||||
from .genre import GenreBase, GenreCreate, GenreUpdate, GenreRead, GenreList
|
from .book import BookBase, BookCreate, BookList, BookRead, BookUpdate
|
||||||
|
from .role import RoleBase, RoleCreate, RoleList, RoleRead, RoleUpdate
|
||||||
|
from .user import UserBase, UserCreate, UserLogin, UserRead, UserUpdate
|
||||||
|
from .token import Token, TokenData
|
||||||
|
from .combined import (AuthorWithBooks, GenreWithBooks, BookWithAuthors, BookWithGenres,
|
||||||
|
BookWithAuthorsAndGenres, BookFilteredList)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AuthorBase",
|
"AuthorBase",
|
||||||
@@ -14,9 +19,22 @@ __all__ = [
|
|||||||
"BookUpdate",
|
"BookUpdate",
|
||||||
"BookRead",
|
"BookRead",
|
||||||
"BookList",
|
"BookList",
|
||||||
|
"BookFilteredList",
|
||||||
"GenreBase",
|
"GenreBase",
|
||||||
"GenreCreate",
|
"GenreCreate",
|
||||||
"GenreUpdate",
|
"GenreUpdate",
|
||||||
"GenreRead",
|
"GenreRead",
|
||||||
"GenreList",
|
"GenreList",
|
||||||
|
"RoleBase",
|
||||||
|
"RoleCreate",
|
||||||
|
"RoleUpdate",
|
||||||
|
"RoleRead",
|
||||||
|
"RoleList",
|
||||||
|
"Token",
|
||||||
|
"TokenData",
|
||||||
|
"UserBase",
|
||||||
|
"UserCreate",
|
||||||
|
"UserRead",
|
||||||
|
"UserUpdate",
|
||||||
|
"UserLogin",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from sqlmodel import SQLModel
|
"""Модуль DTO-моделей авторов"""
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
from pydantic import ConfigDict
|
||||||
from typing import Optional, List
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
|
||||||
class AuthorBase(SQLModel):
|
class AuthorBase(SQLModel):
|
||||||
|
"""Базовая модель автора"""
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
model_config = ConfigDict( # pyright: ignore
|
model_config = ConfigDict( # pyright: ignore
|
||||||
@@ -12,17 +15,21 @@ class AuthorBase(SQLModel):
|
|||||||
|
|
||||||
|
|
||||||
class AuthorCreate(AuthorBase):
|
class AuthorCreate(AuthorBase):
|
||||||
|
"""Модель автора для создания"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AuthorUpdate(SQLModel):
|
class AuthorUpdate(SQLModel):
|
||||||
name: Optional[str] = None
|
"""Модель автора для обновления"""
|
||||||
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuthorRead(AuthorBase):
|
class AuthorRead(AuthorBase):
|
||||||
|
"""Модель автора для чтения"""
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
class AuthorList(SQLModel):
|
class AuthorList(SQLModel):
|
||||||
|
"""Список авторов"""
|
||||||
authors: List[AuthorRead]
|
authors: List[AuthorRead]
|
||||||
total: int
|
total: int
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
from sqlmodel import SQLModel
|
"""Модуль DTO-моделей книг"""
|
||||||
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
from pydantic import ConfigDict
|
||||||
from typing import Optional, List
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .combined import BookWithAuthorsAndGenres
|
||||||
|
|
||||||
|
|
||||||
class BookBase(SQLModel):
|
class BookBase(SQLModel):
|
||||||
|
"""Базовая модель книги"""
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
|
|
||||||
@@ -15,18 +21,22 @@ class BookBase(SQLModel):
|
|||||||
|
|
||||||
|
|
||||||
class BookCreate(BookBase):
|
class BookCreate(BookBase):
|
||||||
|
"""Модель книги для создания"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BookUpdate(SQLModel):
|
class BookUpdate(SQLModel):
|
||||||
title: Optional[str] = None
|
"""Модель книги для обновления"""
|
||||||
description: Optional[str] = None
|
title: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class BookRead(BookBase):
|
class BookRead(BookBase):
|
||||||
|
"""Модель книги для чтения"""
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
class BookList(SQLModel):
|
class BookList(SQLModel):
|
||||||
|
"""Список книг"""
|
||||||
books: List[BookRead]
|
books: List[BookRead]
|
||||||
total: int
|
total: int
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Модуль объединёных объектов"""
|
||||||
|
from typing import List
|
||||||
|
from sqlmodel import SQLModel, Field
|
||||||
|
|
||||||
|
from .author import AuthorRead
|
||||||
|
from .genre import GenreRead
|
||||||
|
from .book import BookRead
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorWithBooks(SQLModel):
|
||||||
|
"""Модель автора с книгами"""
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
bio: str
|
||||||
|
books: List[BookRead] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class GenreWithBooks(SQLModel):
|
||||||
|
"""Модель жанра с книгами"""
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
books: List[BookRead] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class BookWithAuthors(SQLModel):
|
||||||
|
"""Модель книги с авторами"""
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
authors: List[AuthorRead] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class BookWithGenres(SQLModel):
|
||||||
|
"""Модель книги с жанрами"""
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
genres: List[GenreRead] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class BookWithAuthorsAndGenres(SQLModel):
|
||||||
|
"""Модель с авторами и жанрами"""
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
authors: List[AuthorRead] = Field(default_factory=list)
|
||||||
|
genres: List[GenreRead] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class BookFilteredList(SQLModel):
|
||||||
|
"""Список книг с фильтрацией"""
|
||||||
|
books: List[BookWithAuthorsAndGenres]
|
||||||
|
total: int
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
from sqlmodel import SQLModel
|
"""Модуль DTO-моделей жанров"""
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
from pydantic import ConfigDict
|
||||||
from typing import Optional, List
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
|
||||||
class GenreBase(SQLModel):
|
class GenreBase(SQLModel):
|
||||||
|
"""Базовая модель жанра"""
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
model_config = ConfigDict( # pyright: ignore
|
model_config = ConfigDict( # pyright: ignore
|
||||||
@@ -12,17 +15,21 @@ class GenreBase(SQLModel):
|
|||||||
|
|
||||||
|
|
||||||
class GenreCreate(GenreBase):
|
class GenreCreate(GenreBase):
|
||||||
|
"""Модель жанра для создания"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GenreUpdate(SQLModel):
|
class GenreUpdate(SQLModel):
|
||||||
name: Optional[str] = None
|
"""Модель жанра для обновления"""
|
||||||
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class GenreRead(GenreBase):
|
class GenreRead(GenreBase):
|
||||||
|
"""Модель жанра для чтения"""
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
class GenreList(SQLModel):
|
class GenreList(SQLModel):
|
||||||
|
"""Списко жанров"""
|
||||||
genres: List[GenreRead]
|
genres: List[GenreRead]
|
||||||
total: int
|
total: int
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Модуль DTO-моделей ролей"""
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class RoleBase(SQLModel):
|
||||||
|
"""Базовая модель роли"""
|
||||||
|
name: str
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleCreate(RoleBase):
|
||||||
|
"""Модель роли для создания"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RoleUpdate(SQLModel):
|
||||||
|
"""Модель роли для обновления"""
|
||||||
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleRead(RoleBase):
|
||||||
|
"""Модель роли для чтения"""
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class RoleList(SQLModel):
|
||||||
|
"""Список ролей"""
|
||||||
|
roles: List[RoleRead]
|
||||||
|
total: int
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Модуль DTO-моделей токенов"""
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class Token(SQLModel):
|
||||||
|
"""Модель токена"""
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
refresh_token: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class TokenData(SQLModel):
|
||||||
|
"""Модель содержимого токена"""
|
||||||
|
username: str | None = None
|
||||||
|
user_id: int | None = None
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Модуль DTO-моделей пользователей"""
|
||||||
|
import re
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pydantic import ConfigDict, EmailStr, field_validator
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserBase(SQLModel):
|
||||||
|
"""Базовая модель пользователя"""
|
||||||
|
username: str = Field(min_length=3, max_length=50, index=True, unique=True)
|
||||||
|
email: EmailStr = Field(index=True, unique=True)
|
||||||
|
full_name: str | None = Field(default=None, max_length=100)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"username": "johndoe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"full_name": "John Doe",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
"""Модель пользователя для создания"""
|
||||||
|
password: str = Field(min_length=8, max_length=100)
|
||||||
|
|
||||||
|
@field_validator("password")
|
||||||
|
@classmethod
|
||||||
|
def validate_password(cls, v: str) -> str:
|
||||||
|
"""Валидация пароля"""
|
||||||
|
if not re.search(r"[A-Z]", v):
|
||||||
|
raise ValueError("Пароль должен содержать символы в верхнем регистре")
|
||||||
|
if not re.search(r"[a-z]", v):
|
||||||
|
raise ValueError("Пароль должен содержать символы в нижнем регистре")
|
||||||
|
if not re.search(r"\d", v):
|
||||||
|
raise ValueError("пароль должен содержать цифры")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogin(SQLModel):
|
||||||
|
"""Модель аутентификации для пользователя"""
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserRead(UserBase):
|
||||||
|
"""Модель пользователя для чтения"""
|
||||||
|
id: int
|
||||||
|
is_active: bool
|
||||||
|
is_verified: bool
|
||||||
|
roles: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(SQLModel):
|
||||||
|
"""Модель пользователя для обновления"""
|
||||||
|
email: EmailStr | None = None
|
||||||
|
full_name: str | None = None
|
||||||
|
password: str | None = None
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
"""Модуль объединения роутеров"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from .auth import router as auth_router
|
||||||
from .authors import router as authors_router
|
from .authors import router as authors_router
|
||||||
from .books import router as books_router
|
from .books import router as books_router
|
||||||
from .genres import router as genres_router
|
from .genres import router as genres_router
|
||||||
@@ -9,6 +11,7 @@ from .misc import router as misc_router
|
|||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
# Подключение всех маршрутов
|
# Подключение всех маршрутов
|
||||||
|
api_router.include_router(auth_router)
|
||||||
api_router.include_router(authors_router)
|
api_router.include_router(authors_router)
|
||||||
api_router.include_router(books_router)
|
api_router.include_router(books_router)
|
||||||
api_router.include_router(genres_router)
|
api_router.include_router(genres_router)
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""Модуль работы с авторизацией и аутентификацией пользователей"""
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from library_service.models.db import Role, User
|
||||||
|
from library_service.models.dto import Token, UserCreate, UserRead, UserUpdate
|
||||||
|
from library_service.settings import get_session
|
||||||
|
from library_service.auth import (ACCESS_TOKEN_EXPIRE_MINUTES, RequireAdmin,
|
||||||
|
RequireAuth, authenticate_user, get_password_hash,
|
||||||
|
create_access_token, create_refresh_token)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["authentication"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/register",
|
||||||
|
response_model=UserRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
summary="Регистрация нового пользователя",
|
||||||
|
description="Создает нового пользователя в системе",
|
||||||
|
)
|
||||||
|
def register(user_data: UserCreate, session: Session = Depends(get_session)):
|
||||||
|
"""Эндпоинт регистрации пользователя"""
|
||||||
|
# Проверка если username существует
|
||||||
|
existing_user = session.exec(
|
||||||
|
select(User).where(User.username == user_data.username)
|
||||||
|
).first()
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Username already registered",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверка если email существует
|
||||||
|
existing_email = session.exec(
|
||||||
|
select(User).where(User.email == user_data.email)
|
||||||
|
).first()
|
||||||
|
if existing_email:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создание пользователя
|
||||||
|
db_user = User(
|
||||||
|
**user_data.model_dump(exclude={"password"}),
|
||||||
|
hashed_password=get_password_hash(user_data.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Назначение роли по умолчанию
|
||||||
|
default_role = session.exec(select(Role).where(Role.name == "user")).first()
|
||||||
|
if default_role:
|
||||||
|
db_user.roles.append(default_role)
|
||||||
|
|
||||||
|
session.add(db_user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_user)
|
||||||
|
|
||||||
|
return UserRead(**db_user.model_dump(), roles=[role.name for role in db_user.roles])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/token",
|
||||||
|
response_model=Token,
|
||||||
|
summary="Получение токена",
|
||||||
|
description="Аутентификация и получение JWT токена",
|
||||||
|
)
|
||||||
|
def login(
|
||||||
|
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""Эндпоинт аутентификации и получения JWT токена"""
|
||||||
|
user = authenticate_user(session, form_data.username, form_data.password)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user.username, "user_id": user.id},
|
||||||
|
expires_delta=access_token_expires,
|
||||||
|
)
|
||||||
|
refresh_token = create_refresh_token(
|
||||||
|
data={"sub": user.username, "user_id": user.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
return Token(
|
||||||
|
access_token=access_token, refresh_token=refresh_token, token_type="bearer"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/me",
|
||||||
|
response_model=UserRead,
|
||||||
|
summary="Текущий пользователь",
|
||||||
|
description="Получить информацию о текущем авторизованном пользователе",
|
||||||
|
)
|
||||||
|
def read_users_me(current_user: RequireAuth):
|
||||||
|
"""Эндпоинт получения информации о себе"""
|
||||||
|
return UserRead(
|
||||||
|
**current_user.model_dump(), roles=[role.name for role in current_user.roles]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/me",
|
||||||
|
response_model=UserRead,
|
||||||
|
summary="Обновить профиль",
|
||||||
|
description="Обновить информацию текущего пользователя",
|
||||||
|
)
|
||||||
|
def update_user_me(
|
||||||
|
user_update: UserUpdate,
|
||||||
|
current_user: RequireAuth,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""Эндпоинт обновления пользователя"""
|
||||||
|
if user_update.email:
|
||||||
|
current_user.email = user_update.email
|
||||||
|
if user_update.full_name:
|
||||||
|
current_user.full_name = user_update.full_name
|
||||||
|
if user_update.password:
|
||||||
|
current_user.hashed_password = get_password_hash(user_update.password)
|
||||||
|
|
||||||
|
session.add(current_user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(current_user)
|
||||||
|
|
||||||
|
return UserRead(
|
||||||
|
**current_user.model_dump(), roles=[role.name for role in current_user.roles]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/users",
|
||||||
|
response_model=list[UserRead],
|
||||||
|
summary="Список пользователей",
|
||||||
|
description="Получить список всех пользователей (только для админов)",
|
||||||
|
)
|
||||||
|
def read_users(
|
||||||
|
admin: RequireAdmin,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
):
|
||||||
|
"""Эндпоинт получения списка всех пользователей"""
|
||||||
|
users = session.exec(select(User).offset(skip).limit(limit)).all()
|
||||||
|
return [
|
||||||
|
UserRead(**user.model_dump(), roles=[role.name for role in user.roles])
|
||||||
|
for user in users
|
||||||
|
]
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
from fastapi import APIRouter, Path, Depends, HTTPException
|
"""Модуль работы с авторами"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from library_service.auth import RequireAuth
|
||||||
from library_service.settings import get_session
|
from library_service.settings import get_session
|
||||||
from library_service.models.db import Author, AuthorBookLink, Book, AuthorWithBooks
|
from library_service.models.db import Author, AuthorBookLink, Book
|
||||||
from library_service.models.dto import (
|
from library_service.models.dto import (BookRead, AuthorWithBooks,
|
||||||
AuthorCreate,
|
AuthorCreate, AuthorList, AuthorRead, AuthorUpdate)
|
||||||
AuthorUpdate,
|
|
||||||
AuthorRead,
|
|
||||||
AuthorList,
|
|
||||||
BookRead,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/authors", tags=["authors"])
|
router = APIRouter(prefix="/authors", tags=["authors"])
|
||||||
|
|
||||||
|
|
||||||
# Create an author
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/",
|
"/",
|
||||||
response_model=AuthorRead,
|
response_model=AuthorRead,
|
||||||
summary="Создать автора",
|
summary="Создать автора",
|
||||||
description="Добавляет автора в систему",
|
description="Добавляет автора в систему",
|
||||||
)
|
)
|
||||||
def create_author(author: AuthorCreate, session: Session = Depends(get_session)):
|
def create_author(
|
||||||
|
current_user: RequireAuth,
|
||||||
|
author: AuthorCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""Эндпоинт создания автора"""
|
||||||
db_author = Author(**author.model_dump())
|
db_author = Author(**author.model_dump())
|
||||||
session.add(db_author)
|
session.add(db_author)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -30,7 +30,6 @@ def create_author(author: AuthorCreate, session: Session = Depends(get_session))
|
|||||||
return AuthorRead(**db_author.model_dump())
|
return AuthorRead(**db_author.model_dump())
|
||||||
|
|
||||||
|
|
||||||
# Read authors
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/",
|
"/",
|
||||||
response_model=AuthorList,
|
response_model=AuthorList,
|
||||||
@@ -38,6 +37,7 @@ def create_author(author: AuthorCreate, session: Session = Depends(get_session))
|
|||||||
description="Возвращает список всех авторов в системе",
|
description="Возвращает список всех авторов в системе",
|
||||||
)
|
)
|
||||||
def read_authors(session: Session = Depends(get_session)):
|
def read_authors(session: Session = Depends(get_session)):
|
||||||
|
"""Эндпоинт чтения списка авторов"""
|
||||||
authors = session.exec(select(Author)).all()
|
authors = session.exec(select(Author)).all()
|
||||||
return AuthorList(
|
return AuthorList(
|
||||||
authors=[AuthorRead(**author.model_dump()) for author in authors],
|
authors=[AuthorRead(**author.model_dump()) for author in authors],
|
||||||
@@ -45,7 +45,6 @@ def read_authors(session: Session = Depends(get_session)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Read an author with their books
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{author_id}",
|
"/{author_id}",
|
||||||
response_model=AuthorWithBooks,
|
response_model=AuthorWithBooks,
|
||||||
@@ -56,6 +55,7 @@ def get_author(
|
|||||||
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт чтения конкретного автора"""
|
||||||
author = session.get(Author, author_id)
|
author = session.get(Author, author_id)
|
||||||
if not author:
|
if not author:
|
||||||
raise HTTPException(status_code=404, detail="Author not found")
|
raise HTTPException(status_code=404, detail="Author not found")
|
||||||
@@ -72,7 +72,6 @@ def get_author(
|
|||||||
return AuthorWithBooks(**author_data)
|
return AuthorWithBooks(**author_data)
|
||||||
|
|
||||||
|
|
||||||
# Update an author
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/{author_id}",
|
"/{author_id}",
|
||||||
response_model=AuthorRead,
|
response_model=AuthorRead,
|
||||||
@@ -80,10 +79,12 @@ def get_author(
|
|||||||
description="Обновляет информацию об авторе в системе",
|
description="Обновляет информацию об авторе в системе",
|
||||||
)
|
)
|
||||||
def update_author(
|
def update_author(
|
||||||
|
current_user: RequireAuth,
|
||||||
author: AuthorUpdate,
|
author: AuthorUpdate,
|
||||||
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт обновления автора"""
|
||||||
db_author = session.get(Author, author_id)
|
db_author = session.get(Author, author_id)
|
||||||
if not db_author:
|
if not db_author:
|
||||||
raise HTTPException(status_code=404, detail="Author not found")
|
raise HTTPException(status_code=404, detail="Author not found")
|
||||||
@@ -97,7 +98,6 @@ def update_author(
|
|||||||
return AuthorRead(**db_author.model_dump())
|
return AuthorRead(**db_author.model_dump())
|
||||||
|
|
||||||
|
|
||||||
# Delete an author
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{author_id}",
|
"/{author_id}",
|
||||||
response_model=AuthorRead,
|
response_model=AuthorRead,
|
||||||
@@ -105,9 +105,11 @@ def update_author(
|
|||||||
description="Удаляет автора из системы",
|
description="Удаляет автора из системы",
|
||||||
)
|
)
|
||||||
def delete_author(
|
def delete_author(
|
||||||
|
current_user: RequireAuth,
|
||||||
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт удаления автора"""
|
||||||
author = session.get(Author, author_id)
|
author = session.get(Author, author_id)
|
||||||
if not author:
|
if not author:
|
||||||
raise HTTPException(status_code=404, detail="Author not found")
|
raise HTTPException(status_code=404, detail="Author not found")
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
from fastapi import APIRouter, Path, Depends, HTTPException
|
"""Модуль работы с книгами"""
|
||||||
from sqlmodel import Session, select
|
from typing import List
|
||||||
|
|
||||||
from library_service.models.db.links import BookWithAuthorsAndGenres
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||||
|
from sqlmodel import Session, select, col, func
|
||||||
|
|
||||||
|
from library_service.auth import RequireAuth
|
||||||
from library_service.settings import get_session
|
from library_service.settings import get_session
|
||||||
from library_service.models.db import Author, Book, BookWithAuthors, AuthorBookLink
|
from library_service.models.db import Author, AuthorBookLink, Book
|
||||||
from library_service.models.dto import (
|
from library_service.models.dto import AuthorRead, BookCreate, BookList, BookRead, BookUpdate
|
||||||
AuthorRead,
|
from library_service.models.dto.combined import (
|
||||||
BookList,
|
BookWithAuthorsAndGenres,
|
||||||
BookRead,
|
BookFilteredList
|
||||||
BookCreate,
|
|
||||||
BookUpdate,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/books", tags=["books"])
|
router = APIRouter(prefix="/books", tags=["books"])
|
||||||
|
|
||||||
|
|
||||||
# Create a book
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/",
|
"/",
|
||||||
response_model=Book,
|
response_model=Book,
|
||||||
summary="Создать книгу",
|
summary="Создать книгу",
|
||||||
description="Добавляет книгу в систему",
|
description="Добавляет книгу в систему",
|
||||||
)
|
)
|
||||||
def create_book(book: BookCreate, session: Session = Depends(get_session)):
|
def create_book(
|
||||||
|
current_user: RequireAuth, book: BookCreate, session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""Эндпоинт создания книги"""
|
||||||
db_book = Book(**book.model_dump())
|
db_book = Book(**book.model_dump())
|
||||||
session.add(db_book)
|
session.add(db_book)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -31,7 +34,6 @@ def create_book(book: BookCreate, session: Session = Depends(get_session)):
|
|||||||
return BookRead(**db_book.model_dump())
|
return BookRead(**db_book.model_dump())
|
||||||
|
|
||||||
|
|
||||||
# Read books
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/",
|
"/",
|
||||||
response_model=BookList,
|
response_model=BookList,
|
||||||
@@ -39,13 +41,13 @@ def create_book(book: BookCreate, session: Session = Depends(get_session)):
|
|||||||
description="Возвращает список всех книг в системе",
|
description="Возвращает список всех книг в системе",
|
||||||
)
|
)
|
||||||
def read_books(session: Session = Depends(get_session)):
|
def read_books(session: Session = Depends(get_session)):
|
||||||
|
"""Эндпоинт чтения списка книг"""
|
||||||
books = session.exec(select(Book)).all()
|
books = session.exec(select(Book)).all()
|
||||||
return BookList(
|
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 and genres
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{book_id}",
|
"/{book_id}",
|
||||||
response_model=BookWithAuthorsAndGenres,
|
response_model=BookWithAuthorsAndGenres,
|
||||||
@@ -56,6 +58,7 @@ def get_book(
|
|||||||
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт чтения конкретной книги"""
|
||||||
book = session.get(Book, book_id)
|
book = session.get(Book, book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(status_code=404, detail="Book not found")
|
raise HTTPException(status_code=404, detail="Book not found")
|
||||||
@@ -76,10 +79,9 @@ def get_book(
|
|||||||
book_data["authors"] = author_reads
|
book_data["authors"] = author_reads
|
||||||
book_data["genres"] = genre_reads
|
book_data["genres"] = genre_reads
|
||||||
|
|
||||||
return BookWithAuthors(**book_data)
|
return BookWithAuthorsAndGenres(**book_data)
|
||||||
|
|
||||||
|
|
||||||
# Update a book
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/{book_id}",
|
"/{book_id}",
|
||||||
response_model=Book,
|
response_model=Book,
|
||||||
@@ -87,10 +89,12 @@ def get_book(
|
|||||||
description="Обновляет информацию о книге в системе",
|
description="Обновляет информацию о книге в системе",
|
||||||
)
|
)
|
||||||
def update_book(
|
def update_book(
|
||||||
|
current_user: RequireAuth,
|
||||||
book: BookUpdate,
|
book: BookUpdate,
|
||||||
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт обновления книги"""
|
||||||
db_book = session.get(Book, book_id)
|
db_book = session.get(Book, book_id)
|
||||||
if not db_book:
|
if not db_book:
|
||||||
raise HTTPException(status_code=404, detail="Book not found")
|
raise HTTPException(status_code=404, detail="Book not found")
|
||||||
@@ -102,7 +106,6 @@ def update_book(
|
|||||||
return db_book
|
return db_book
|
||||||
|
|
||||||
|
|
||||||
# Delete a book
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{book_id}",
|
"/{book_id}",
|
||||||
response_model=BookRead,
|
response_model=BookRead,
|
||||||
@@ -110,9 +113,11 @@ def update_book(
|
|||||||
description="Удаляет книгу их системы",
|
description="Удаляет книгу их системы",
|
||||||
)
|
)
|
||||||
def delete_book(
|
def delete_book(
|
||||||
|
current_user: RequireAuth,
|
||||||
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт удаления книги"""
|
||||||
book = session.get(Book, book_id)
|
book = session.get(Book, book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(status_code=404, detail="Book not found")
|
raise HTTPException(status_code=404, detail="Book not found")
|
||||||
@@ -122,3 +127,51 @@ def delete_book(
|
|||||||
session.delete(book)
|
session.delete(book)
|
||||||
session.commit()
|
session.commit()
|
||||||
return book_read
|
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)
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
from fastapi import APIRouter, Path, Depends, HTTPException
|
"""Модуль работы с жанрами"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from library_service.auth import RequireAuth
|
||||||
|
from library_service.models.db import Book, Genre, GenreBookLink
|
||||||
|
from library_service.models.dto import BookRead, GenreCreate, GenreList, GenreRead, GenreUpdate, GenreWithBooks
|
||||||
from library_service.settings import get_session
|
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/genres", tags=["genres"])
|
router = APIRouter(prefix="/genres", tags=["genres"])
|
||||||
|
|
||||||
|
|
||||||
# Create a genre
|
# Создание жанра
|
||||||
@router.post(
|
@router.post(
|
||||||
"/",
|
"/",
|
||||||
response_model=GenreRead,
|
response_model=GenreRead,
|
||||||
summary="Создать жанр",
|
summary="Создать жанр",
|
||||||
description="Добавляет жанр книг в систему",
|
description="Добавляет жанр книг в систему",
|
||||||
)
|
)
|
||||||
def create_genre(genre: GenreCreate, session: Session = Depends(get_session)):
|
def create_genre(
|
||||||
|
current_user: RequireAuth,
|
||||||
|
genre: GenreCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""Эндпоинт создания жанра"""
|
||||||
db_genre = Genre(**genre.model_dump())
|
db_genre = Genre(**genre.model_dump())
|
||||||
session.add(db_genre)
|
session.add(db_genre)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -30,7 +30,7 @@ def create_genre(genre: GenreCreate, session: Session = Depends(get_session)):
|
|||||||
return GenreRead(**db_genre.model_dump())
|
return GenreRead(**db_genre.model_dump())
|
||||||
|
|
||||||
|
|
||||||
# Read genres
|
# Чтение жанров
|
||||||
@router.get(
|
@router.get(
|
||||||
"/",
|
"/",
|
||||||
response_model=GenreList,
|
response_model=GenreList,
|
||||||
@@ -38,13 +38,14 @@ def create_genre(genre: GenreCreate, session: Session = Depends(get_session)):
|
|||||||
description="Возвращает список всех жанров в системе",
|
description="Возвращает список всех жанров в системе",
|
||||||
)
|
)
|
||||||
def read_genres(session: Session = Depends(get_session)):
|
def read_genres(session: Session = Depends(get_session)):
|
||||||
|
"""Эндпоинт чтения списка жанров"""
|
||||||
genres = session.exec(select(Genre)).all()
|
genres = session.exec(select(Genre)).all()
|
||||||
return GenreList(
|
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(
|
@router.get(
|
||||||
"/{genre_id}",
|
"/{genre_id}",
|
||||||
response_model=GenreWithBooks,
|
response_model=GenreWithBooks,
|
||||||
@@ -55,6 +56,7 @@ def get_genre(
|
|||||||
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт чтения конкретного жанра"""
|
||||||
genre = session.get(Genre, genre_id)
|
genre = session.get(Genre, genre_id)
|
||||||
if not genre:
|
if not genre:
|
||||||
raise HTTPException(status_code=404, detail="Genre not found")
|
raise HTTPException(status_code=404, detail="Genre not found")
|
||||||
@@ -71,7 +73,7 @@ def get_genre(
|
|||||||
return GenreWithBooks(**genre_data)
|
return GenreWithBooks(**genre_data)
|
||||||
|
|
||||||
|
|
||||||
# Update a genre
|
# Обновление жанра
|
||||||
@router.put(
|
@router.put(
|
||||||
"/{genre_id}",
|
"/{genre_id}",
|
||||||
response_model=GenreRead,
|
response_model=GenreRead,
|
||||||
@@ -79,10 +81,12 @@ def get_genre(
|
|||||||
description="Обновляет информацию о жанре в системе",
|
description="Обновляет информацию о жанре в системе",
|
||||||
)
|
)
|
||||||
def update_genre(
|
def update_genre(
|
||||||
|
current_user: RequireAuth,
|
||||||
genre: GenreUpdate,
|
genre: GenreUpdate,
|
||||||
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт обновления жанра"""
|
||||||
db_genre = session.get(Genre, genre_id)
|
db_genre = session.get(Genre, genre_id)
|
||||||
if not db_genre:
|
if not db_genre:
|
||||||
raise HTTPException(status_code=404, detail="Genre not found")
|
raise HTTPException(status_code=404, detail="Genre not found")
|
||||||
@@ -96,7 +100,7 @@ def update_genre(
|
|||||||
return GenreRead(**db_genre.model_dump())
|
return GenreRead(**db_genre.model_dump())
|
||||||
|
|
||||||
|
|
||||||
# Delete a genre
|
# Удаление жанра
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{genre_id}",
|
"/{genre_id}",
|
||||||
response_model=GenreRead,
|
response_model=GenreRead,
|
||||||
@@ -104,9 +108,11 @@ def update_genre(
|
|||||||
description="Удаляет автора из системы",
|
description="Удаляет автора из системы",
|
||||||
)
|
)
|
||||||
def delete_genre(
|
def delete_genre(
|
||||||
|
current_user: RequireAuth,
|
||||||
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
"""Эндпоинт удаления жанра"""
|
||||||
genre = session.get(Genre, genre_id)
|
genre = session.get(Genre, genre_id)
|
||||||
if not genre:
|
if not genre:
|
||||||
raise HTTPException(status_code=404, detail="Genre not found")
|
raise HTTPException(status_code=404, detail="Genre not found")
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
from fastapi import APIRouter, Path, Request
|
"""Модуль прочих эндпоинтов"""
|
||||||
from fastapi.params import Depends
|
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from library_service.settings import get_app
|
from library_service.settings import get_app
|
||||||
|
|
||||||
# Загрузка шаблонов
|
|
||||||
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
|
|
||||||
|
|
||||||
router = APIRouter(tags=["misc"])
|
router = APIRouter(tags=["misc"])
|
||||||
|
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
|
||||||
|
|
||||||
|
|
||||||
# Форматированная информация о приложении
|
|
||||||
def get_info(app) -> Dict:
|
def get_info(app) -> Dict:
|
||||||
|
"""Форматированная информация о приложении"""
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"app_info": {
|
"app_info": {
|
||||||
@@ -27,29 +28,49 @@ def get_info(app) -> Dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Эндпоинт главной страницы
|
|
||||||
@router.get("/", include_in_schema=False)
|
@router.get("/", include_in_schema=False)
|
||||||
async def root(request: Request, app=Depends(get_app)):
|
async def root(request: Request, app=Depends(get_app)):
|
||||||
|
"""Эндпоинт главной страницы"""
|
||||||
return templates.TemplateResponse(request, "index.html", get_info(app))
|
return templates.TemplateResponse(request, "index.html", get_info(app))
|
||||||
|
|
||||||
|
|
||||||
# Редирект иконки вкладки
|
@router.get("/api", include_in_schema=False)
|
||||||
|
async def root(request: Request, app=Depends(get_app)):
|
||||||
|
"""Страница с сылками на документацию API"""
|
||||||
|
return templates.TemplateResponse(request, "api.html", get_info(app))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/favicon.ico", include_in_schema=False)
|
@router.get("/favicon.ico", include_in_schema=False)
|
||||||
def redirect_favicon():
|
def redirect_favicon():
|
||||||
|
"""Редирект иконки вкладки"""
|
||||||
return RedirectResponse("/favicon.svg")
|
return RedirectResponse("/favicon.svg")
|
||||||
|
|
||||||
|
|
||||||
# Эндпоинт иконки вкладки
|
|
||||||
@router.get("/favicon.svg", include_in_schema=False)
|
@router.get("/favicon.svg", include_in_schema=False)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return FileResponse("library_service/favicon.svg", media_type="image/svg+xml")
|
"""Эндпоинт иконки вкладки"""
|
||||||
|
return FileResponse(
|
||||||
|
"library_service/static/favicon.svg", media_type="image/svg+xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/static/{path:path}", include_in_schema=False)
|
||||||
|
async def serve_static(path: str):
|
||||||
|
"""Статические файлы"""
|
||||||
|
static_dir = Path(__file__).parent.parent / "static"
|
||||||
|
file_path = static_dir / path
|
||||||
|
|
||||||
|
if not file_path.is_file() or not file_path.is_relative_to(static_dir):
|
||||||
|
return JSONResponse(status_code=404, content={"error": "File not found"})
|
||||||
|
|
||||||
|
return FileResponse(file_path)
|
||||||
|
|
||||||
|
|
||||||
# Эндпоинт информации об API
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/api/info",
|
"/api/info",
|
||||||
summary="Информация о сервисе",
|
summary="Информация о сервисе",
|
||||||
description="Возвращает информацию о системе",
|
description="Возвращает информацию о системе",
|
||||||
)
|
)
|
||||||
async def api_info(app=Depends(get_app)):
|
async def api_info(app=Depends(get_app)):
|
||||||
|
"""Эндпоинт информации об API"""
|
||||||
return JSONResponse(content=get_info(app))
|
return JSONResponse(content=get_info(app))
|
||||||
|
|||||||
@@ -1,15 +1,82 @@
|
|||||||
|
"""Модуль работы со связями"""
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from typing import List, Dict
|
|
||||||
|
|
||||||
from library_service.settings import get_session
|
from library_service.auth import RequireAuth
|
||||||
from library_service.models.db import Author, Book, Genre, AuthorBookLink, GenreBookLink
|
from library_service.models.db import Author, AuthorBookLink, Book, Genre, GenreBookLink
|
||||||
from library_service.models.dto import AuthorRead, BookRead, GenreRead
|
from library_service.models.dto import AuthorRead, BookRead, GenreRead
|
||||||
|
from library_service.settings import get_session
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(tags=["relations"])
|
router = APIRouter(tags=["relations"])
|
||||||
|
|
||||||
|
|
||||||
# Add author to book
|
def check_entity_exists(session, model, entity_id, entity_name):
|
||||||
|
"""Проверка существования связи между сущностями в БД"""
|
||||||
|
entity = session.get(model, entity_id)
|
||||||
|
if not entity:
|
||||||
|
raise HTTPException(status_code=404, detail=f"{entity_name} not found")
|
||||||
|
return entity
|
||||||
|
|
||||||
|
|
||||||
|
def add_relationship(session, link_model, id1, field1, id2, field2, detail):
|
||||||
|
"""Создание связи между сущностями в БД"""
|
||||||
|
existing_link = session.exec(
|
||||||
|
select(link_model)
|
||||||
|
.where(getattr(link_model, field1) == id1)
|
||||||
|
.where(getattr(link_model, field2) == id2)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_link:
|
||||||
|
raise HTTPException(status_code=400, detail=detail)
|
||||||
|
|
||||||
|
link = link_model(**{field1: id1, field2: id2})
|
||||||
|
session.add(link)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(link)
|
||||||
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
def remove_relationship(session, link_model, id1, field1, id2, field2):
|
||||||
|
"""Удаление связи между сущностями в БД"""
|
||||||
|
link = session.exec(
|
||||||
|
select(link_model)
|
||||||
|
.where(getattr(link_model, field1) == id1)
|
||||||
|
.where(getattr(link_model, field2) == id2)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not link:
|
||||||
|
raise HTTPException(status_code=404, detail="Relationship not found")
|
||||||
|
|
||||||
|
session.delete(link)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "Relationship removed successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_related(
|
||||||
|
session,
|
||||||
|
main_model,
|
||||||
|
main_id,
|
||||||
|
main_name,
|
||||||
|
related_model,
|
||||||
|
link_model,
|
||||||
|
link_main_field,
|
||||||
|
link_related_field,
|
||||||
|
read_model
|
||||||
|
):
|
||||||
|
"""Получение связанных в БД сущностей"""
|
||||||
|
check_entity_exists(session, main_model, main_id, main_name)
|
||||||
|
|
||||||
|
related = session.exec(
|
||||||
|
select(related_model).join(link_model)
|
||||||
|
.where(getattr(link_model, link_main_field) == main_id)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return [read_model(**obj.model_dump()) for obj in related]
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/relationships/author-book",
|
"/relationships/author-book",
|
||||||
response_model=AuthorBookLink,
|
response_model=AuthorBookLink,
|
||||||
@@ -17,33 +84,19 @@ router = APIRouter(tags=["relations"])
|
|||||||
description="Добавляет связь между автором и книгой в систему",
|
description="Добавляет связь между автором и книгой в систему",
|
||||||
)
|
)
|
||||||
def add_author_to_book(
|
def add_author_to_book(
|
||||||
author_id: int, book_id: int, session: Session = Depends(get_session)
|
current_user: RequireAuth,
|
||||||
|
author_id: int,
|
||||||
|
book_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
author = session.get(Author, author_id)
|
"""Эндпоинт добавления автора к книге"""
|
||||||
if not author:
|
check_entity_exists(session, Author, author_id, "Author")
|
||||||
raise HTTPException(status_code=404, detail="Author not found")
|
check_entity_exists(session, Book, book_id, "Book")
|
||||||
|
|
||||||
book = session.get(Book, book_id)
|
return add_relationship(session, AuthorBookLink,
|
||||||
if not book:
|
author_id, "author_id", book_id, "book_id", "Relationship already exists")
|
||||||
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(
|
@router.delete(
|
||||||
"/relationships/author-book",
|
"/relationships/author-book",
|
||||||
response_model=Dict[str, str],
|
response_model=Dict[str, str],
|
||||||
@@ -51,23 +104,16 @@ def add_author_to_book(
|
|||||||
description="Удаляет связь между автором и книгой в системе",
|
description="Удаляет связь между автором и книгой в системе",
|
||||||
)
|
)
|
||||||
def remove_author_from_book(
|
def remove_author_from_book(
|
||||||
author_id: int, book_id: int, session: Session = Depends(get_session)
|
current_user: RequireAuth,
|
||||||
|
author_id: int,
|
||||||
|
book_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
link = session.exec(
|
"""Эндпоинт удаления автора из книги"""
|
||||||
select(AuthorBookLink)
|
return remove_relationship(session, AuthorBookLink,
|
||||||
.where(AuthorBookLink.author_id == author_id)
|
author_id, "author_id", book_id, "book_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"}
|
|
||||||
|
|
||||||
|
|
||||||
# Get author's books
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/authors/{author_id}/books/",
|
"/authors/{author_id}/books/",
|
||||||
response_model=List[BookRead],
|
response_model=List[BookRead],
|
||||||
@@ -75,18 +121,12 @@ def remove_author_from_book(
|
|||||||
description="Возвращает все книги в системе, написанные автором",
|
description="Возвращает все книги в системе, написанные автором",
|
||||||
)
|
)
|
||||||
def get_books_for_author(author_id: int, session: Session = Depends(get_session)):
|
def get_books_for_author(author_id: int, session: Session = Depends(get_session)):
|
||||||
author = session.get(Author, author_id)
|
"""Эндпоинт получения книг, написанных автором"""
|
||||||
if not author:
|
return get_related(session,
|
||||||
raise HTTPException(status_code=404, detail="Author not found")
|
Author, author_id, "Author", Book,
|
||||||
|
AuthorBookLink, "author_id", "book_id", BookRead)
|
||||||
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(
|
@router.get(
|
||||||
"/books/{book_id}/authors/",
|
"/books/{book_id}/authors/",
|
||||||
response_model=List[AuthorRead],
|
response_model=List[AuthorRead],
|
||||||
@@ -94,18 +134,12 @@ def get_books_for_author(author_id: int, session: Session = Depends(get_session)
|
|||||||
description="Возвращает всех авторов книги в системе",
|
description="Возвращает всех авторов книги в системе",
|
||||||
)
|
)
|
||||||
def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||||
book = session.get(Book, book_id)
|
"""Эндпоинт получения авторов книги"""
|
||||||
if not book:
|
return get_related(session,
|
||||||
raise HTTPException(status_code=404, detail="Book not found")
|
Book, book_id, "Book", Author,
|
||||||
|
AuthorBookLink, "book_id", "author_id", AuthorRead)
|
||||||
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(
|
@router.post(
|
||||||
"/relationships/genre-book",
|
"/relationships/genre-book",
|
||||||
response_model=GenreBookLink,
|
response_model=GenreBookLink,
|
||||||
@@ -113,33 +147,19 @@ def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
|||||||
description="Добавляет связь между книгой и жанром в систему",
|
description="Добавляет связь между книгой и жанром в систему",
|
||||||
)
|
)
|
||||||
def add_genre_to_book(
|
def add_genre_to_book(
|
||||||
genre_id: int, book_id: int, session: Session = Depends(get_session)
|
current_user: RequireAuth,
|
||||||
|
genre_id: int,
|
||||||
|
book_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
genre = session.get(Genre, genre_id)
|
"""Эндпоинт добавления жанра к книге"""
|
||||||
if not genre:
|
check_entity_exists(session, Genre, genre_id, "Genre")
|
||||||
raise HTTPException(status_code=404, detail="Genre not found")
|
check_entity_exists(session, Book, book_id, "Book")
|
||||||
|
|
||||||
book = session.get(Book, book_id)
|
return add_relationship(session, GenreBookLink,
|
||||||
if not book:
|
genre_id, "genre_id", book_id, "book_id", "Relationship already exists")
|
||||||
raise HTTPException(status_code=404, detail="Book not found")
|
|
||||||
|
|
||||||
existing_link = session.exec(
|
|
||||||
select(GenreBookLink)
|
|
||||||
.where(GenreBookLink.genre_id == genre_id)
|
|
||||||
.where(GenreBookLink.book_id == book_id)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if existing_link:
|
|
||||||
raise HTTPException(status_code=400, detail="Relationship already exists")
|
|
||||||
|
|
||||||
link = GenreBookLink(genre_id=genre_id, book_id=book_id)
|
|
||||||
session.add(link)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(link)
|
|
||||||
return link
|
|
||||||
|
|
||||||
|
|
||||||
# Remove author from book
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/relationships/genre-book",
|
"/relationships/genre-book",
|
||||||
response_model=Dict[str, str],
|
response_model=Dict[str, str],
|
||||||
@@ -147,55 +167,37 @@ def add_genre_to_book(
|
|||||||
description="Удаляет связь между жанром и книгой в системе",
|
description="Удаляет связь между жанром и книгой в системе",
|
||||||
)
|
)
|
||||||
def remove_genre_from_book(
|
def remove_genre_from_book(
|
||||||
genre_id: int, book_id: int, session: Session = Depends(get_session)
|
current_user: RequireAuth,
|
||||||
|
genre_id: int,
|
||||||
|
book_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
link = session.exec(
|
"""Эндпоинт удаления жанра из книги"""
|
||||||
select(GenreBookLink)
|
return remove_relationship(session, GenreBookLink,
|
||||||
.where(GenreBookLink.genre_id == genre_id)
|
genre_id, "genre_id", book_id, "book_id")
|
||||||
.where(GenreBookLink.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"}
|
|
||||||
|
|
||||||
|
|
||||||
# Get genre's books
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/genres/{author_id}/books/",
|
"/genres/{genre_id}/books/",
|
||||||
response_model=List[BookRead],
|
response_model=List[BookRead],
|
||||||
summary="Получить книги, написанные в жанре",
|
summary="Получить книги, написанные в жанре",
|
||||||
description="Возвращает все книги в системе в этом жанре",
|
description="Возвращает все книги в системе в этом жанре",
|
||||||
)
|
)
|
||||||
def get_books_for_genre(genre_id: int, session: Session = Depends(get_session)):
|
def get_books_for_genre(genre_id: int, session: Session = Depends(get_session)):
|
||||||
genre = session.get(Genre, genre_id)
|
"""Эндпоинт получения книг с жанром"""
|
||||||
if not genre:
|
return get_related(session,
|
||||||
raise HTTPException(status_code=404, detail="Genre not found")
|
Genre, genre_id, "Genre", Book,
|
||||||
|
GenreBookLink, "genre_id", "book_id", BookRead)
|
||||||
books = session.exec(
|
|
||||||
select(Book).join(GenreBookLink).where(GenreBookLink.author_id == genre_id)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
return [BookRead(**book.model_dump()) for book in books]
|
|
||||||
|
|
||||||
|
|
||||||
# Get book's genres
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/books/{book_id}/genres/",
|
"/books/{book_id}/genres/",
|
||||||
response_model=List[GenreRead],
|
response_model=List[GenreRead],
|
||||||
summary="Получить жанры книги",
|
summary="Получить жанры книги",
|
||||||
description="Возвращает все жанры книги в системе",
|
description="Возвращает все жанры книги в системе",
|
||||||
)
|
)
|
||||||
def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
def get_genres_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||||
book = session.get(Book, book_id)
|
"""Эндпоинт получения жанров книги"""
|
||||||
if not book:
|
return get_related(session,
|
||||||
raise HTTPException(status_code=404, detail="Book not found")
|
Book, book_id, "Book", Genre,
|
||||||
|
GenreBookLink, "book_id", "genre_id", GenreRead)
|
||||||
genres = session.exec(
|
|
||||||
select(Genre).join(GenreBookLink).where(GenreBookLink.book_id == book_id)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
return [GenreRead(**author.model_dump()) for genre in genres]
|
|
||||||
|
|||||||
+18
-11
@@ -1,59 +1,66 @@
|
|||||||
|
"""Модуль настроек проекта"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from sqlmodel import create_engine, SQLModel, Session
|
from sqlmodel import Session, create_engine
|
||||||
from toml import load
|
from toml import load
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
with open("pyproject.toml") as f:
|
with open("pyproject.toml", 'r', encoding='utf-8') as f:
|
||||||
config = load(f)
|
config = load(f)
|
||||||
|
|
||||||
|
|
||||||
# Dependency to get the FastAPI application instance
|
|
||||||
def get_app() -> FastAPI:
|
def get_app() -> FastAPI:
|
||||||
|
"""Dependency, для получение экземплярра FastAPI application"""
|
||||||
return FastAPI(
|
return FastAPI(
|
||||||
title=config["tool"]["poetry"]["name"],
|
title=config["tool"]["poetry"]["name"],
|
||||||
description=config["tool"]["poetry"]["description"],
|
description=config["tool"]["poetry"]["description"],
|
||||||
version=config["tool"]["poetry"]["version"],
|
version=config["tool"]["poetry"]["version"],
|
||||||
openapi_tags=[
|
openapi_tags=[
|
||||||
|
{
|
||||||
|
"name": "authentication",
|
||||||
|
"description": "Авторизация пользователя."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "authors",
|
"name": "authors",
|
||||||
"description": "Operations with authors.",
|
"description": "Действия с авторами.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "books",
|
"name": "books",
|
||||||
"description": "Operations with books.",
|
"description": "Действия с книгами.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "genres",
|
"name": "genres",
|
||||||
"description": "Operations with genres.",
|
"description": "Действия с жанрами.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "relations",
|
"name": "relations",
|
||||||
"description": "Operations with relations.",
|
"description": "Действия с связями.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "misc",
|
"name": "misc",
|
||||||
"description": "Miscellaneous operations.",
|
"description": "Прочие.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HOST = os.getenv("POSTGRES_HOST")
|
||||||
|
PORT = os.getenv("POSTGRES_PORT")
|
||||||
USER = os.getenv("POSTGRES_USER")
|
USER = os.getenv("POSTGRES_USER")
|
||||||
PASSWORD = os.getenv("POSTGRES_PASSWORD")
|
PASSWORD = os.getenv("POSTGRES_PASSWORD")
|
||||||
DATABASE = os.getenv("POSTGRES_DB")
|
DATABASE = os.getenv("POSTGRES_DB")
|
||||||
HOST = os.getenv("POSTGRES_SERVER")
|
|
||||||
|
|
||||||
if not USER or not PASSWORD or not DATABASE or not HOST:
|
if not USER or not PASSWORD or not DATABASE or not HOST:
|
||||||
raise ValueError("Missing environment variables")
|
raise ValueError("Missing environment variables")
|
||||||
|
|
||||||
POSTGRES_DATABASE_URL = f"postgresql://{USER}:{PASSWORD}@{HOST}:5432/{DATABASE}"
|
POSTGRES_DATABASE_URL = f"postgresql://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}"
|
||||||
engine = create_engine(POSTGRES_DATABASE_URL, echo=True, future=True)
|
engine = create_engine(POSTGRES_DATABASE_URL, echo=True, future=True)
|
||||||
|
|
||||||
|
|
||||||
# Dependency to get a database session
|
|
||||||
def get_session():
|
def get_session():
|
||||||
|
"""Dependency, для получение сессии БД"""
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
yield session
|
yield session
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M0.877014 7.49988C0.877014 3.84219 3.84216 0.877045 7.49985 0.877045C11.1575 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1575 14.1227 7.49985 14.1227C3.84216 14.1227 0.877014 11.1575 0.877014 7.49988ZM7.49985 1.82704C4.36683 1.82704 1.82701 4.36686 1.82701 7.49988C1.82701 8.97196 2.38774 10.3131 3.30727 11.3213C4.19074 9.94119 5.73818 9.02499 7.50023 9.02499C9.26206 9.02499 10.8093 9.94097 11.6929 11.3208C12.6121 10.3127 13.1727 8.97172 13.1727 7.49988C13.1727 4.36686 10.6328 1.82704 7.49985 1.82704ZM10.9818 11.9787C10.2839 10.7795 8.9857 9.97499 7.50023 9.97499C6.01458 9.97499 4.71624 10.7797 4.01845 11.9791C4.97952 12.7272 6.18765 13.1727 7.49985 13.1727C8.81227 13.1727 10.0206 12.727 10.9818 11.9787ZM5.14999 6.50487C5.14999 5.207 6.20212 4.15487 7.49999 4.15487C8.79786 4.15487 9.84999 5.207 9.84999 6.50487C9.84999 7.80274 8.79786 8.85487 7.49999 8.85487C6.20212 8.85487 5.14999 7.80274 5.14999 6.50487ZM7.49999 5.10487C6.72679 5.10487 6.09999 5.73167 6.09999 6.50487C6.09999 7.27807 6.72679 7.90487 7.49999 7.90487C8.27319 7.90487 8.89999 7.27807 8.89999 6.50487C8.89999 5.73167 8.27319 5.10487 7.49999 5.10487Z"
|
||||||
|
fill="#000000"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
@@ -0,0 +1,62 @@
|
|||||||
|
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><rect
|
||||||
|
x="10"
|
||||||
|
y="10"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
rx="4"
|
||||||
|
ry="4"
|
||||||
|
fill="#fff"
|
||||||
|
stroke="#000"
|
||||||
|
stroke-width="2"
|
||||||
|
/><rect x="20" y="15" width="60" height="70" rx="10" ry="10" /><rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="66"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="62" rx="10" ry="10" /><rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="60"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="56" rx="10" ry="10" /><rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="54"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="50" rx="10" ry="10" /><rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="48"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="44" rx="10" ry="10" /><rect
|
||||||
|
x="22"
|
||||||
|
y="21"
|
||||||
|
width="2"
|
||||||
|
height="58"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
stroke="#000"
|
||||||
|
stroke-width="4"
|
||||||
|
/><rect x="22" y="55" width="4" height="26" rx="2" ry="15" /><text
|
||||||
|
x="50"
|
||||||
|
y="40"
|
||||||
|
text-anchor="middle"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
alignment-baseline="middle"
|
||||||
|
stroke="#fff"
|
||||||
|
stroke-width=".5"
|
||||||
|
fill="none"
|
||||||
|
font-size="20"
|
||||||
|
>『LiB』</text></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,59 @@
|
|||||||
|
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="20" y="15" width="60" height="70" rx="10" ry="10" /><rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="66"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="62" rx="10" ry="10" />
|
||||||
|
<rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="60"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="56" rx="10" ry="10" />
|
||||||
|
<rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="54"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="50" rx="10" ry="10" />
|
||||||
|
<rect
|
||||||
|
x="20"
|
||||||
|
y="15"
|
||||||
|
width="60"
|
||||||
|
height="48"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
fill="#fff"
|
||||||
|
/><rect x="20" y="15" width="60" height="44" rx="10" ry="10" />
|
||||||
|
<rect
|
||||||
|
x="22"
|
||||||
|
y="21"
|
||||||
|
width="2"
|
||||||
|
height="58"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
stroke="#000"
|
||||||
|
stroke-width="4"
|
||||||
|
/><rect x="22" y="55" width="4" height="26" rx="2" ry="15" />
|
||||||
|
<text
|
||||||
|
x="50"
|
||||||
|
y="40"
|
||||||
|
text-anchor="middle"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
alignment-baseline="middle"
|
||||||
|
stroke="#fff"
|
||||||
|
stroke-width=".5"
|
||||||
|
fill="none"
|
||||||
|
font-size="20"
|
||||||
|
>『LiB』</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
@@ -0,0 +1,135 @@
|
|||||||
|
// Load authors and genres asynchronously
|
||||||
|
Promise.all([
|
||||||
|
fetch("/authors").then((response) => response.json()),
|
||||||
|
fetch("/genres").then((response) => response.json()),
|
||||||
|
])
|
||||||
|
.then(([authorsData, genresData]) => {
|
||||||
|
// Populate authors dropdown
|
||||||
|
const dropdown = document.getElementById("author-dropdown");
|
||||||
|
authorsData.authors.forEach((author) => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "p-2 hover:bg-gray-100 cursor-pointer";
|
||||||
|
div.setAttribute("data-value", author.name);
|
||||||
|
div.textContent = author.name;
|
||||||
|
dropdown.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate genres list
|
||||||
|
const list = document.getElementById("genres-list");
|
||||||
|
genresData.genres.forEach((genre) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "mb-1";
|
||||||
|
li.innerHTML = `
|
||||||
|
<label class="custom-checkbox flex items-center">
|
||||||
|
<input type="checkbox" />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
${genre.name}
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
initializeAuthorDropdown();
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error loading data:", error));
|
||||||
|
|
||||||
|
function initializeAuthorDropdown() {
|
||||||
|
const authorSearchInput = document.getElementById("author-search-input");
|
||||||
|
const authorDropdown = document.getElementById("author-dropdown");
|
||||||
|
const selectedAuthorsContainer = document.getElementById(
|
||||||
|
"selected-authors-container",
|
||||||
|
);
|
||||||
|
const dropdownItems = authorDropdown.querySelectorAll("[data-value]");
|
||||||
|
let selectedAuthors = new Set();
|
||||||
|
|
||||||
|
// Function to update highlights in dropdown
|
||||||
|
const updateDropdownHighlights = () => {
|
||||||
|
dropdownItems.forEach((item) => {
|
||||||
|
const value = item.dataset.value;
|
||||||
|
if (selectedAuthors.has(value)) {
|
||||||
|
item.classList.add("bg-gray-200");
|
||||||
|
} else {
|
||||||
|
item.classList.remove("bg-gray-200");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to render selected authors
|
||||||
|
const renderSelectedAuthors = () => {
|
||||||
|
Array.from(selectedAuthorsContainer.children).forEach((child) => {
|
||||||
|
if (child.id !== "author-search-input") {
|
||||||
|
child.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedAuthors.forEach((author) => {
|
||||||
|
const authorChip = document.createElement("span");
|
||||||
|
authorChip.className =
|
||||||
|
"flex items-center bg-gray-200 text-gray-800 text-sm font-medium px-2.5 py-0.5 rounded-full";
|
||||||
|
authorChip.innerHTML = `
|
||||||
|
${author}
|
||||||
|
<button type="button" class="ml-1 inline-flex items-center p-0.5 text-sm text-gray-400 bg-transparent rounded-sm hover:bg-gray-200 hover:text-gray-900" data-author="${author}">
|
||||||
|
<svg class="w-2 h-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Remove author</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
selectedAuthorsContainer.insertBefore(authorChip, authorSearchInput);
|
||||||
|
});
|
||||||
|
updateDropdownHighlights();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle input focus to show dropdown
|
||||||
|
authorSearchInput.addEventListener("focus", () => {
|
||||||
|
authorDropdown.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle input for filtering
|
||||||
|
authorSearchInput.addEventListener("input", () => {
|
||||||
|
const query = authorSearchInput.value.toLowerCase();
|
||||||
|
dropdownItems.forEach((item) => {
|
||||||
|
const text = item.textContent.toLowerCase();
|
||||||
|
item.style.display = text.includes(query) ? "block" : "none";
|
||||||
|
});
|
||||||
|
authorDropdown.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle clicks outside to hide dropdown
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (
|
||||||
|
!selectedAuthorsContainer.contains(event.target) &&
|
||||||
|
!authorDropdown.contains(event.target)
|
||||||
|
) {
|
||||||
|
authorDropdown.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle author selection from dropdown
|
||||||
|
authorDropdown.addEventListener("click", (event) => {
|
||||||
|
const selectedValue = event.target.dataset.value;
|
||||||
|
if (selectedValue) {
|
||||||
|
if (selectedAuthors.has(selectedValue)) {
|
||||||
|
selectedAuthors.delete(selectedValue);
|
||||||
|
} else {
|
||||||
|
selectedAuthors.add(selectedValue);
|
||||||
|
}
|
||||||
|
authorSearchInput.value = "";
|
||||||
|
renderSelectedAuthors();
|
||||||
|
authorSearchInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle removing selected author chip
|
||||||
|
selectedAuthorsContainer.addEventListener("click", (event) => {
|
||||||
|
if (event.target.closest("button")) {
|
||||||
|
const authorToRemove = event.target.closest("button").dataset.author;
|
||||||
|
selectedAuthors.delete(authorToRemove);
|
||||||
|
renderSelectedAuthors();
|
||||||
|
authorSearchInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial render and highlights (without auto-focus)
|
||||||
|
renderSelectedAuthors();
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Novem";
|
||||||
|
src: url("novem.regular.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Dited";
|
||||||
|
src: url("dited.regular.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: "Novem", sans-serif;
|
||||||
|
letter-spacing: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li a {
|
||||||
|
font-family: "Dited", sans-serif;
|
||||||
|
letter-spacing: 2.5px;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom checkbox styles */
|
||||||
|
.custom-checkbox {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 2px solid #d1d5db; /* gray-300 */
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox:hover input ~ .checkmark {
|
||||||
|
border-color: #6b7280; /* gray-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox input:checked ~ .checkmark {
|
||||||
|
background-color: #6b7280; /* gray-500 */
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox input:checked ~ .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox .checkmark:after {
|
||||||
|
left: 6.5px;
|
||||||
|
top: 6px;
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{ app_info.title }}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img src="/favicon.ico" />
|
||||||
|
<h1>Welcome to {{ app_info.title }}!</h1>
|
||||||
|
<p>Description: {{ app_info.description }}</p>
|
||||||
|
<p>Version: {{ app_info.version }}</p>
|
||||||
|
<p>Current Time: {{ server_time }}</p>
|
||||||
|
<p>Status: {{ status }}</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/docs">Swagger UI</a></li>
|
||||||
|
<li><a href="/redoc">ReDoc</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/wowlikon/LibraryAPI">Source Code</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,58 +3,136 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{{ app_info.title }}</title>
|
<title>LiB</title>
|
||||||
<style>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
body {
|
<link rel="stylesheet" href="static/styles.css" />
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #2c3e50;
|
|
||||||
border-bottom: 2px solid #3498db;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 15px;
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="flex flex-col min-h-screen bg-gray-100">
|
||||||
<img src="/favicon.ico" />
|
<!-- Header -->
|
||||||
<h1>Welcome to {{ app_info.title }}!</h1>
|
<header class="bg-gray-500 text-white p-4 shadow-md">
|
||||||
<p>Description: {{ app_info.description }}</p>
|
<div class="mx-auto pl-5 pr-3 flex justify-between items-center">
|
||||||
<p>Version: {{ app_info.version }}</p>
|
<a class="flex gap-4 items-center max-w-10 h-auto" href="/">
|
||||||
<p>Current Time: {{ server_time }}</p>
|
<img class="invert" src="static/logo.svg" />
|
||||||
<p>Status: {{ status }}</p>
|
<h1 class="text-2xl font-bold">LiB</h1>
|
||||||
<ul>
|
</a>
|
||||||
<li><a href="/docs">Swagger UI</a></li>
|
<nav>
|
||||||
<li><a href="/redoc">ReDoc</a></li>
|
<ul class="flex space-x-4">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/wowlikon/LibraryAPI">Source Code</a>
|
<a href="/" class="hover:text-gray-200">Home</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="hover:text-gray-200">Products</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="hover:text-gray-200">About</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/api" class="hover:text-gray-200">API</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<img class="max-w-6 h-auto invert" src="static/avatar.svg" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<div class="flex flex-1 mt-4 p-4">
|
||||||
|
<aside
|
||||||
|
class="w-1/4 bg-white p-4 rounded-lg shadow-md mr-4 h-fit resize-x overflow-auto min-w-64 max-w-96"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Фильтры</h2>
|
||||||
|
<!-- Authors -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="font-medium mb-2">Авторы</h3>
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-2 p-2 border border-gray-300 rounded-md bg-white"
|
||||||
|
id="selected-authors-container"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="author-search-input"
|
||||||
|
class="flex-grow outline-none bg-transparent"
|
||||||
|
placeholder="Начните вводить..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="author-dropdown"
|
||||||
|
class="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 hidden max-h-60 overflow-y-auto"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Genres -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="font-medium mb-2">Жанры</h3>
|
||||||
|
<ul id="genres-list"></ul>
|
||||||
|
</div>
|
||||||
|
<!-- Apply -->
|
||||||
|
<button
|
||||||
|
class="w-full bg-gray-500 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition duration-200"
|
||||||
|
>
|
||||||
|
Применить фильтры
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Area -->
|
||||||
|
<main class="flex-1">
|
||||||
|
<!-- Book Card 1 -->
|
||||||
|
<div
|
||||||
|
class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold mb-1">Product Title 1</h3>
|
||||||
|
<p class="text-gray-700 text-sm">
|
||||||
|
A short description of the product, highlighting its
|
||||||
|
key features and benefits.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg font-semibold text-gray-600"
|
||||||
|
>$29.99</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Book Card 2 -->
|
||||||
|
<div
|
||||||
|
class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold mb-1">Product Title 2</h3>
|
||||||
|
<p class="text-gray-700 text-sm">
|
||||||
|
Another great product with amazing features. You'll
|
||||||
|
love it!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg font-semibold text-blue-600"
|
||||||
|
>$49.99</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Book Card 3 -->
|
||||||
|
<div
|
||||||
|
class="bg-white p-4 rounded-lg shadow-md mb-4 flex justify-between items-start"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold mb-1">Product Title 3</h3>
|
||||||
|
<p class="text-gray-700 text-sm">
|
||||||
|
This product is a must-have for every modern home.
|
||||||
|
High quality and durable.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg font-semibold text-gray-600"
|
||||||
|
>$19.99</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-gray-800 text-white p-4 mt-8">
|
||||||
|
<div class="container mx-auto text-center">
|
||||||
|
<p>© 2025 My Awesome Library. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="text/javascript" src="static/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2
-3
@@ -1,8 +1,7 @@
|
|||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
from alembic import context
|
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
from alembic import context
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import engine_from_config, pool
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from library_service.settings import POSTGRES_DATABASE_URL
|
from library_service.settings import POSTGRES_DATABASE_URL
|
||||||
|
|||||||
@@ -5,41 +5,49 @@ Revises: d266fdc61e99
|
|||||||
Create Date: 2025-06-25 11:24:30.229418
|
Create Date: 2025-06-25 11:24:30.229418
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
import sqlmodel
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '9d7a43ac5dfc'
|
revision: str = "9d7a43ac5dfc"
|
||||||
down_revision: Union[str, None] = 'd266fdc61e99'
|
down_revision: Union[str, None] = "d266fdc61e99"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('genre',
|
op.create_table(
|
||||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
"genre",
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_genre_id'), 'genre', ['id'], unique=False)
|
op.create_index(op.f("ix_genre_id"), "genre", ["id"], unique=False)
|
||||||
op.create_table('genrebooklink',
|
op.create_table(
|
||||||
sa.Column('genre_id', sa.Integer(), nullable=False),
|
"genrebooklink",
|
||||||
sa.Column('book_id', sa.Integer(), nullable=False),
|
sa.Column("genre_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['book_id'], ['book.id'], ),
|
sa.Column("book_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['genre_id'], ['genre.id'], ),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint('genre_id', 'book_id')
|
["book_id"],
|
||||||
|
["book.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["genre_id"],
|
||||||
|
["genre.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("genre_id", "book_id"),
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('genrebooklink')
|
op.drop_table("genrebooklink")
|
||||||
op.drop_index(op.f('ix_genre_id'), table_name='genre')
|
op.drop_index(op.f("ix_genre_id"), table_name="genre")
|
||||||
op.drop_table('genre')
|
op.drop_table("genre")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""auth
|
||||||
|
|
||||||
|
Revision ID: b838606ad8d1
|
||||||
|
Revises: 9d7a43ac5dfc
|
||||||
|
Create Date: 2025-12-07 20:18:05.839579
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "b838606ad8d1"
|
||||||
|
down_revision: Union[str, None] = "9d7a43ac5dfc"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"roles",
|
||||||
|
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(op.f("ix_roles_id"), "roles", ["id"], unique=False)
|
||||||
|
op.create_index(op.f("ix_roles_name"), "roles", ["name"], unique=True)
|
||||||
|
op.create_table(
|
||||||
|
"users",
|
||||||
|
sa.Column(
|
||||||
|
"username", sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"full_name", sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True
|
||||||
|
),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("is_verified", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
||||||
|
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
|
||||||
|
op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
|
||||||
|
op.create_table(
|
||||||
|
"user_roles",
|
||||||
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("role_id", sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["role_id"],
|
||||||
|
["roles.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user_id"],
|
||||||
|
["users.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("user_id", "role_id"),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("user_roles")
|
||||||
|
op.drop_index(op.f("ix_users_username"), table_name="users")
|
||||||
|
op.drop_index(op.f("ix_users_id"), table_name="users")
|
||||||
|
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||||
|
op.drop_table("users")
|
||||||
|
op.drop_index(op.f("ix_roles_name"), table_name="roles")
|
||||||
|
op.drop_index(op.f("ix_roles_id"), table_name="roles")
|
||||||
|
op.drop_table("roles")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -5,15 +5,15 @@ Revises:
|
|||||||
Create Date: 2025-05-27 18:04:22.279035
|
Create Date: 2025-05-27 18:04:22.279035
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
import sqlmodel
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = 'd266fdc61e99'
|
revision: str = "d266fdc61e99"
|
||||||
down_revision: Union[str, None] = None
|
down_revision: Union[str, None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
@@ -21,34 +21,43 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('author',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
"author",
|
||||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_author_id'), 'author', ['id'], unique=False)
|
op.create_index(op.f("ix_author_id"), "author", ["id"], unique=False)
|
||||||
op.create_table('book',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
"book",
|
||||||
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_book_id'), 'book', ['id'], unique=False)
|
op.create_index(op.f("ix_book_id"), "book", ["id"], unique=False)
|
||||||
op.create_table('authorbooklink',
|
op.create_table(
|
||||||
sa.Column('author_id', sa.Integer(), nullable=False),
|
"authorbooklink",
|
||||||
sa.Column('book_id', sa.Integer(), nullable=False),
|
sa.Column("author_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['author_id'], ['author.id'], ),
|
sa.Column("book_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['book_id'], ['book.id'], ),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint('author_id', 'book_id')
|
["author_id"],
|
||||||
|
["author.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["book_id"],
|
||||||
|
["book.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("author_id", "book_id"),
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('authorbooklink')
|
op.drop_table("authorbooklink")
|
||||||
op.drop_index(op.f('ix_book_id'), table_name='book')
|
op.drop_index(op.f("ix_book_id"), table_name="book")
|
||||||
op.drop_table('book')
|
op.drop_table("book")
|
||||||
op.drop_index(op.f('ix_author_id'), table_name='author')
|
op.drop_index(op.f("ix_author_id"), table_name="author")
|
||||||
op.drop_table('author')
|
op.drop_table("author")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
Generated
+608
-114
@@ -1,5 +1,17 @@
|
|||||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiofiles"
|
||||||
|
version = "25.1.0"
|
||||||
|
description = "File support for asyncio."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695"},
|
||||||
|
{file = "aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.16.2"
|
version = "1.16.2"
|
||||||
@@ -53,6 +65,75 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
|
|||||||
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
|
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
|
||||||
trio = ["trio (>=0.26.1)"]
|
trio = ["trio (>=0.26.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi"
|
||||||
|
version = "25.1.0"
|
||||||
|
description = "Argon2 for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741"},
|
||||||
|
{file = "argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
argon2-cffi-bindings = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi-bindings"
|
||||||
|
version = "25.1.0"
|
||||||
|
description = "Low-level CFFI bindings for Argon2"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520"},
|
||||||
|
{file = "argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = [
|
||||||
|
{version = ">=1.0.1", markers = "python_version < \"3.14\""},
|
||||||
|
{version = ">=2.0.0b1", markers = "python_version >= \"3.14\""},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "astroid"
|
||||||
|
version = "4.0.2"
|
||||||
|
description = "An abstract syntax tree for Python with inference support."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10.0"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b"},
|
||||||
|
{file = "astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "25.1.0"
|
version = "25.1.0"
|
||||||
@@ -110,6 +191,103 @@ files = [
|
|||||||
{file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"},
|
{file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
|
||||||
|
{file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
|
||||||
|
{file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
|
||||||
|
{file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
|
||||||
|
{file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
|
||||||
|
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.2.1"
|
version = "8.2.1"
|
||||||
@@ -138,6 +316,99 @@ files = [
|
|||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "46.0.3"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"},
|
||||||
|
{file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"},
|
||||||
|
{file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"},
|
||||||
|
{file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"},
|
||||||
|
{file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||||
|
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||||
|
nox = ["nox[uv] (>=2024.4.15)"]
|
||||||
|
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
|
||||||
|
sdist = ["build (>=1.0.0)"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||||
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dill"
|
||||||
|
version = "0.4.0"
|
||||||
|
description = "serialize all of Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"},
|
||||||
|
{file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
graph = ["objgraph (>=1.7.2)"]
|
||||||
|
profile = ["gprof2dot (>=2022.7.29)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dnspython"
|
name = "dnspython"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@@ -159,6 +430,25 @@ idna = ["idna (>=3.7)"]
|
|||||||
trio = ["trio (>=0.23)"]
|
trio = ["trio (>=0.23)"]
|
||||||
wmi = ["wmi (>=1.5.1)"]
|
wmi = ["wmi (>=1.5.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.19.1"
|
||||||
|
description = "ECDSA cryptographic signature library (pure python)"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"},
|
||||||
|
{file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = ">=1.9.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
gmpy = ["gmpy"]
|
||||||
|
gmpy2 = ["gmpy2"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "email-validator"
|
name = "email-validator"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -439,6 +729,22 @@ files = [
|
|||||||
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "7.0.0"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10.0"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"},
|
||||||
|
{file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colors = ["colorama"]
|
||||||
|
plugins = ["setuptools"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itsdangerous"
|
name = "itsdangerous"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -585,6 +891,18 @@ files = [
|
|||||||
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "McCabe checker, plugin for flake8"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdurl"
|
name = "mdurl"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -703,6 +1021,27 @@ files = [
|
|||||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "passlib"
|
||||||
|
version = "1.7.4"
|
||||||
|
description = "comprehensive password hashing framework supporting over 30 schemes"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
||||||
|
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
argon2-cffi = {version = ">=18.2.0", optional = true, markers = "extra == \"argon2\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
argon2 = ["argon2-cffi (>=18.2.0)"]
|
||||||
|
bcrypt = ["bcrypt (>=3.1.0)"]
|
||||||
|
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
||||||
|
totp = ["cryptography"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -826,23 +1165,49 @@ files = [
|
|||||||
{file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1"
|
||||||
|
version = "0.6.1"
|
||||||
|
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
|
||||||
|
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.23"
|
||||||
|
description = "C parser in Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "implementation_name != \"PyPy\""
|
||||||
|
files = [
|
||||||
|
{file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"},
|
||||||
|
{file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.11.7"
|
version = "2.12.5"
|
||||||
description = "Data validation using Python type hints"
|
description = "Data validation using Python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"},
|
{file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"},
|
||||||
{file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"},
|
{file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
annotated-types = ">=0.6.0"
|
annotated-types = ">=0.6.0"
|
||||||
pydantic-core = "2.33.2"
|
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
|
||||||
typing-extensions = ">=4.12.2"
|
pydantic-core = "2.41.5"
|
||||||
typing-inspection = ">=0.4.0"
|
typing-extensions = ">=4.14.1"
|
||||||
|
typing-inspection = ">=0.4.2"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
email = ["email-validator (>=2.0.0)"]
|
email = ["email-validator (>=2.0.0)"]
|
||||||
@@ -850,115 +1215,137 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-core"
|
name = "pydantic-core"
|
||||||
version = "2.33.2"
|
version = "2.41.5"
|
||||||
description = "Core functionality for Pydantic validation and serialization"
|
description = "Core functionality for Pydantic validation and serialization"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"},
|
||||||
{file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"},
|
{file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"},
|
||||||
{file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"},
|
{file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"},
|
||||||
{file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"},
|
{file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"},
|
{file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"},
|
||||||
{file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"},
|
{file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"},
|
||||||
{file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"},
|
||||||
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"},
|
{file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"},
|
||||||
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"},
|
{file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"},
|
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"},
|
||||||
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"},
|
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"},
|
||||||
{file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"},
|
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"},
|
||||||
|
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"},
|
||||||
|
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"},
|
||||||
|
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"},
|
||||||
|
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"},
|
||||||
|
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"},
|
||||||
|
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"},
|
||||||
|
{file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
typing-extensions = ">=4.14.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-extra-types"
|
name = "pydantic-extra-types"
|
||||||
@@ -1023,6 +1410,31 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
windows-terminal = ["colorama (>=0.4.6)"]
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pylint"
|
||||||
|
version = "4.0.4"
|
||||||
|
description = "python code static checker"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10.0"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0"},
|
||||||
|
{file = "pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
astroid = ">=4.0.2,<=4.1.dev0"
|
||||||
|
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||||
|
dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""}
|
||||||
|
isort = ">=5,<5.13 || >5.13,<8"
|
||||||
|
mccabe = ">=0.6,<0.8"
|
||||||
|
platformdirs = ">=2.2"
|
||||||
|
tomlkit = ">=0.10.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
spelling = ["pyenchant (>=3.2,<4.0)"]
|
||||||
|
testutils = ["gitpython (>3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.4.1"
|
version = "8.4.1"
|
||||||
@@ -1045,6 +1457,25 @@ pygments = ">=2.7.2"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.3.0"
|
||||||
|
description = "Pytest support for asyncio"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"},
|
||||||
|
{file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=8.2,<10"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
|
||||||
|
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -1060,6 +1491,30 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
cli = ["click (>=5.0)"]
|
cli = ["click (>=5.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-jose"
|
||||||
|
version = "3.5.0"
|
||||||
|
description = "JOSE implementation in Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771"},
|
||||||
|
{file = "python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""}
|
||||||
|
ecdsa = "!=0.15"
|
||||||
|
pyasn1 = ">=0.5.0"
|
||||||
|
rsa = ">=4.0,<4.1.1 || >4.1.1,<4.4 || >4.4,<5.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cryptography = ["cryptography (>=3.4.0)"]
|
||||||
|
pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"]
|
||||||
|
pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)"]
|
||||||
|
test = ["pytest", "pytest-cov"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.20"
|
version = "0.0.20"
|
||||||
@@ -1171,6 +1626,21 @@ click = ">=8.1.7"
|
|||||||
rich = ">=13.7.1"
|
rich = ">=13.7.1"
|
||||||
typing-extensions = ">=4.12.2"
|
typing-extensions = ">=4.12.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsa"
|
||||||
|
version = "4.9.1"
|
||||||
|
description = "Pure-Python RSA implementation"
|
||||||
|
optional = false
|
||||||
|
python-versions = "<4,>=3.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"},
|
||||||
|
{file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pyasn1 = ">=0.1.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
@@ -1183,6 +1653,18 @@ files = [
|
|||||||
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||||
|
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1337,6 +1819,18 @@ files = [
|
|||||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomlkit"
|
||||||
|
version = "0.13.3"
|
||||||
|
description = "Style preserving TOML library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"},
|
||||||
|
{file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -1357,26 +1851,26 @@ typing-extensions = ">=3.7.4.3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.14.0"
|
version = "4.15.0"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.9+"
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"},
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
{file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"},
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-inspection"
|
name = "typing-inspection"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
description = "Runtime typing introspection tools"
|
description = "Runtime typing introspection tools"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
|
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
|
||||||
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
|
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1750,4 +2244,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.13"
|
python-versions = "^3.13"
|
||||||
content-hash = "a3555dac28547317a5d8d75507b5923d03b58412a988a260b627c079782bc15c"
|
content-hash = "2fdd550e819b1456733250a62196acb442127a296ff60e010c424ecbebec294e"
|
||||||
|
|||||||
+8
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "LibraryAPI"
|
name = "LibraryAPI"
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
description = "Это простое API для управления авторами, книгами и их жанрами."
|
description = "Это простое API для управления авторами, книгами и их жанрами."
|
||||||
authors = ["wowlikon"]
|
authors = ["wowlikon"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -16,10 +16,17 @@ sqlmodel = "^0.0.24"
|
|||||||
uvicorn = "^0.34.3"
|
uvicorn = "^0.34.3"
|
||||||
jinja2 = "^3.1.6"
|
jinja2 = "^3.1.6"
|
||||||
toml = "^0.10.2"
|
toml = "^0.10.2"
|
||||||
|
python-jose = {extras = ["cryptography"], version = "^3.5.0"}
|
||||||
|
passlib = {extras = ["argon2"], version = "^1.7.4"}
|
||||||
|
aiofiles = "^25.1.0"
|
||||||
|
pydantic = {extras = ["email"], version = "^2.12.5"}
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^25.1.0"
|
black = "^25.1.0"
|
||||||
pytest = "^8.4.1"
|
pytest = "^8.4.1"
|
||||||
|
isort = "^7.0.0"
|
||||||
|
pytest-asyncio = "^1.3.0"
|
||||||
|
pylint = "^4.0.4"
|
||||||
|
|
||||||
[tool.poetry.requires-plugins]
|
[tool.poetry.requires-plugins]
|
||||||
poetry-plugin-export = ">=1.8"
|
poetry-plugin-export = ">=1.8"
|
||||||
|
|||||||
+5
-4
@@ -1,23 +1,24 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from tests.mock_routers import books, authors, genres, relationships
|
|
||||||
from library_service.routers.misc import router as misc_router
|
from library_service.routers.misc import router as misc_router
|
||||||
|
from tests.mock_routers import authors, books, genres, relationships
|
||||||
|
|
||||||
|
|
||||||
def create_mock_app() -> FastAPI:
|
def create_mock_app() -> FastAPI:
|
||||||
"""Create FastAPI app with mock routers for testing"""
|
"""Создание FastAPI app с моками роутеров для тестов"""
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Library API Test",
|
title="Library API Test",
|
||||||
description="Library API for testing without database",
|
description="Library API for testing without database",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Include mock routers
|
# Подключение мок-роутеров
|
||||||
app.include_router(books.router)
|
app.include_router(books.router)
|
||||||
app.include_router(authors.router)
|
app.include_router(authors.router)
|
||||||
app.include_router(genres.router)
|
app.include_router(genres.router)
|
||||||
app.include_router(relationships.router)
|
app.include_router(relationships.router)
|
||||||
|
|
||||||
# Include real misc router (it doesn't use database)
|
# Подключение реального misc роутера
|
||||||
app.include_router(misc_router)
|
app.include_router(misc_router)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
router = APIRouter(prefix="/authors", tags=["authors"])
|
router = APIRouter(prefix="/authors", tags=["authors"])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
router = APIRouter(prefix="/books", tags=["books"])
|
router = APIRouter(prefix="/books", tags=["books"])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
router = APIRouter(prefix="/genres", tags=["genres"])
|
router = APIRouter(prefix="/genres", tags=["genres"])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
router = APIRouter(tags=["relations"])
|
router = APIRouter(tags=["relations"])
|
||||||
@@ -36,5 +37,4 @@ def get_authors_for_book(book_id: int):
|
|||||||
|
|
||||||
@router.post("/relationships/genre-book")
|
@router.post("/relationships/genre-book")
|
||||||
def add_genre_to_book(genre_id: int, book_id: int):
|
def add_genre_to_book(genre_id: int, book_id: int):
|
||||||
# For tests that need genre functionality
|
|
||||||
return {"genre_id": genre_id, "book_id": book_id}
|
return {"genre_id": genre_id, "book_id": book_id}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Optional, List, Any
|
from typing import Any, List
|
||||||
|
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
|
|
||||||
@@ -8,20 +9,13 @@ class MockSession:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.storage = mock_storage
|
self.storage = mock_storage
|
||||||
|
|
||||||
def add(self, obj: Any):
|
def add(self, obj: Any): ...
|
||||||
"""Mock add - not needed for our implementation"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def commit(self):
|
def commit(self): ...
|
||||||
"""Mock commit - not needed for our implementation"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def refresh(self, obj: Any):
|
def refresh(self, obj: Any): ...
|
||||||
"""Mock refresh - not needed for our implementation"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get(self, model_class, pk: int):
|
def get(self, model_class, pk: int):
|
||||||
"""Mock get method to retrieve object by primary key"""
|
|
||||||
if hasattr(model_class, "__name__"):
|
if hasattr(model_class, "__name__"):
|
||||||
model_name = model_class.__name__.lower()
|
model_name = model_class.__name__.lower()
|
||||||
else:
|
else:
|
||||||
@@ -35,12 +29,9 @@ class MockSession:
|
|||||||
return self.storage.get_genre(pk)
|
return self.storage.get_genre(pk)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete(self, obj: Any):
|
def delete(self, obj: Any): ...
|
||||||
"""Mock delete - handled in storage methods"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def exec(self, statement):
|
def exec(self, statement):
|
||||||
"""Mock exec method for queries"""
|
|
||||||
return MockResult([])
|
return MockResult([])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-16
@@ -1,4 +1,4 @@
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List
|
||||||
|
|
||||||
|
|
||||||
class MockStorage:
|
class MockStorage:
|
||||||
@@ -15,7 +15,7 @@ class MockStorage:
|
|||||||
self.genre_id_counter = 1
|
self.genre_id_counter = 1
|
||||||
|
|
||||||
def clear_all(self):
|
def clear_all(self):
|
||||||
"""Clear all data"""
|
"""Очистка всех данных"""
|
||||||
self.books.clear()
|
self.books.clear()
|
||||||
self.authors.clear()
|
self.authors.clear()
|
||||||
self.genres.clear()
|
self.genres.clear()
|
||||||
@@ -33,7 +33,7 @@ class MockStorage:
|
|||||||
self.book_id_counter += 1
|
self.book_id_counter += 1
|
||||||
return book
|
return book
|
||||||
|
|
||||||
def get_book(self, book_id: int) -> Optional[dict]:
|
def get_book(self, book_id: int) -> dict | None:
|
||||||
return self.books.get(book_id)
|
return self.books.get(book_id)
|
||||||
|
|
||||||
def get_all_books(self) -> List[dict]:
|
def get_all_books(self) -> List[dict]:
|
||||||
@@ -42,9 +42,9 @@ class MockStorage:
|
|||||||
def update_book(
|
def update_book(
|
||||||
self,
|
self,
|
||||||
book_id: int,
|
book_id: int,
|
||||||
title: Optional[str] = None,
|
title: str | None = None,
|
||||||
description: Optional[str] = None,
|
description: str | None = None,
|
||||||
) -> Optional[dict]:
|
) -> dict | None:
|
||||||
if book_id not in self.books:
|
if book_id not in self.books:
|
||||||
return None
|
return None
|
||||||
book = self.books[book_id]
|
book = self.books[book_id]
|
||||||
@@ -54,7 +54,7 @@ class MockStorage:
|
|||||||
book["description"] = description
|
book["description"] = description
|
||||||
return book
|
return book
|
||||||
|
|
||||||
def delete_book(self, book_id: int) -> Optional[dict]:
|
def delete_book(self, book_id: int) -> dict | None:
|
||||||
if book_id not in self.books:
|
if book_id not in self.books:
|
||||||
return None
|
return None
|
||||||
book = self.books.pop(book_id)
|
book = self.books.pop(book_id)
|
||||||
@@ -74,15 +74,15 @@ class MockStorage:
|
|||||||
self.author_id_counter += 1
|
self.author_id_counter += 1
|
||||||
return author
|
return author
|
||||||
|
|
||||||
def get_author(self, author_id: int) -> Optional[dict]:
|
def get_author(self, author_id: int) -> dict | None:
|
||||||
return self.authors.get(author_id)
|
return self.authors.get(author_id)
|
||||||
|
|
||||||
def get_all_authors(self) -> List[dict]:
|
def get_all_authors(self) -> List[dict]:
|
||||||
return list(self.authors.values())
|
return list(self.authors.values())
|
||||||
|
|
||||||
def update_author(
|
def update_author(
|
||||||
self, author_id: int, name: Optional[str] = None
|
self, author_id: int, name: str | None = None
|
||||||
) -> Optional[dict]:
|
) -> dict | None:
|
||||||
if author_id not in self.authors:
|
if author_id not in self.authors:
|
||||||
return None
|
return None
|
||||||
author = self.authors[author_id]
|
author = self.authors[author_id]
|
||||||
@@ -90,7 +90,7 @@ class MockStorage:
|
|||||||
author["name"] = name
|
author["name"] = name
|
||||||
return author
|
return author
|
||||||
|
|
||||||
def delete_author(self, author_id: int) -> Optional[dict]:
|
def delete_author(self, author_id: int) -> dict | None:
|
||||||
if author_id not in self.authors:
|
if author_id not in self.authors:
|
||||||
return None
|
return None
|
||||||
author = self.authors.pop(author_id)
|
author = self.authors.pop(author_id)
|
||||||
@@ -107,15 +107,13 @@ class MockStorage:
|
|||||||
self.genre_id_counter += 1
|
self.genre_id_counter += 1
|
||||||
return genre
|
return genre
|
||||||
|
|
||||||
def get_genre(self, genre_id: int) -> Optional[dict]:
|
def get_genre(self, genre_id: int) -> dict | None:
|
||||||
return self.genres.get(genre)
|
return self.genres.get(genre)
|
||||||
|
|
||||||
def get_all_authors(self) -> List[dict]:
|
def get_all_authors(self) -> List[dict]:
|
||||||
return list(self.authors.values())
|
return list(self.authors.values())
|
||||||
|
|
||||||
def update_genre(
|
def update_genre(self, genre_id: int, name: str | None = None) -> dict | None:
|
||||||
self, genre_id: int, name: Optional[str] = None
|
|
||||||
) -> Optional[dict]:
|
|
||||||
if genre_id not in self.genres:
|
if genre_id not in self.genres:
|
||||||
return None
|
return None
|
||||||
genre = self.genres[genre_id]
|
genre = self.genres[genre_id]
|
||||||
@@ -123,7 +121,7 @@ class MockStorage:
|
|||||||
genre["name"] = name
|
genre["name"] = name
|
||||||
return genre
|
return genre
|
||||||
|
|
||||||
def delete_genre(self, genre_id: int) -> Optional[dict]:
|
def delete_genre(self, genre_id: int) -> dict | None:
|
||||||
if genre_id not in self.genres:
|
if genre_id not in self.genres:
|
||||||
return None
|
return None
|
||||||
genre = self.genres.pop(genre_id)
|
genre = self.genres.pop(genre_id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from tests.mock_app import mock_app
|
from tests.mock_app import mock_app
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
@@ -8,7 +9,6 @@ client = TestClient(mock_app)
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup_database():
|
def setup_database():
|
||||||
"""Clear mock storage before each test"""
|
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
yield
|
yield
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
@@ -29,7 +29,6 @@ def test_create_author():
|
|||||||
|
|
||||||
|
|
||||||
def test_list_authors():
|
def test_list_authors():
|
||||||
# First create an author
|
|
||||||
client.post("/authors", json={"name": "Test Author"})
|
client.post("/authors", json={"name": "Test Author"})
|
||||||
|
|
||||||
response = client.get("/authors")
|
response = client.get("/authors")
|
||||||
@@ -42,7 +41,6 @@ def test_list_authors():
|
|||||||
|
|
||||||
|
|
||||||
def test_get_existing_author():
|
def test_get_existing_author():
|
||||||
# First create an author
|
|
||||||
client.post("/authors", json={"name": "Test Author"})
|
client.post("/authors", json={"name": "Test Author"})
|
||||||
|
|
||||||
response = client.get("/authors/1")
|
response = client.get("/authors/1")
|
||||||
@@ -63,7 +61,6 @@ def test_get_not_existing_author():
|
|||||||
|
|
||||||
|
|
||||||
def test_update_author():
|
def test_update_author():
|
||||||
# First create an author
|
|
||||||
client.post("/authors", json={"name": "Test Author"})
|
client.post("/authors", json={"name": "Test Author"})
|
||||||
|
|
||||||
response = client.get("/authors/1")
|
response = client.get("/authors/1")
|
||||||
@@ -84,10 +81,7 @@ def test_update_not_existing_author():
|
|||||||
|
|
||||||
|
|
||||||
def test_delete_author():
|
def test_delete_author():
|
||||||
# First create an author
|
|
||||||
client.post("/authors", json={"name": "Test Author"})
|
client.post("/authors", json={"name": "Test Author"})
|
||||||
|
|
||||||
# Update it first
|
|
||||||
client.put("/authors/1", json={"name": "Updated Author"})
|
client.put("/authors/1", json={"name": "Updated Author"})
|
||||||
|
|
||||||
response = client.get("/authors/1")
|
response = client.get("/authors/1")
|
||||||
|
|||||||
+6
-18
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from tests.mock_app import mock_app
|
from tests.mock_app import mock_app
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
@@ -8,7 +9,6 @@ client = TestClient(mock_app)
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup_database():
|
def setup_database():
|
||||||
"""Clear mock storage before each test"""
|
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
yield
|
yield
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
@@ -35,9 +35,7 @@ def test_create_book():
|
|||||||
|
|
||||||
|
|
||||||
def test_list_books():
|
def test_list_books():
|
||||||
# First create a book
|
client.post("/books", json={"title": "Test Book", "description": "Test Description"}
|
||||||
client.post(
|
|
||||||
"/books", json={"title": "Test Book", "description": "Test Description"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.get("/books")
|
response = client.get("/books")
|
||||||
@@ -50,9 +48,7 @@ def test_list_books():
|
|||||||
|
|
||||||
|
|
||||||
def test_get_existing_book():
|
def test_get_existing_book():
|
||||||
# First create a book
|
client.post("/books", json={"title": "Test Book", "description": "Test Description"}
|
||||||
client.post(
|
|
||||||
"/books", json={"title": "Test Book", "description": "Test Description"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.get("/books/1")
|
response = client.get("/books/1")
|
||||||
@@ -74,9 +70,7 @@ def test_get_not_existing_book():
|
|||||||
|
|
||||||
|
|
||||||
def test_update_book():
|
def test_update_book():
|
||||||
# First create a book
|
client.post("/books", json={"title": "Test Book", "description": "Test Description"}
|
||||||
client.post(
|
|
||||||
"/books", json={"title": "Test Book", "description": "Test Description"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.get("/books/1")
|
response = client.get("/books/1")
|
||||||
@@ -102,14 +96,8 @@ def test_update_not_existing_book():
|
|||||||
|
|
||||||
|
|
||||||
def test_delete_book():
|
def test_delete_book():
|
||||||
# First create a book
|
client.post("/books", json={"title": "Test Book", "description": "Test Description"})
|
||||||
client.post(
|
client.put("/books/1", json={"title": "Updated Book", "description": "Updated Description"}
|
||||||
"/books", json={"title": "Test Book", "description": "Test Description"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update it first
|
|
||||||
client.put(
|
|
||||||
"/books/1", json={"title": "Updated Book", "description": "Updated Description"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.get("/books/1")
|
response = client.get("/books/1")
|
||||||
|
|||||||
+10
-22
@@ -1,6 +1,8 @@
|
|||||||
import pytest
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from tests.mock_app import mock_app
|
from tests.mock_app import mock_app
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
@@ -9,20 +11,15 @@ client = TestClient(mock_app)
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup_database():
|
def setup_database():
|
||||||
"""Setup and cleanup mock database for each test"""
|
|
||||||
# Clear data before each test
|
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
yield
|
yield
|
||||||
# Clear data after each test (optional, but good practice)
|
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
|
|
||||||
|
|
||||||
# Test the main page of the application
|
|
||||||
def test_main_page():
|
def test_main_page():
|
||||||
response = client.get("/") # Send GET request to the main page
|
response = client.get("/api")
|
||||||
try:
|
try:
|
||||||
content = response.content.decode("utf-8") # Decode response content
|
content = response.content.decode("utf-8")
|
||||||
# Find indices of key elements in the content
|
|
||||||
title_idx = content.index("Welcome to ")
|
title_idx = content.index("Welcome to ")
|
||||||
description_idx = content.index("Description: ")
|
description_idx = content.index("Description: ")
|
||||||
version_idx = content.index("Version: ")
|
version_idx = content.index("Version: ")
|
||||||
@@ -38,25 +35,16 @@ def test_main_page():
|
|||||||
assert content[time_idx + 1] != "<", "Time not provided"
|
assert content[time_idx + 1] != "<", "Time not provided"
|
||||||
assert content[status_idx + 1] != "<", "Status not provided"
|
assert content[status_idx + 1] != "<", "Status not provided"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}") # Print error if an exception occurs
|
print(f"Error: {e}")
|
||||||
assert False, "Unexpected error" # Force test failure on unexpected error
|
assert False, "Unexpected error"
|
||||||
|
|
||||||
|
|
||||||
# Test application info endpoint
|
|
||||||
def test_app_info_test():
|
def test_app_info_test():
|
||||||
response = client.get("/api/info") # Send GET request to the info endpoint
|
response = client.get("/api/info")
|
||||||
assert response.status_code == 200, "Invalid response status"
|
assert response.status_code == 200, "Invalid response status"
|
||||||
assert response.json()["status"] == "ok", "Status not ok"
|
assert response.json()["status"] == "ok", "Status not ok"
|
||||||
assert response.json()["app_info"]["title"] != "", "Title not provided"
|
assert response.json()["app_info"]["title"] != "", "Title not provided"
|
||||||
assert response.json()["app_info"]["description"] != "", "Description not provided"
|
assert response.json()["app_info"]["description"] != "", "Description not provided"
|
||||||
assert response.json()["app_info"]["version"] != "", "Version not provided"
|
assert response.json()["app_info"]["version"] != "", "Version not provided"
|
||||||
# Check time difference
|
assert (0 < (datetime.now() - datetime.fromisoformat(response.json()["server_time"])).total_seconds()), "Negative time difference"
|
||||||
assert (
|
assert (datetime.now() - datetime.fromisoformat(response.json()["server_time"])).total_seconds() < 1, "Time difference too large"
|
||||||
0
|
|
||||||
< (
|
|
||||||
datetime.now() - datetime.fromisoformat(response.json()["server_time"])
|
|
||||||
).total_seconds()
|
|
||||||
), "Negative time difference"
|
|
||||||
assert (
|
|
||||||
datetime.now() - datetime.fromisoformat(response.json()["server_time"])
|
|
||||||
).total_seconds() < 1, "Time difference too large"
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from tests.mock_app import mock_app
|
from tests.mock_app import mock_app
|
||||||
from tests.mocks.mock_storage import mock_storage
|
from tests.mocks.mock_storage import mock_storage
|
||||||
|
|
||||||
@@ -8,7 +9,6 @@ client = TestClient(mock_app)
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup_database():
|
def setup_database():
|
||||||
"""Clear mock storage before each test"""
|
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
yield
|
yield
|
||||||
mock_storage.clear_all()
|
mock_storage.clear_all()
|
||||||
@@ -30,28 +30,18 @@ def make_genrebook_relationship(genre_id, book_id):
|
|||||||
|
|
||||||
|
|
||||||
def test_prepare_data():
|
def test_prepare_data():
|
||||||
# Create books
|
assert (client.post("/books", json={"title": "Test Book 1", "description": "Test Description 1"}).status_code == 200)
|
||||||
assert client.post(
|
assert (client.post("/books", json={"title": "Test Book 2", "description": "Test Description 2"}).status_code == 200)
|
||||||
"/books", json={"title": "Test Book 1", "description": "Test Description 1"}
|
assert (client.post("/books", json={"title": "Test Book 3", "description": "Test Description 3"}).status_code == 200)
|
||||||
).status_code == 200
|
|
||||||
assert client.post(
|
|
||||||
"/books", json={"title": "Test Book 2", "description": "Test Description 2"}
|
|
||||||
).status_code == 200
|
|
||||||
assert client.post(
|
|
||||||
"/books", json={"title": "Test Book 3", "description": "Test Description 3"}
|
|
||||||
).status_code == 200
|
|
||||||
|
|
||||||
# Create authors
|
|
||||||
assert client.post("/authors", json={"name": "Test Author 1"}).status_code == 200
|
assert client.post("/authors", json={"name": "Test Author 1"}).status_code == 200
|
||||||
assert client.post("/authors", json={"name": "Test Author 2"}).status_code == 200
|
assert client.post("/authors", json={"name": "Test Author 2"}).status_code == 200
|
||||||
assert client.post("/authors", json={"name": "Test Author 3"}).status_code == 200
|
assert client.post("/authors", json={"name": "Test Author 3"}).status_code == 200
|
||||||
|
|
||||||
# Create genres
|
|
||||||
assert client.post("/genres", json={"name": "Test Genre 1"}).status_code == 200
|
assert client.post("/genres", json={"name": "Test Genre 1"}).status_code == 200
|
||||||
assert client.post("/genres", json={"name": "Test Genre 2"}).status_code == 200
|
assert client.post("/genres", json={"name": "Test Genre 2"}).status_code == 200
|
||||||
assert client.post("/genres", json={"name": "Test Genre 3"}).status_code == 200
|
assert client.post("/genres", json={"name": "Test Genre 3"}).status_code == 200
|
||||||
|
|
||||||
# Create relationships
|
|
||||||
make_authorbook_relationship(1, 1)
|
make_authorbook_relationship(1, 1)
|
||||||
make_authorbook_relationship(2, 1)
|
make_authorbook_relationship(2, 1)
|
||||||
make_authorbook_relationship(1, 2)
|
make_authorbook_relationship(1, 2)
|
||||||
@@ -63,8 +53,8 @@ def test_prepare_data():
|
|||||||
make_genrebook_relationship(2, 3)
|
make_genrebook_relationship(2, 3)
|
||||||
make_genrebook_relationship(3, 3)
|
make_genrebook_relationship(3, 3)
|
||||||
|
|
||||||
|
|
||||||
def test_get_book_authors():
|
def test_get_book_authors():
|
||||||
# Setup test data
|
|
||||||
test_prepare_data()
|
test_prepare_data()
|
||||||
|
|
||||||
response1 = client.get("/books/1/authors")
|
response1 = client.get("/books/1/authors")
|
||||||
@@ -91,7 +81,6 @@ def test_get_book_authors():
|
|||||||
|
|
||||||
|
|
||||||
def test_get_author_books():
|
def test_get_author_books():
|
||||||
# Setup test data
|
|
||||||
test_prepare_data()
|
test_prepare_data()
|
||||||
|
|
||||||
response1 = client.get("/authors/1/books")
|
response1 = client.get("/authors/1/books")
|
||||||
|
|||||||
Reference in New Issue
Block a user