LichessStatTgWeb/LichessWebServices/models.py
vrubelroman 619c00aa06
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 12s
stop infinite retry loop on rejected Lichess tokens
A gamer's periodic check would get permanently stuck if their stored token
was revoked/expired: our stats API collapsed both "Lichess rejected the
token" (401/403, permanent) and genuine transient errors into the same 502
response, so the bot treated an invalid token exactly like a network blip —
retrying the same window forever at a capped 300s backoff, never advancing
the checkpoint (observed in prod: Dor1zz stuck for 100+ consecutive errors
over 8+ hours, admin alerts firing every 25 failures).

Preserve the distinction that already existed one layer down (lichess_client.py
already tells 401/403 apart from other failures) instead of collapsing it in
stats_service.py: add PuzzleOfPeriodResponse.auth_failed, have main.py return
401 specifically for that case, and have the bot raise a distinct
InvalidTokenError instead of returning None. On InvalidTokenError, the bot now
clears the token for that pair, notifies the user to reconnect via /addtoken,
and continues tracking games normally instead of stalling forever.
2026-07-05 07:37:14 +00:00

261 lines
18 KiB
Python
Raw Permalink 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.

"""
Lichess Statistics API - Модели данных
Этот модуль содержит все Pydantic модели для валидации и сериализации данных.
Модели используются для:
- Валидации входных параметров API
- Сериализации ответов API
- Документации в Swagger UI
- Типизации данных в коде
Автор: Lichess Web Services Team
Версия: 1.0.0
"""
from pydantic import BaseModel, Field
from typing import Dict, List, Optional, Any
# =============================================================================
# МОДЕЛИ СТАТИСТИКИ ЗАДАЧ (ПАЗЛОВ)
# =============================================================================
class TaskStats(BaseModel):
"""
Статистика решения задач (пазлов) пользователя.
Содержит информацию о том, сколько задач пользователь решил,
сколько решил правильно и сколько не решил или решил неправильно.
"""
total: int = Field(..., description="Общее количество решенных задач", example=15)
solved: int = Field(..., description="Количество правильно решенных задач", example=12)
unsolved: int = Field(..., description="Количество нерешенных или неправильно решенных задач", example=3)
# =============================================================================
# МОДЕЛИ СТАТИСТИКИ ИГР
# =============================================================================
class GameModeStats(BaseModel):
"""
Статистика игр для конкретного режима (Bullet, Blitz, Rapid и т.д.).
Содержит полную статистику по играм в определенном временном формате:
- Количество сыгранных игр
- Изменение рейтинга за период
- Текущий рейтинг
- Результаты игр (победы, поражения, ничьи)
"""
games_played: int = Field(..., description="Общее количество сыгранных игр", example=8)
rating_change: int = Field(..., description="Изменение рейтинга (может быть отрицательным)", example=15)
final_rating: int = Field(..., description="Текущий рейтинг игрока", example=2850)
wins: int = Field(..., description="Количество побед", example=5)
losses: int = Field(..., description="Количество поражений", example=2)
draws: int = Field(..., description="Количество ничьих", example=1)
class GamesStats(BaseModel):
"""
Статистика игр по всем режимам.
Агрегирует статистику игр по всем временным форматам:
- Bullet: быстрые игры (1-3 минуты)
- Blitz: блиц игры (3-10 минут)
- Rapid: рапид игры (10+ минут)
- Classical: классические игры (30+ минут)
"""
bullet: GameModeStats = Field(..., description="Статистика Bullet игр (1-3 минуты)")
blitz: GameModeStats = Field(..., description="Статистика Blitz игр (3-10 минут)")
rapid: GameModeStats = Field(..., description="Статистика Rapid игр (10+ минут)")
classical: GameModeStats = Field(..., description="Статистика Classical игр (30+ минут)")
class UserStats(BaseModel):
"""
Полная статистика пользователя.
Содержит всю доступную статистику по пользователю:
- Статистику решения задач (пазлов)
- Статистику игр по всем режимам
"""
username: str = Field(..., description="Имя пользователя на Lichess", example="magnus")
tasks: TaskStats = Field(..., description="Статистика решения задач")
games: GamesStats = Field(..., description="Статистика игр по всем режимам")
# =============================================================================
# МОДЕЛИ ОТВЕТОВ API
# =============================================================================
class ActivityResponse(BaseModel):
"""
Стандартный ответ API с результатами запроса статистики.
Используется для всех эндпоинтов статистики (сегодня, вчера, неделя).
Содержит сообщение о результате и данные статистики пользователя.
"""
message: str = Field(..., description="Сообщение о результате запроса", example="Статистика за сегодняшний день")
data: Optional[UserStats] = Field(None, description="Данные статистики пользователя (null если пользователь не найден или неактивен)")
class ErrorResponse(BaseModel):
"""
Модель для стандартизированных ошибок API.
Используется для возврата структурированных ошибок с дополнительной информацией.
"""
detail: str = Field(..., description="Описание ошибки", example="Пользователь не найден")
error_code: Optional[str] = Field(None, description="Код ошибки", example="USER_NOT_FOUND")
timestamp: Optional[str] = Field(None, description="Время возникновения ошибки", example="2024-01-15T10:30:00Z")
class HealthResponse(BaseModel):
"""
Ответ для health check эндпоинта.
Используется для мониторинга состояния сервиса и проверки его работоспособности.
"""
status: str = Field(..., description="Статус сервиса", example="healthy")
timestamp: str = Field(..., description="Время проверки", example="2024-01-15T10:30:00Z")
service: str = Field(..., description="Название сервиса", example="Lichess Statistics API")
# =============================================================================
# МОДЕЛИ ДЛЯ ЭНДПОИНТА СТАТИСТИКИ ИГР ЗА ПЕРИОД
# =============================================================================
class GamePlayer(BaseModel):
"""
Информация об игроке в партии.
Содержит данные о пользователе, его рейтинге и изменении рейтинга в конкретной игре.
"""
user: Optional[Dict[str, Any]] = Field(None, description="Информация о пользователе")
rating: Optional[int] = Field(None, description="Рейтинг игрока")
ratingDiff: Optional[int] = Field(None, description="Изменение рейтинга")
analysis: Optional[Dict[str, Any]] = Field(None, description="Пост-анализ партии (только если игрок запускал анализ на Lichess), содержит поле 'accuracy'")
class Game(BaseModel):
"""
Модель игры из Lichess API.
Содержит полную информацию об игре, включая:
- Метаданные игры (ID, время создания, статус)
- Информацию об игроках и их рейтингах
- Результат игры и ходы в PGN формате
"""
id: str = Field(..., description="ID игры")
rated: bool = Field(..., description="Рейтинговая ли игра")
variant: str = Field(..., description="Вариант игры")
speed: str = Field(..., description="Скорость игры (bullet, blitz, rapid, classical, correspondence)")
perf: str = Field(..., description="Тип производительности")
createdAt: int = Field(..., description="Время создания игры (timestamp)")
lastMoveAt: int = Field(..., description="Время последнего хода (timestamp)")
status: str = Field(..., description="Статус игры")
players: Dict[str, GamePlayer] = Field(..., description="Игроки (white, black)")
winner: Optional[str] = Field(None, description="Победитель (white, black или null)")
moves: str = Field(..., description="Ходы игры в PGN формате")
class GameRow(BaseModel):
"""
Одна отдельная партия для построчного разбора (today/yesterday/периодические уведомления).
Порядок игроков — как в партии (белые/чёрные), а не "отслеживаемый пользователь/соперник".
"""
white_name: str = Field(..., description="Имя игрока белыми")
black_name: str = Field(..., description="Имя игрока чёрными")
white_rating: Optional[int] = Field(None, description="Рейтинг белых")
black_rating: Optional[int] = Field(None, description="Рейтинг чёрных")
white_accuracy: Optional[float] = Field(None, description="Точность белых, если партия анализировалась")
black_accuracy: Optional[float] = Field(None, description="Точность чёрных, если партия анализировалась")
result: str = Field(..., description="Результат партии: '1-0', '0-1' или '1/2-1/2'")
created_at: int = Field(..., description="Время создания партии (Unix timestamp в миллисекундах)")
tracked_is_white: Optional[bool] = Field(None, description="True, если отслеживаемый пользователь играл белыми; False — чёрными; None — не удалось определить сторону")
class GameStats(BaseModel):
"""
Статистика игр по конкретному типу (Bullet, Blitz, Rapid и т.д.).
Содержит агрегированную статистику по играм определенного типа:
- Количество сыгранных игр
- Результаты игр (победы, поражения, ничьи)
- Общее изменение рейтинга
- Итоговый рейтинг после последней игры
"""
games_played: int = Field(..., description="Общее количество сыгранных игр", example=10)
wins: int = Field(..., description="Количество побед", example=6)
losses: int = Field(..., description="Количество поражений", example=3)
draws: int = Field(..., description="Количество ничьих", example=1)
rating_change: int = Field(..., description="Общее изменение рейтинга", example=15)
rating: Optional[int] = Field(None, description="Итоговый рейтинг после последней игры (только если games_played > 0)", example=2850)
accuracy: Optional[float] = Field(None, description="Средняя точность отслеживаемого пользователя по проанализированным партиям этого режима; None если нет проанализированных партий", example=82.5)
games: Optional[List[GameRow]] = Field(None, description="Построчный список отдельных партий (только blitz/rapid/classical), заполняется только если запрошено include_games=True")
class GamesOfPeriodStats(BaseModel):
"""
Статистика игр за период по всем типам.
Агрегирует статистику игр по всем временным форматам:
- Bullet, Blitz, Rapid, Classical, Correspondence
- Общая статистика по всем типам
"""
bullet: GameStats = Field(..., description="Статистика Bullet игр")
blitz: GameStats = Field(..., description="Статистика Blitz игр")
rapid: GameStats = Field(..., description="Статистика Rapid игр")
classical: GameStats = Field(..., description="Статистика Classical игр")
correspondence: GameStats = Field(..., description="Статистика Correspondence игр")
total: GameStats = Field(..., description="Общая статистика по всем типам")
class GamesOfPeriodResponse(BaseModel):
"""
Ответ API с результатами запроса статистики игр за период.
Содержит метаинформацию о запросе и агрегированную статистику игр.
"""
message: str = Field(..., description="Сообщение о результате запроса", example="Статистика игр за период")
success: bool = Field(True, description="False, если запрос к Lichess завершился ошибкой (а не легитимным нулевым результатом)", example=True)
username: str = Field(..., description="Имя пользователя", example="magnus")
period_start: int = Field(..., description="Начало периода (Unix timestamp)", example=1640995200)
period_end: int = Field(..., description="Конец периода (Unix timestamp)", example=1641081600)
games_count: int = Field(..., description="Общее количество игр", example=25)
earliest_game_ts: Optional[int] = Field(None, description="Время самой старой найденной партии (Unix timestamp, секунды)", example=1638316800)
data: Optional[GamesOfPeriodStats] = Field(None, description="Данные статистики игр")
# =============================================================================
# МОДЕЛИ ДЛЯ ЭНДПОИНТА СТАТИСТИКИ ЗАДАЧ ЗА ПЕРИОД
# =============================================================================
class PuzzleActivity(BaseModel):
"""
Активность по решению задачи (пазла) из Lichess API.
Содержит информацию о попытке решения задачи пользователем:
- ID задачи и время решения
- Результат решения (решена/не решена)
- Дополнительная информация о задаче
"""
id: str = Field(..., description="ID задачи", example="abc123")
createdAt: int = Field(..., description="Время создания активности (timestamp в миллисекундах)", example=1640995200000)
win: bool = Field(..., description="Решена ли задача правильно", example=True)
puzzle: Dict[str, Any] = Field(..., description="Информация о задаче")
class PuzzleStats(BaseModel):
"""
Агрегированная статистика решения задач за период.
Содержит сводную информацию о решении задач:
- Общее количество попыток
- Количество успешных и неуспешных решений
- Процент успешности
"""
total_attempts: int = Field(..., description="Общее количество попыток решения", example=25)
solved: int = Field(..., description="Количество решенных задач", example=18)
failed: int = Field(..., description="Количество нерешенных задач", example=7)
success_rate: float = Field(..., description="Процент успешных решений", example=72.0)
class PuzzleOfPeriodResponse(BaseModel):
"""
Ответ API с результатами запроса статистики решения задач за период.
Содержит метаинформацию о запросе и агрегированную статистику решения задач.
"""
message: str = Field(..., description="Сообщение о результате запроса", example="Статистика решения задач за период")
success: bool = Field(True, description="False, если запрос к Lichess завершился ошибкой (а не легитимным нулевым результатом)", example=True)
auth_failed: bool = Field(False, description="True, если Lichess отклонил токен (401/403) — permanent, не стоит ретраить; отличается от транзитных ошибок")
period_start: int = Field(..., description="Начало периода (Unix timestamp в миллисекундах)", example=1640995200000)
period_end: int = Field(..., description="Конец периода (Unix timestamp в миллисекундах)", example=1641081600000)
max_puzzles: int = Field(..., description="Максимальное количество задач для получения", example=50)
puzzles_in_period: int = Field(..., description="Количество задач в указанном периоде", example=15)
data: Optional[PuzzleStats] = Field(None, description="Данные статистики решения задач")