Files
anti-plagiarism/services/worker-notifier/app/tasks/notify.py
jze9 2daaa8c8a4 chore(lint): ruff-гейт в CI + фиксы (0 находок) — блокирует кривой деплой
Второй CI-гейт после тестов: ruff как статический анализатор всего Python-кода
(services + scripts). Раньше ни линта, ни проверки типов в CI не было вовсе.

Конфиг ruff.toml: правила E/F/W/I/UP/B/SIM/C4, line-length 100. Осознанно
выключены E501 (длину держит форматтер; длинные RU-комментарии — норма),
B008 (Depends()/Query() в дефолтах — идиома FastAPI, не баг) и UP042
((str, Enum)→StrEnum меняет __str__/сериализацию — не трогаем).

Починено под ноль находок:
- B904 (11): raise ... from exc / from None — читаемые цепочки исключений в
  Celery-ретраях и HTTPException, ошибки обработки не маскируют исходные.
- SIM105 (5): try/except/pass → contextlib.suppress (faiss remove_ids, lsh.remove,
  сброс кэша, ws-disconnect, парс года).
- C416/SIM108/B905/F841/UP035/UP017/F401/I001: dict(rows), тернарник, zip strict,
  мёртвая переменная, устаревшие импорты, timezone.utc→UTC, чистка/сортировка.

Обвязка: scripts/run_lint.sh (ruff в изолированном python:3.11-slim), шаг «Линт»
в job test перед юнит-тестами (падаем раньше). make lint / make lint-fix.
Все 41 юнит-тест по-прежнему зелёные, изменённые файлы компилируются.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 17:20:24 +05:00

147 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Celery задачи отправки email уведомлений."""
from celery.utils.log import get_task_logger
from app.celery_app import celery_app
from app.config import settings
from app.db import db_session
from app.email_sender import EmailSender
logger = get_task_logger(__name__)
@celery_app.task(
name="notify.send_task_done",
bind=True,
max_retries=3,
default_retry_delay=30,
)
def send_task_done(self, task_id: str) -> dict:
"""
Отправить email уведомление о завершении задачи.
Args:
task_id: ID завершённой задачи
Returns:
dict со статусом отправки
"""
from app.models import Task, User
logger.info(f"Отправка уведомления для задачи {task_id!r}")
try:
with db_session() as session:
task = session.get(Task, task_id)
if task is None:
logger.error(f"Задача {task_id!r} не найдена")
return {"status": "task_not_found"}
user = session.get(User, task.user_id)
if user is None:
logger.error(f"Пользователь {task.user_id} не найден")
return {"status": "user_not_found"}
# Формируем краткое описание результата
summary = _build_summary(task)
# Достаём значения ДО закрытия сессии — иначе объекты становятся
# detached и обращение к атрибутам падает DetachedInstanceError.
user_email = user.email
user_name = user.name
task_type = task.type
sender = EmailSender()
sender.send_task_done(
user_email=user_email,
user_name=user_name,
task_id=task_id,
task_type=task_type,
summary=summary,
app_url=settings.APP_URL,
)
logger.info(f"Уведомление отправлено пользователю {user_email!r} для задачи {task_id!r}")
return {"status": "sent", "email": user_email}
except Exception as exc:
logger.error(f"Ошибка отправки уведомления для задачи {task_id!r}: {exc}", exc_info=True)
raise self.retry(exc=exc, countdown=30) from exc
def _build_summary(task) -> str:
"""Сформировать краткое описание результата для email."""
result = task.result or {}
input_data = task.input_data or {}
if task.type == "search":
count = len(result.get("sources", []))
query = input_data.get("query", "")
return (
f'Найдено <strong>{count} источников</strong> по запросу "{query[:80]}".'
f' Откройте результат, чтобы просмотреть список с ГОСТ-цитатами.'
)
elif task.type == "plagiarism":
similarity = result.get("overall_similarity", 0)
total = result.get("total_fragments", 0)
flagged = result.get("flagged_fragments", 0)
filename = input_data.get("filename", "")
color = "#dc2626" if similarity > 30 else "#d97706" if similarity > 10 else "#16a34a"
label = "Высокий" if similarity > 30 else "Средний" if similarity > 10 else "Низкий"
return (
f'Проверка файла <strong>"{filename}"</strong> завершена.<br>'
f'Уровень схожести: <strong style="color:{color};">{similarity:.1f}% ({label})</strong>.<br>'
f'Проверено {total} фрагментов, выявлено совпадений: {flagged}.'
)
elif task.type == "gost":
count = len(result.get("bibliography", []))
style = result.get("style", "7.1")
return (
f'Библиография из <strong>{count} источников</strong>'
f' отформатирована по ГОСТ {style}-2003.'
)
elif task.type == "summarize":
return "Краткое изложение успешно создано."
return "Ваша задача выполнена."
@celery_app.task(
name="notify.send_verification",
bind=True,
max_retries=3,
default_retry_delay=60,
)
def send_verification(self, user_email: str, user_name: str, token: str) -> dict:
"""
Отправить письмо с подтверждением email.
Args:
user_email: Email для верификации
user_name: Имя пользователя
token: Токен верификации
Returns:
dict со статусом отправки
"""
logger.info(f"Отправка письма верификации на {user_email!r}")
try:
sender = EmailSender()
sender.send_verification(
user_email=user_email,
user_name=user_name,
token=token,
app_url=settings.APP_URL,
)
logger.info(f"Письмо верификации отправлено на {user_email!r}")
return {"status": "sent"}
except Exception as exc:
logger.error(f"Ошибка отправки верификации на {user_email!r}: {exc}", exc_info=True)
raise self.retry(exc=exc, countdown=60) from exc