diff --git a/alembic/versions/f210b1391ab0_users_migration.py b/alembic/versions/f210b1391ab0_users_migration.py new file mode 100644 index 0000000..0260680 --- /dev/null +++ b/alembic/versions/f210b1391ab0_users_migration.py @@ -0,0 +1,39 @@ +"""Users Migration + +Revision ID: f210b1391ab0 +Revises: ffb4e984a7e9 +Create Date: 2026-02-25 12:37:10.316531 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f210b1391ab0' +down_revision: Union[str, Sequence[str], None] = 'ffb4e984a7e9' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('telegram_id', sa.Integer(), nullable=False), + sa.Column('telegram_username', sa.String(length=256), nullable=True), + sa.Column('create_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('telegram_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users') + # ### end Alembic commands ### diff --git a/app/core/base.py b/app/core/base.py index 0d88ce2..c3a77d2 100644 --- a/app/core/base.py +++ b/app/core/base.py @@ -1,4 +1,5 @@ """Импорты класса Base и всех моделей Alembic.""" from app.core.database import Base # noqa +from app.models.users import User # noqa from app.models.words import Word # noqa diff --git a/app/crud/users.py b/app/crud/users.py new file mode 100644 index 0000000..3406cfd --- /dev/null +++ b/app/crud/users.py @@ -0,0 +1,75 @@ +import logging + +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.users import User +from app.schemas.users import UserCreateSchema + +logger = logging.getLogger(__name__) + + +class CRUDUser: + + async def create_user(self, user: UserCreateSchema, session: AsyncSession): + try: + new_user = User(**user.model_dump()) + session.add(new_user) + await session.flush() + await session.commit() + await session.refresh(new_user) + logger.info( + f"Добавлен пользователь '{new_user.telegram_username}' id: {new_user.telegram_id} " + ) + return new_user + except SQLAlchemyError as e: + logger.error(f"Ошибка при добавлении пользователя: {e}") + raise + + async def get_all_users(self, session: AsyncSession): + try: + query = select(User) + result = await session.execute(query) + users = result.scalars().all() + logger.info("Получен список всех пользователей") + return users + except SQLAlchemyError as e: + logger.error( + f"Ошибка при получении списка всех пользователей: {e}" + ) + raise + + async def get_user(self, telegram_id: int, session: AsyncSession): + try: + query = select(User).where(User.telegram_id == telegram_id) + result = await session.execute(query) + user = result.scalars().first() + if user: + logger.info( + f"Получен пользователь: '{user.telegram_username}' id={user.telegram_id}" + ) + return user + else: + logger.warning( + f"Пользователь id={telegram_id} не найден в базе" + ) + raise + except SQLAlchemyError as e: + logger.error( + f"Ошибка при получении пользователя id={telegram_id}: {e}" + ) + raise + + async def delete_user(self, user: User, session: AsyncSession): + try: + await session.delete(user) + await session.commit() + logger.info( + f"Пользователь: '{user.telegram_username}' id={user.telegram_id} удалён" + ) + except SQLAlchemyError as e: + logger.error( + f"Ошибка при удалении пользователя '{user.telegram_username}' id={user.telegram_id}: {e}" + ) + raise diff --git a/app/crud/words.py b/app/crud/words.py index 593437b..7ad5efe 100644 --- a/app/crud/words.py +++ b/app/crud/words.py @@ -44,7 +44,9 @@ async def get_word(self, word: str, session: AsyncSession): result = await session.execute(query) find_word = result.scalars().first() if find_word: - logger.info(f"Получено слово: '{find_word}'") + logger.info( + f"Получено слово: '{find_word.english} - {find_word.russian}'" + ) else: logger.warning(f"Слово '{word}' не найдено в базе") return find_word diff --git a/app/main.py b/app/main.py index 3be0b17..7f02bd3 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,7 @@ from app.core.config import settings from app.core.logging import setup_logging from app.routers.questions import question_router +from app.routers.users import user_router from app.routers.words import word_router setup_logging() @@ -28,3 +29,4 @@ async def lifespan(app: FastAPI): app.include_router(word_router) app.include_router(question_router) +app.include_router(user_router) diff --git a/app/models/users.py b/app/models/users.py new file mode 100644 index 0000000..5321073 --- /dev/null +++ b/app/models/users.py @@ -0,0 +1,19 @@ +from datetime import datetime + +from sqlalchemy import DateTime, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + telegram_id: Mapped[int] = mapped_column( + Integer, nullable=False, unique=True + ) + telegram_username: Mapped[str] = mapped_column(String(256), nullable=True) + create_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) diff --git a/app/routers/questions.py b/app/routers/questions.py index 66b138d..a896e6e 100644 --- a/app/routers/questions.py +++ b/app/routers/questions.py @@ -5,7 +5,7 @@ from app.core.database import get_session from app.crud.words import CRUDWord -from app.schemas.words import QuestionResponseSchema +from app.schemas.questions import QuestionReadSchema question_router = APIRouter( prefix="/questions", @@ -18,11 +18,11 @@ @question_router.get( - "", response_model=QuestionResponseSchema, summary="Получение вопроса" + "", response_model=QuestionReadSchema, summary="Получение вопроса" ) async def get_question( session: AsyncSession = Depends(get_session), -) -> QuestionResponseSchema: +) -> QuestionReadSchema: words = await word_crud.get_all_words(session) if len(words) < 4: return {"error": "В базее должно быть минимум 4 слова"} diff --git a/app/routers/users.py b/app/routers/users.py new file mode 100644 index 0000000..5a839ca --- /dev/null +++ b/app/routers/users.py @@ -0,0 +1,88 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_session +from app.crud.users import CRUDUser +from app.schemas.users import UserCreateSchema, UserReadSchema + +user_router = APIRouter( + prefix="/users", + tags=[ + "Пользователи", + ], +) + +user_crud = CRUDUser() + + +@user_router.post( + "", response_model=UserReadSchema, summary="Добавление пользователя" +) +async def add_user( + user: UserCreateSchema, session: AsyncSession = Depends(get_session) +) -> UserReadSchema: + try: + add_user = await user_crud.create_user(user, session) + return add_user + except Exception as e: + raise e + + +@user_router.delete("/{telegram_id}", summary="Удаление Пользователя") +async def delete_user( + telegram_id: int, session: AsyncSession = Depends(get_session) +) -> dict: + user = await user_crud.get_user(telegram_id, session) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Пользователь не найден", + ) + try: + await user_crud.delete_user(user, session) + return {"detail": f"Пользователь id={telegram_id} удален"} + except Exception: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Ошибка при удалении пользователя", + ) + + +@user_router.get( + "/all", + response_model=list[UserReadSchema], + summary="Получение всех пользователей", +) +async def get_all_users( + session: AsyncSession = Depends(get_session), +) -> list[UserReadSchema]: + try: + users = await user_crud.get_all_users(session) + if not users: + raise HTTPException( + status_code=status.HTT_404_NOT_FOUND, + detail="Список пользоввателей отсутсттвует", + ) + return users + except Exception as e: + raise e + + +@user_router.get( + "/{telegram_id}", + response_model=UserReadSchema, + summary="Получение пользователя по telegram_id", +) +async def get_user( + telegram_id: int, session: AsyncSession = Depends(get_session) +) -> UserReadSchema: + try: + user = await user_crud.get_user(telegram_id, session) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Пользователь id={telegram_id} не найден", + ) + return user + except Exception as e: + raise e diff --git a/app/schemas/questions.py b/app/schemas/questions.py new file mode 100644 index 0000000..0a0160b --- /dev/null +++ b/app/schemas/questions.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + + +class QuestionReadSchema(BaseModel): + word: str + options: list[str] + correct: str + direction: str diff --git a/app/schemas/users.py b/app/schemas/users.py new file mode 100644 index 0000000..620eed5 --- /dev/null +++ b/app/schemas/users.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class UserBaseSchema(BaseModel): + telegram_id: int + telegram_username: str | None = None + + +class UserCreateSchema(UserBaseSchema): + pass + + +class UserReadSchema(UserBaseSchema): + id: int + create_at: datetime + + class Config: + orm_mode = True