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>
This commit is contained in:
jze9
2026-08-11 17:20:24 +05:00
parent ecc10a4413
commit 2daaa8c8a4
38 changed files with 143 additions and 103 deletions

View File

@@ -4,8 +4,9 @@
дополнительно проверяют секретный код сессии (verify_admin_code).
"""
import contextlib
import logging
from datetime import datetime, timezone
from datetime import datetime
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -150,11 +151,11 @@ async def stats(db: AsyncSession = Depends(get_db)) -> AdminStats:
users_total = (await db.execute(select(func.count()).select_from(User))).scalar_one()
rows = (await db.execute(select(Task.status, func.count()).group_by(Task.status))).all()
tasks_by_status = {s: c for s, c in rows}
tasks_by_status = dict(rows)
docs_total = (await db.execute(select(func.count()).select_from(Document))).scalar_one()
rows = (await db.execute(select(Document.source, func.count()).group_by(Document.source))).all()
docs_by_source = {s: c for s, c in rows}
docs_by_source = dict(rows)
staging_pending = (
await db.execute(
@@ -257,10 +258,8 @@ async def update_user(
await db.commit()
await db.refresh(user)
# Сбросить кэш пользователя
try:
with contextlib.suppress(Exception):
await get_redis().delete(f"user:cache:{user_id}")
except Exception:
pass
return AdminUserResponse.model_validate(user)
@@ -533,7 +532,7 @@ async def approve_staging(
obj = get_minio().get_object("staging", sw.text_key)
full_text = obj.read().decode("utf-8", errors="replace")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Не удалось прочитать текст: {e}")
raise HTTPException(status_code=500, detail=f"Не удалось прочитать текст: {e}") from e
# Добавить в базу документов через существующую задачу индексатора
doc_data = {

View File

@@ -1,7 +1,7 @@
"""Роутер аутентификации: регистрация, вход, верификация email."""
import secrets
import logging
import secrets
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
@@ -224,7 +224,7 @@ async def resend_verification(
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Не удалось отправить письмо, попробуйте позже",
)
) from e
@router.post("/verify-email/{token}", status_code=status.HTTP_200_OK)

View File

@@ -116,7 +116,7 @@ async def upload_for_plagiarism_check(
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Ошибка сохранения файла. Попробуйте позже.",
)
) from e
# ── 5. Создаём задачу и диспатчим ─────────────────────────────────────────

View File

@@ -1,7 +1,6 @@
"""Роутер для получения отчётов о выполненных задачах."""
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select

View File

@@ -1,7 +1,7 @@
"""Авторизация админ-панели: проверка роли is_admin и секретного кода сессии."""
import secrets
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
from fastapi import Depends, HTTPException, Path, status
from sqlalchemy import select

View File

@@ -9,7 +9,7 @@ Redis гарантирует, что между командами внутри
"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from app.core.redis_client import get_redis
@@ -93,7 +93,7 @@ return 1
def _period_suffix(period: str) -> str:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
return now.strftime("%Y-%m-%d") if period == "day" else now.strftime("%Y-%m")

View File

@@ -2,7 +2,7 @@
import json
import logging
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any
from fastapi import Depends, HTTPException, Query, WebSocket, status
@@ -35,7 +35,7 @@ def verify_password(plain: str, hashed: str) -> bool:
def create_access_token(data: dict[str, Any], expires_delta: timedelta | None = None) -> str:
payload = data.copy()
expire = datetime.now(timezone.utc) + (
expire = datetime.now(UTC) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
payload["exp"] = expire
@@ -58,7 +58,7 @@ def _decode_token(token: str) -> int:
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Невалидный или просроченный токен",
headers={"WWW-Authenticate": "Bearer"},
)
) from None
async def _load_user(user_id: int, db: AsyncSession):

View File

@@ -1,5 +1,6 @@
"""WebSocket менеджер для real-time обновлений статуса задач."""
import contextlib
import json
import logging
from typing import Any
@@ -27,10 +28,8 @@ class ConnectionManager:
def disconnect(self, task_id: str, websocket: WebSocket) -> None:
"""Удалить соединение из реестра."""
if task_id in self._connections:
try:
with contextlib.suppress(ValueError):
self._connections[task_id].remove(websocket)
except ValueError:
pass
if not self._connections[task_id]:
del self._connections[task_id]
logger.info(f"WebSocket отключён от задачи {task_id!r}")

View File

@@ -1,10 +1,10 @@
"""Точка входа FastAPI приложения — Академический помощник."""
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends
from fastapi import Depends, FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
@@ -98,6 +98,7 @@ async def websocket_task_updates(
ownership проверяется до установки соединения.
"""
from sqlalchemy import select
from app.database import AsyncSessionLocal
from app.models.task import Task

View File

@@ -3,14 +3,14 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import func, String
from sqlalchemy import String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
if TYPE_CHECKING:
from app.models.task import Task
from app.models.document import UsageLog
from app.models.task import Task
class User(Base):