Skip to content

Commit

Permalink
Merge pull request #91 from Studio-Yandex-Practicum/fix/mypy
Browse files Browse the repository at this point in the history
Added strict mode to pre-commit and ignore imports
  • Loading branch information
NiKuma0 authored Nov 24, 2023
2 parents 0f2ddbd + 4b40a1c commit e07f5ef
Show file tree
Hide file tree
Showing 20 changed files with 216 additions and 56 deletions.
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ repos:
rev: 'v1.6.1'
hooks:
- id: mypy
args: [--strict]
additional_dependencies: [
'sqlalchemy',
'structlog',
'dependency_injector',
'faker',
'pydantic',
'pydantic_settings'
]
20 changes: 11 additions & 9 deletions fill_db.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import argparse
import asyncio
import random
from argparse import Namespace

from dependency_injector import wiring
from faker import Faker
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from sqlalchemy.schema import Table

from src.core.db.models import User, UsersMatch
from src.core.db.models import Base, User, UsersMatch
from src.core.db.repository.user import UserRepository
from src.core.db.repository.usersmatch import UsersMatchRepository
from src.depends import Container

fake = Faker()


def parse_arguments():
def parse_arguments() -> Namespace:
parser = argparse.ArgumentParser(description="Загрузка тестовых данных пользователей и их мэтчей в БД")
parser.add_argument(
"-u",
Expand Down Expand Up @@ -48,8 +50,8 @@ async def filling_users_in_db(user_repo: UserRepository, num_users: int) -> None

async def filling_users_match_in_db(session: AsyncSession, match_repo: UsersMatchRepository, num_pairs: int) -> None:
"""Заполняем базу мэтчами"""
users = await session.execute(User.__table__.select())
users = users.all()
users_query = await session.execute(User.__table__.select())
users = users_query.all()

if num_pairs != 0 and len(users) <= 1:
print("Недостаточно пользователей для создания пар. Требуется как минимум два пользователя.")
Expand Down Expand Up @@ -77,17 +79,17 @@ async def filling_users_match_in_db(session: AsyncSession, match_repo: UsersMatc

async def delete_all_data(session: AsyncSession) -> None:
"""Удаление всех данных User, UsersMatch из таблицы"""
await session.execute(UsersMatch.__table__.delete())
await session.execute(User.__table__.delete())
await session.execute(Table(UsersMatch.__tablename__, Base.metadata).delete())
await session.execute(Table(User.__tablename__, Base.metadata).delete())
await session.commit()


@wiring.inject
async def main(
sessionmaker: AsyncSession = wiring.Provide[Container.sessionmaker],
sessionmaker: async_sessionmaker[AsyncSession] = wiring.Provide[Container.sessionmaker],
user_repo: UserRepository = wiring.Provide[Container.user_repository],
match_repo: UsersMatchRepository = wiring.Provide[Container.match_repository],
):
) -> None:
args = parse_arguments()

try:
Expand Down
17 changes: 15 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
[flake8]
ignore = E501, W503, E265, F811, PT001, D, W504, W292
exclude = *.pyi
max-line-length = 120

[mypy]
mypy_path = stubs
python_version = 3.11
exclude = src/core/db/migrations
exclude = (?x)(
src/core/db/migrations
| fill_db.py
| stubs/
)

[mypy-src.*]
[mypy-mmpy_bot.*]
ignore_missing_imports=True

[mypy-mattermostautodriver.*]
ignore_missing_imports=True

[mypy-apscheduler.*]
# TODO: В 4.x версии apscheduler планируется добавить типизацию. Стоит задуматься на обновления, когда новая версия выйдет.
ignore_missing_imports=True

[mypy-src.core.db.migrations.*]
Expand Down
21 changes: 11 additions & 10 deletions src/bot/plugins/week_routine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from dependency_injector.wiring import Provide, inject
from mmpy_bot import ActionEvent, Plugin, listen_to, listen_webhook
from mmpy_bot.wrappers import Message

from src.bot.schemas import Actions, Attachment, Context, Integration
from src.bot.services.matching import MatchingService
Expand All @@ -20,7 +21,7 @@

class WeekRoutine(Plugin):
@inject
def direct_friday_message(self, endpoints: Endpoints = Provide[Container.endpoints]):
def direct_friday_message(self, endpoints: Endpoints = Provide[Container.endpoints]) -> Attachment:
action_yes = Actions(
id="yes",
name="Да",
Expand All @@ -43,8 +44,8 @@ def direct_friday_message(self, endpoints: Endpoints = Provide[Container.endpoin
@listen_to("/notify_all_users", re.IGNORECASE)
@inject
async def test_notify_all_users(
self, message, notify_service: NotifyService = Provide[Container.week_routine_service,]
):
self, message: Message, notify_service: NotifyService = Provide[Container.week_routine_service,]
) -> None:
attachments = self.direct_friday_message()
await notify_service.notify_all_users(
plugin=self, attachments=attachments, title="Еженедельный пятничный опрос"
Expand All @@ -53,8 +54,8 @@ async def test_notify_all_users(
@listen_to("/monday_message", re.IGNORECASE)
@inject
async def test_monday_message(
self, message, notify_service: NotifyService = Provide[Container.week_routine_service,]
):
self, message: Message, notify_service: NotifyService = Provide[Container.week_routine_service,]
) -> None:
await notify_service.meeting_notifications(plugin=self)

@inject
Expand All @@ -63,7 +64,7 @@ def on_start(
notify_service: NotifyService = Provide[Container.week_routine_service,],
matching_service: MatchingService = Provide[Container.matching_service],
scheduler: AsyncIOScheduler = Provide[Container.scheduler],
):
) -> None:
attachments = self.direct_friday_message()

scheduler.add_job(
Expand All @@ -90,21 +91,21 @@ def on_start(

@listen_to("/stop_jobs", re.IGNORECASE)
@inject
def cancel_jobs(self, message, scheduler=Provide[Container.scheduler,]):
def cancel_jobs(self, message: Message, scheduler: AsyncIOScheduler = Provide[Container.scheduler,]) -> None:
scheduler.shutdown()
self.driver.reply_to(message, "All jobs cancelled.")

@inject
async def _change_user_status(
self, user_id: str, notify_service: NotifyService = Provide[Container.week_routine_service,]
):
) -> None:
await notify_service.set_waiting_meeting_status(user_id)

@listen_webhook("set_waiting_meeting_status")
async def add_to_meeting(
self,
event: ActionEvent,
):
) -> None:
await self._change_user_status(event.user_id)
self.driver.respond_to_web(
event,
Expand All @@ -114,7 +115,7 @@ async def add_to_meeting(
)

@listen_webhook("not_meeting")
async def no(self, event: ActionEvent):
async def no(self, event: ActionEvent) -> None:
self.driver.respond_to_web(
event,
{
Expand Down
9 changes: 6 additions & 3 deletions src/bot/services/matching.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Sequence

import structlog

from src.core.db.models import StatusEnum, UsersMatch
Expand All @@ -14,20 +16,21 @@ def __init__(self, user_repository: UserRepository, match_repository: UsersMatch
self._user_repository = user_repository
self._match_repository = match_repository

async def run_matching(self) -> list[UsersMatch]:
async def run_matching(self) -> list[UsersMatch] | None:
"""Запускает создание метчей."""
matches: list[UsersMatch] = []
while user_one := await self._user_repository.get_free_user():
if not (user_two := await self._user_repository.get_suitable_pair(user_one)):
return log.info(f"Невозможно создать пару для пользователя с id {user_one.id}.")
log.info(f"Невозможно создать пару для пользователя с id {user_one.id}.")
return None
matches.append(match := await self._match_repository.make_match_for_user(user_one, user_two))
for user in (user_one, user_two):
user.status = StatusEnum.IN_MEETING
user.matches.append(match)
await self._user_repository.update(user.id, user)
return matches

async def run_closing_meetings(self):
async def run_closing_meetings(self) -> Sequence[UsersMatch]:
"""Запускает закрытие встреч."""

users = await self._user_repository.get_by_status(StatusEnum.IN_MEETING)
Expand Down
9 changes: 6 additions & 3 deletions src/bot/services/notify_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ def __init__(self, user_repository: UserRepository, match_repository: UsersMatch
self._user_repository = user_repository
self._match_repository = match_repository

async def notify_all_users(self, plugin, attachments: Attachment, title: str = "Еженедельный опрос"):
async def notify_all_users(
self, plugin: Plugin, attachments: Attachment, title: str = "Еженедельный опрос"
) -> None:
"""Функция отправки еженедельного сообщения (создания поста)"""

users_id = await self._user_repository.get_all_chat_id()
if not users_id:
return logger.error("Пользователи отсутствуют.")
logger.error("Пользователи отсутствуют.")
return None
for user_id in users_id:
try:
plugin.driver.direct_message(
Expand All @@ -29,7 +32,7 @@ async def notify_all_users(self, plugin, attachments: Attachment, title: str = "
except InvalidOrMissingParameters:
logger.error(f"Пользователя с таким user_id {user_id} нет в matter_most")

async def set_waiting_meeting_status(self, user_id: str):
async def set_waiting_meeting_status(self, user_id: str) -> None:
await self._user_repository.set_waiting_meeting_status(user_id)

async def meeting_notifications(self, plugin: Plugin) -> None:
Expand Down
1 change: 0 additions & 1 deletion src/core/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class Base(DeclarativeBase):
updated_at: Mapped[date] = mapped_column(
nullable=False, onupdate=SERVER_DEFAULT_TIME, server_default=SERVER_DEFAULT_TIME
)
__name__: Mapped[str]


class Admin(Base):
Expand Down
Empty file.
17 changes: 9 additions & 8 deletions src/core/db/repository/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import abc
from typing import Generic, TypeVar
from typing import Generic, Sequence, TypeVar

from sqlalchemy import select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from src.core.db.models import Base
from src.core.exceptions import exceptions
Expand All @@ -17,7 +16,7 @@ class AbstractRepository(abc.ABC, Generic[Model]):

_model: type[Model]

def __init__(self, sessionmaker: sessionmaker[AsyncSession]) -> None:
def __init__(self, sessionmaker: async_sessionmaker[AsyncSession]) -> None:
self._sessionmaker = sessionmaker

async def get_or_none(self, instance_id: int) -> Model | None:
Expand All @@ -30,7 +29,7 @@ async def get(self, instance_id: int) -> Model:
"""Получает объект модели по ID. В случае отсутствия объекта бросает ошибку."""
db_obj = await self.get_or_none(instance_id)
if db_obj is None:
raise exceptions.ObjectNotFoundError(self._model.Base, instance_id)
raise exceptions.ObjectNotFoundError(self._model, instance_id)
return db_obj

async def create(self, instance: Model) -> Model:
Expand All @@ -52,18 +51,20 @@ async def update(self, instance_id: int, instance: Model) -> Model:
await session.commit()
return instance

async def update_all(self, instances: list[dict[Model, Model]]) -> list[dict[Model, Model]]:
async def update_all(
self, instances: list[dict[str, Model]] | dict[str, Model] | None
) -> list[dict[str, Model]] | dict[str, Model] | None:
"""Обновляет несколько измененных объектов модели в базе."""
async with self._sessionmaker() as session:
await session.execute(update(self._model), instances)
await session.commit()
return instances

async def get_all(self) -> list[Model]:
async def get_all(self) -> Sequence[Model]:
"""Возвращает все объекты модели из базы данных."""
async with self._sessionmaker() as session:
objects = await session.scalars(select(self._model))
return objects
return objects.all()

async def create_all(self, instances: list[Model]) -> None:
"""Создает несколько объектов модели в базе данных."""
Expand Down
17 changes: 11 additions & 6 deletions src/core/db/repository/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Sequence

from sqlalchemy import select
from sqlalchemy.orm import joinedload

Expand All @@ -8,7 +10,7 @@
class UserRepository(AbstractRepository[User]):
_model = User

async def get_all_chat_id(self) -> list[str] | None:
async def get_all_chat_id(self) -> Sequence[str] | None:
"""Получает все user id для маттермост"""
async with self._sessionmaker() as session:
instance = await session.execute(select(self._model.user_id))
Expand All @@ -26,7 +28,7 @@ async def create_or_update(self, instance: User) -> User | None:
return await self.create(instance)
return await self.update(db_instance.id, instance)

async def get_by_status(self, status: str) -> list[User] | None:
async def get_by_status(self, status: str) -> Sequence[User]:
"""Получает пользователей по статусу участия во встречах."""
async with self._sessionmaker() as session:
users = await session.scalars(select(self._model).where(self._model.status == status))
Expand All @@ -35,17 +37,18 @@ async def get_by_status(self, status: str) -> list[User] | None:
async def get_free_user(self) -> User | None:
"""Получает пользователя ожидающего встречи."""
async with self._sessionmaker() as session:
return await session.scalar(
user: User | None = await session.scalar(
select(self._model)
.options(joinedload(self._model.matches))
.where(self._model.status == StatusEnum.WAITING_MEETING)
.limit(1)
)
return user

async def get_suitable_pair(self, user: User) -> User | None:
"""Возвращает подходящего для встречи пользователя."""
async with self._sessionmaker() as session:
return await session.scalar(
pair: User | None = await session.scalar(
select(self._model)
.options(joinedload(self._model.matches))
.where(self._model.id != user.id, self._model.status == StatusEnum.WAITING_MEETING)
Expand All @@ -57,6 +60,7 @@ async def get_suitable_pair(self, user: User) -> User | None:
)
.limit(1)
)
return pair

async def set_in_meeting_status(self, user_id: int) -> None:
"""Устанавливает статус in_meeting для встречи после опроса."""
Expand All @@ -68,5 +72,6 @@ async def set_waiting_meeting_status(self, user_id: str) -> None:
"""Устанавливает статус waiting_meeting для встречи после еженедельного опроса."""
async with self._sessionmaker() as session:
current_user = await session.scalar(select(self._model).where(self._model.user_id == user_id))
current_user.status = StatusEnum.WAITING_MEETING
await self.update(current_user.id, current_user)
if current_user is not None:
current_user.status = StatusEnum.WAITING_MEETING
await self.update(current_user.id, current_user)
Loading

0 comments on commit e07f5ef

Please sign in to comment.