ci-mypy-fixes
Some checks failed
Deploy on push / deploy (push) Failing after 2m34s

This commit is contained in:
2025-08-12 18:23:53 +03:00
parent d6d88133bd
commit 5876995838
8 changed files with 76 additions and 61 deletions

View File

@@ -68,10 +68,8 @@ uv run pytest --cov=services,utils,orm,resolvers
```bash ```bash
# Run ruff linter # Run ruff linter
uv run ruff check . uv run ruff check . --select I
uv run ruff format --line-length=120
# Run ruff formatter
uv run ruff format .
# Run mypy type checker # Run mypy type checker
uv run mypy . uv run mypy .

View File

@@ -95,6 +95,7 @@ async def get_auth_token(request: Any) -> Optional[str]:
# 2. Проверяем наличие auth_token в scope (приоритет) # 2. Проверяем наличие auth_token в scope (приоритет)
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth_token" in request.scope: if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth_token" in request.scope:
token = request.scope.get("auth_token") token = request.scope.get("auth_token")
if token is not None:
token_len = len(token) if hasattr(token, "__len__") else "unknown" token_len = len(token) if hasattr(token, "__len__") else "unknown"
logger.debug(f"[decorators] Токен получен из request.scope['auth_token']: {token_len}") logger.debug(f"[decorators] Токен получен из request.scope['auth_token']: {token_len}")
return token return token
@@ -147,7 +148,8 @@ async def get_auth_token(request: Any) -> Optional[str]:
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope: if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope:
auth_info = request.scope.get("auth", {}) auth_info = request.scope.get("auth", {})
if isinstance(auth_info, dict) and "token" in auth_info: if isinstance(auth_info, dict) and "token" in auth_info:
token = auth_info["token"] token = auth_info.get("token")
if token is not None:
token_len = len(token) if hasattr(token, "__len__") else "unknown" token_len = len(token) if hasattr(token, "__len__") else "unknown"
logger.debug(f"[decorators] Токен получен из request.scope['auth']: {token_len}") logger.debug(f"[decorators] Токен получен из request.scope['auth']: {token_len}")
return token return token
@@ -164,6 +166,7 @@ async def get_auth_token(request: Any) -> Optional[str]:
logger.debug(f"[decorators] Токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}") logger.debug(f"[decorators] Токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}")
return token return token
token = auth_header.strip() token = auth_header.strip()
if token:
token_len = len(token) if hasattr(token, "__len__") else "unknown" token_len = len(token) if hasattr(token, "__len__") else "unknown"
logger.debug(f"[decorators] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}") logger.debug(f"[decorators] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}")
return token return token
@@ -173,6 +176,7 @@ async def get_auth_token(request: Any) -> Optional[str]:
auth_header = headers.get("authorization", "") auth_header = headers.get("authorization", "")
if auth_header and auth_header.startswith("Bearer "): if auth_header and auth_header.startswith("Bearer "):
token = auth_header[7:].strip() token = auth_header[7:].strip()
if token:
token_len = len(token) if hasattr(token, "__len__") else "unknown" token_len = len(token) if hasattr(token, "__len__") else "unknown"
logger.debug(f"[decorators] Токен получен из заголовка Authorization: {token_len}") logger.debug(f"[decorators] Токен получен из заголовка Authorization: {token_len}")
return token return token
@@ -232,6 +236,7 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
return return
# Если аутентификации нет в request.auth, пробуем получить ее из scope # Если аутентификации нет в request.auth, пробуем получить ее из scope
token: Optional[str] = None
if hasattr(request, "scope") and "auth" in request.scope: if hasattr(request, "scope") and "auth" in request.scope:
auth_cred = request.scope.get("auth") auth_cred = request.scope.get("auth")
if isinstance(auth_cred, AuthCredentials) and getattr(auth_cred, "logged_in", False): if isinstance(auth_cred, AuthCredentials) and getattr(auth_cred, "logged_in", False):
@@ -261,7 +266,8 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
return return
# Логируем информацию о найденном токене # Логируем информацию о найденном токене
logger.debug(f"[validate_graphql_context] Токен найден, длина: {len(token)}") token_len = len(token) if hasattr(token, "__len__") else 0
logger.debug(f"[validate_graphql_context] Токен найден, длина: {token_len}")
# Используем единый механизм проверки токена из auth.internal # Используем единый механизм проверки токена из auth.internal
auth_state = await authenticate(request) auth_state = await authenticate(request)

View File

@@ -1,3 +1,5 @@
from typing import Any
from ariadne.asgi.handlers import GraphQLHTTPHandler from ariadne.asgi.handlers import GraphQLHTTPHandler
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
@@ -62,20 +64,22 @@ class EnhancedGraphQLHTTPHandler(GraphQLHTTPHandler):
# Добавляем данные авторизации только если они доступны # Добавляем данные авторизации только если они доступны
# Проверяем наличие данных авторизации в scope # Проверяем наличие данных авторизации в scope
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope: if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope:
auth_cred = request.scope.get("auth") auth_cred: Any | None = request.scope.get("auth")
context["auth"] = auth_cred context["auth"] = auth_cred
# Безопасно логируем информацию о типе объекта auth # Безопасно логируем информацию о типе объекта auth
logger.debug(f"[graphql] Добавлены данные авторизации в контекст из scope: {type(auth_cred).__name__}") logger.debug(f"[graphql] Добавлены данные авторизации в контекст из scope: {type(auth_cred).__name__}")
# Проверяем, есть ли токен в auth_cred # Проверяем, есть ли токен в auth_cred
if hasattr(auth_cred, "token") and auth_cred.token: if auth_cred is not None and hasattr(auth_cred, "token") and getattr(auth_cred, "token"):
logger.debug(f"[graphql] Токен найден в auth_cred: {len(auth_cred.token)}") token_val = auth_cred.token
token_len = len(token_val) if hasattr(token_val, "__len__") else 0
logger.debug(f"[graphql] Токен найден в auth_cred: {token_len}")
else: else:
logger.debug("[graphql] Токен НЕ найден в auth_cred") logger.debug("[graphql] Токен НЕ найден в auth_cred")
# Добавляем author_id в контекст для RBAC # Добавляем author_id в контекст для RBAC
author_id = None author_id = None
if hasattr(auth_cred, "author_id") and auth_cred.author_id: if auth_cred is not None and hasattr(auth_cred, "author_id") and getattr(auth_cred, "author_id"):
author_id = auth_cred.author_id author_id = auth_cred.author_id
elif isinstance(auth_cred, dict) and "author_id" in auth_cred: elif isinstance(auth_cred, dict) and "author_id" in auth_cred:
author_id = auth_cred["author_id"] author_id = auth_cred["author_id"]

View File

@@ -135,7 +135,7 @@ class AuthMiddleware:
# Роли пользователя будут определяться в контексте конкретной операции # Роли пользователя будут определяться в контексте конкретной операции
# через RBAC систему, а не здесь # через RBAC систему, а не здесь
roles = [] roles: list[str] = []
# Обновляем last_seen # Обновляем last_seen
author.last_seen = int(time.time()) author.last_seen = int(time.time())

View File

@@ -55,12 +55,16 @@ class BatchTokenOperations(BaseTokenManager):
valid_tokens = [] valid_tokens = []
for token, payload in zip(token_batch, decoded_payloads): for token, payload in zip(token_batch, decoded_payloads):
if isinstance(payload, Exception) or not payload: if isinstance(payload, Exception) or payload is None:
results[token] = False results[token] = False
continue continue
# payload может быть словарем или объектом, обрабатываем оба случая # payload может быть словарем или объектом, обрабатываем оба случая
user_id = payload.user_id if hasattr(payload, "user_id") else payload.get("user_id") user_id = (
payload.user_id
if hasattr(payload, "user_id")
else (payload.get("user_id") if isinstance(payload, dict) else None)
)
if not user_id: if not user_id:
results[token] = False results[token] = False
continue continue
@@ -119,10 +123,18 @@ class BatchTokenOperations(BaseTokenManager):
# Декодируем токены и подготавливаем операции # Декодируем токены и подготавливаем операции
for token in token_batch: for token in token_batch:
payload = await self._safe_decode_token(token) payload = await self._safe_decode_token(token)
if payload: if payload is not None:
# payload может быть словарем или объектом, обрабатываем оба случая # payload может быть словарем или объектом, обрабатываем оба случая
user_id = payload.user_id if hasattr(payload, "user_id") else payload.get("user_id") user_id = (
username = payload.username if hasattr(payload, "username") else payload.get("username") payload.user_id
if hasattr(payload, "user_id")
else (payload.get("user_id") if isinstance(payload, dict) else None)
)
username = (
payload.username
if hasattr(payload, "username")
else (payload.get("username") if isinstance(payload, dict) else None)
)
if not user_id: if not user_id:
continue continue

View File

@@ -652,25 +652,11 @@ class CommunityAuthor(BaseModel):
Returns: Returns:
Словарь со статистикой ролей Словарь со статистикой ролей
""" """
# Загружаем список авторов сообщества (одним способом вне зависимости от сессии)
if session is None: if session is None:
with local_session() as s: with local_session() as s:
community_authors = s.query(cls).where(cls.community_id == community_id).all() community_authors = s.query(cls).where(cls.community_id == community_id).all()
else:
role_counts: dict[str, int] = {}
total_members = len(community_authors)
for ca in community_authors:
for role in ca.role_list:
role_counts[role] = role_counts.get(role, 0) + 1
return {
"total_members": total_members,
"role_counts": role_counts,
"roles_distribution": {
role: count / total_members if total_members > 0 else 0 for role, count in role_counts.items()
},
}
community_authors = session.query(cls).where(cls.community_id == community_id).all() community_authors = session.query(cls).where(cls.community_id == community_id).all()
role_counts: dict[str, int] = {} role_counts: dict[str, int] = {}

View File

@@ -7,6 +7,7 @@ from sqlalchemy.pool import StaticPool
import time import time
import uuid import uuid
from starlette.testclient import TestClient from starlette.testclient import TestClient
import requests
from services.redis import redis from services.redis import redis
from orm.base import BaseModel as Base from orm.base import BaseModel as Base
@@ -27,6 +28,22 @@ def get_test_client():
return app return app
return TestClient(_import_app()) return TestClient(_import_app())
@pytest.fixture(autouse=True, scope="session")
def _set_requests_default_timeout():
"""Глобально задаем таймаут по умолчанию для requests в тестах, чтобы исключить зависания.
🪓 Упрощение: мокаем методы requests, добавляя timeout=10, если он не указан.
"""
original_request = requests.sessions.Session.request
def request_with_default_timeout(self, method, url, **kwargs): # type: ignore[override]
if "timeout" not in kwargs:
kwargs["timeout"] = 10
return original_request(self, method, url, **kwargs)
requests.sessions.Session.request = request_with_default_timeout # type: ignore[assignment]
yield
requests.sessions.Session.request = original_request # type: ignore[assignment]
@pytest.fixture(scope="session") @pytest.fixture(scope="session")

View File

@@ -60,8 +60,8 @@ class TestCommunityDeleteE2EBrowser:
# В CI/CD используем uv run python # В CI/CD используем uv run python
backend_process = subprocess.Popen( backend_process = subprocess.Popen(
["uv", "run", "python", "dev.py"], ["uv", "run", "python", "dev.py"],
stdout=subprocess.PIPE, stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE, stderr=subprocess.DEVNULL,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
) )
@@ -80,11 +80,7 @@ class TestCommunityDeleteE2EBrowser:
# Если сервер не запустился, выводим логи и завершаем тест # Если сервер не запустился, выводим логи и завершаем тест
print("❌ Бэкенд сервер не запустился за 20 секунд") print("❌ Бэкенд сервер не запустился за 20 секунд")
# Получаем логи процесса # Логи процесса не собираем, чтобы не блокировать выполнение
if backend_process:
stdout, stderr = backend_process.communicate()
print(f"📋 STDOUT: {stdout.decode()}")
print(f"📋 STDERR: {stderr.decode()}")
raise Exception("Бэкенд сервер не запустился за 20 секунд") raise Exception("Бэкенд сервер не запустился за 20 секунд")
@@ -128,8 +124,8 @@ class TestCommunityDeleteE2EBrowser:
try: try:
frontend_process = subprocess.Popen( frontend_process = subprocess.Popen(
["npm", "run", "dev"], ["npm", "run", "dev"],
stdout=subprocess.PIPE, stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE, stderr=subprocess.DEVNULL,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
) )
@@ -149,11 +145,7 @@ class TestCommunityDeleteE2EBrowser:
# Если фронтенд не запустился, выводим логи # Если фронтенд не запустился, выводим логи
print("❌ Фронтенд сервер не запустился за 15 секунд") print("❌ Фронтенд сервер не запустился за 15 секунд")
# Получаем логи процесса # Логи процесса не собираем, чтобы не блокировать выполнение
if frontend_process:
stdout, stderr = frontend_process.communicate()
print(f"📋 STDOUT: {stdout.decode()}")
print(f"📋 STDERR: {stderr.decode()}")
print("⚠️ Продолжаем тест без фронтенда (только API тесты)") print("⚠️ Продолжаем тест без фронтенда (только API тесты)")
frontend_process = None frontend_process = None