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:
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -116,7 +116,7 @@ async def upload_for_plagiarism_check(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Ошибка сохранения файла. Попробуйте позже.",
|
||||
)
|
||||
) from e
|
||||
|
||||
# ── 5. Создаём задачу и диспатчим ─────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Роутер для получения отчётов о выполненных задачах."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user