Создание единого проекта Lichess Statistics Ecosystem
- Объединены три проекта в один репозиторий - LichessWebServices - REST API для статистики - LichessClientTG_bot - Telegram бот с поддержкой множества пользователей - LichessWebView - Веб-интерфейс для просмотра пользователей и игроков - Добавлен общий docker-compose.yml для запуска всех сервисов - Добавлен скрипт start.sh для удобного запуска - Добавлен README с полным описанием проекта
This commit is contained in:
commit
a08fc8c962
32 changed files with 4990 additions and 0 deletions
58
LichessWebServices/.gitignore
vendored
Normal file
58
LichessWebServices/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
388
LichessWebServices/API_DOCUMENTATION.md
Normal file
388
LichessWebServices/API_DOCUMENTATION.md
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# Lichess Statistics API - Документация
|
||||
|
||||
## Описание
|
||||
|
||||
Lichess Statistics API предоставляет REST API для получения детальной статистики игроков платформы Lichess. API позволяет получать информацию о играх, рейтингах и решении задач (пазлов) за различные периоды времени.
|
||||
|
||||
## Возможности
|
||||
|
||||
- 📊 **Статистика игр** по режимам (Bullet, Blitz, Rapid)
|
||||
- 🧩 **Статистика решения задач** (пазлов)
|
||||
- 📅 **Статистика за разные периоды** (сегодня, вчера, неделя)
|
||||
- 🎯 **Отслеживание изменений рейтинга**
|
||||
- 📈 **Подробная аналитика результатов игр**
|
||||
|
||||
## Режимы игр
|
||||
|
||||
- **Bullet**: Быстрые игры (1-3 минуты)
|
||||
- **Blitz**: Блиц игры (3-10 минут)
|
||||
- **Rapid**: Рапид игры (10+ минут)
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
http://localhost:8001
|
||||
```
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### 1. Информация об API
|
||||
|
||||
#### GET /
|
||||
Возвращает основную информацию об API и доступных эндпоинтах.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"message": "Lichess Statistics API",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API для получения статистики игроков Lichess",
|
||||
"endpoints": {
|
||||
"today": "/stats/{username}/today",
|
||||
"yesterday": "/stats/{username}/yesterday",
|
||||
"week": "/stats/{username}/week"
|
||||
},
|
||||
"documentation": "/docs",
|
||||
"openapi_schema": "/openapi.json"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Health Check
|
||||
|
||||
#### GET /health
|
||||
Проверка состояния сервиса.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"service": "Lichess Statistics API"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Статистика за сегодня
|
||||
|
||||
#### GET /stats/{username}/today
|
||||
Получает статистику игрока за сегодняшний день.
|
||||
|
||||
**Параметры:**
|
||||
- `username` (string, обязательный) - имя пользователя на Lichess
|
||||
|
||||
**Пример запроса:**
|
||||
```bash
|
||||
curl http://localhost:8001/stats/magnus/today
|
||||
```
|
||||
|
||||
**Пример ответа:**
|
||||
```json
|
||||
{
|
||||
"message": "Статистика за сегодняшний день",
|
||||
"data": {
|
||||
"username": "magnus",
|
||||
"tasks": {
|
||||
"total": 15,
|
||||
"solved": 12,
|
||||
"unsolved": 3
|
||||
},
|
||||
"games": {
|
||||
"bullet": {
|
||||
"games_played": 8,
|
||||
"rating_change": 15,
|
||||
"final_rating": 2850,
|
||||
"wins": 5,
|
||||
"losses": 2,
|
||||
"draws": 1
|
||||
},
|
||||
"blitz": {
|
||||
"games_played": 3,
|
||||
"rating_change": -5,
|
||||
"final_rating": 2750,
|
||||
"wins": 1,
|
||||
"losses": 2,
|
||||
"draws": 0
|
||||
},
|
||||
"rapid": {
|
||||
"games_played": 0,
|
||||
"rating_change": 0,
|
||||
"final_rating": 0,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Статистика за вчера
|
||||
|
||||
#### GET /stats/{username}/yesterday
|
||||
Получает статистику игрока за вчерашний день.
|
||||
|
||||
**Параметры:**
|
||||
- `username` (string, обязательный) - имя пользователя на Lichess
|
||||
|
||||
**Пример запроса:**
|
||||
```bash
|
||||
curl http://localhost:8001/stats/magnus/yesterday
|
||||
```
|
||||
|
||||
### 5. Статистика за неделю
|
||||
|
||||
#### GET /stats/{username}/week
|
||||
Получает агрегированную статистику игрока за последние 7 дней.
|
||||
|
||||
**Параметры:**
|
||||
- `username` (string, обязательный) - имя пользователя на Lichess
|
||||
|
||||
**Пример запроса:**
|
||||
```bash
|
||||
curl http://localhost:8001/stats/magnus/week
|
||||
```
|
||||
|
||||
### 6. Статистика игр за период
|
||||
|
||||
#### GET /games/{username}/period
|
||||
Получает детальную статистику игр пользователя за указанный период времени.
|
||||
|
||||
**Параметры:**
|
||||
- `username` (string, обязательный) - имя пользователя на Lichess
|
||||
- `since` (integer, обязательный) - начало периода (Unix timestamp в миллисекундах)
|
||||
- `until` (integer, обязательный) - конец периода (Unix timestamp в миллисекундах)
|
||||
- `rated_only` (boolean, опциональный) - только рейтинговые игры (по умолчанию true - рекомендуется)
|
||||
|
||||
**Пример запроса:**
|
||||
```bash
|
||||
# Статистика за последние 7 дней
|
||||
curl "http://localhost:8001/games/magnus/period?since=1640995200000&until=1641081600000"
|
||||
|
||||
# Только рейтинговые игры
|
||||
curl "http://localhost:8001/games/magnus/period?since=1640995200000&until=1641081600000&rated_only=true"
|
||||
|
||||
# Все игры (включая нерейтинговые)
|
||||
curl "http://localhost:8001/games/magnus/period?since=1640995200000&until=1641081600000&rated_only=false"
|
||||
```
|
||||
|
||||
**Пример ответа:**
|
||||
```json
|
||||
{
|
||||
"message": "Статистика игр за период",
|
||||
"username": "magnus",
|
||||
"period_start": 1640995200000,
|
||||
"period_end": 1641081600000,
|
||||
"games_count": 25,
|
||||
"data": {
|
||||
"bullet": {
|
||||
"games_played": 10,
|
||||
"wins": 6,
|
||||
"losses": 3,
|
||||
"draws": 1,
|
||||
"rating_change": 15
|
||||
},
|
||||
"blitz": {
|
||||
"games_played": 8,
|
||||
"wins": 5,
|
||||
"losses": 2,
|
||||
"draws": 1,
|
||||
"rating_change": 12
|
||||
},
|
||||
"rapid": {
|
||||
"games_played": 5,
|
||||
"wins": 3,
|
||||
"losses": 1,
|
||||
"draws": 1,
|
||||
"rating_change": 8
|
||||
},
|
||||
"classical": {
|
||||
"games_played": 2,
|
||||
"wins": 1,
|
||||
"losses": 1,
|
||||
"draws": 0,
|
||||
"rating_change": 0
|
||||
},
|
||||
"correspondence": {
|
||||
"games_played": 0,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 0,
|
||||
"rating_change": 0
|
||||
},
|
||||
"total": {
|
||||
"games_played": 25,
|
||||
"wins": 15,
|
||||
"losses": 7,
|
||||
"draws": 3,
|
||||
"rating_change": 35
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Статистика решения задач за период
|
||||
|
||||
#### GET /puzzle/period
|
||||
Получает статистику решения задач (пазлов) за указанный период времени. Требует авторизации через Bearer токен.
|
||||
|
||||
**Параметры:**
|
||||
- `since` (integer, обязательный) - начало периода (Unix timestamp в миллисекундах)
|
||||
- `until` (integer, обязательный) - конец периода (Unix timestamp в миллисекундах)
|
||||
- `max` (integer, опциональный) - максимальное количество задач для получения (по умолчанию 50, максимум 1000)
|
||||
- `Authorization` (header, обязательный) - Bearer токен авторизации
|
||||
|
||||
**Важно:** Параметр `max` ограничивает количество активностей, получаемых от Lichess API. Если в указанном периоде было больше задач, чем указано в `max`, то будут показаны только последние N активностей. Для получения полной статистики рекомендуется увеличить значение `max` или использовать значение по умолчанию (50).
|
||||
|
||||
**Пример запроса:**
|
||||
```bash
|
||||
# Статистика за последние 7 дней
|
||||
curl -H "Authorization: Bearer your_token_here" \
|
||||
"http://localhost:8001/puzzle/period?since=1640995200000&until=1641081600000"
|
||||
|
||||
# Больше задач
|
||||
curl -H "Authorization: Bearer your_token_here" \
|
||||
"http://localhost:8001/puzzle/period?since=1640995200000&until=1641081600000&max=100"
|
||||
```
|
||||
|
||||
**Пример ответа:**
|
||||
```json
|
||||
{
|
||||
"message": "Статистика решения задач за период",
|
||||
"period_start": 1640995200000,
|
||||
"period_end": 1641081600000,
|
||||
"max_puzzles": 50,
|
||||
"puzzles_in_period": 15,
|
||||
"data": {
|
||||
"total_attempts": 15,
|
||||
"solved": 12,
|
||||
"failed": 3,
|
||||
"success_rate": 80.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Получение токена:**
|
||||
1. Зайдите на https://lichess.org/account/oauth/token/create
|
||||
2. Создайте новый токен с правами на чтение активности
|
||||
3. Используйте токен в заголовке Authorization
|
||||
|
||||
## Модели данных
|
||||
|
||||
### TaskStats
|
||||
Статистика решения задач (пазлов):
|
||||
```json
|
||||
{
|
||||
"total": 15, // Общее количество решенных задач
|
||||
"solved": 12, // Количество правильно решенных задач
|
||||
"unsolved": 3 // Количество нерешенных или неправильно решенных задач
|
||||
}
|
||||
```
|
||||
|
||||
### GameModeStats
|
||||
Статистика игр для конкретного режима:
|
||||
```json
|
||||
{
|
||||
"games_played": 8, // Общее количество сыгранных игр
|
||||
"rating_change": 15, // Изменение рейтинга (может быть отрицательным)
|
||||
"final_rating": 2850, // Текущий рейтинг игрока
|
||||
"wins": 5, // Количество побед
|
||||
"losses": 2, // Количество поражений
|
||||
"draws": 1 // Количество ничьих
|
||||
}
|
||||
```
|
||||
|
||||
### GamesStats
|
||||
Статистика игр по всем режимам:
|
||||
```json
|
||||
{
|
||||
"bullet": { /* GameModeStats */ },
|
||||
"blitz": { /* GameModeStats */ },
|
||||
"rapid": { /* GameModeStats */ }
|
||||
}
|
||||
```
|
||||
|
||||
### UserStats
|
||||
Полная статистика пользователя:
|
||||
```json
|
||||
{
|
||||
"username": "magnus",
|
||||
"tasks": { /* TaskStats */ },
|
||||
"games": { /* GamesStats */ }
|
||||
}
|
||||
```
|
||||
|
||||
### ActivityResponse
|
||||
Ответ API с результатами запроса:
|
||||
```json
|
||||
{
|
||||
"message": "Статистика за сегодняшний день",
|
||||
"data": { /* UserStats или null */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
- **200** - Успешный запрос
|
||||
- **404** - Пользователь не найден или неактивен
|
||||
- **500** - Внутренняя ошибка сервера
|
||||
|
||||
## Примеры ошибок
|
||||
|
||||
### Пользователь не найден
|
||||
```json
|
||||
{
|
||||
"message": "Пользователь magnus не найден или неактивен"
|
||||
}
|
||||
```
|
||||
|
||||
### Внутренняя ошибка сервера
|
||||
```json
|
||||
{
|
||||
"detail": "Внутренняя ошибка сервера: Connection timeout"
|
||||
}
|
||||
```
|
||||
|
||||
## Swagger UI
|
||||
|
||||
Интерактивная документация доступна по адресу:
|
||||
```
|
||||
http://localhost:8001/docs
|
||||
```
|
||||
|
||||
## OpenAPI Schema
|
||||
|
||||
Схема OpenAPI доступна по адресу:
|
||||
```
|
||||
http://localhost:8001/openapi.json
|
||||
```
|
||||
|
||||
## Запуск в Docker
|
||||
|
||||
```bash
|
||||
# Сборка и запуск
|
||||
docker compose up --build -d
|
||||
|
||||
# Проверка статуса
|
||||
docker compose ps
|
||||
|
||||
# Просмотр логов
|
||||
docker compose logs
|
||||
|
||||
# Остановка
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Разработка
|
||||
|
||||
### Установка зависимостей
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Запуск в режиме разработки
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 8001 --reload
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT License
|
||||
26
LichessWebServices/Dockerfile
Normal file
26
LichessWebServices/Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Lichess Statistics API - Dockerfile
|
||||
#
|
||||
# Этот Dockerfile создает образ для запуска Lichess Statistics API
|
||||
# в контейнере Docker.
|
||||
|
||||
# Используем официальный Python образ на базе Debian slim
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Устанавливаем рабочую директорию в контейнере
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файл зависимостей и устанавливаем пакеты
|
||||
# Делаем это отдельно для кэширования слоев Docker
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем весь код приложения
|
||||
COPY . .
|
||||
|
||||
# Открываем порт 8000 для HTTP запросов
|
||||
EXPOSE 8000
|
||||
|
||||
# Запускаем FastAPI приложение через Uvicorn
|
||||
# --host 0.0.0.0 позволяет принимать соединения с любого IP
|
||||
# --port 8000 указывает порт для прослушивания
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
121
LichessWebServices/README.md
Normal file
121
LichessWebServices/README.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Lichess Statistics API
|
||||
|
||||
REST API сервис для получения статистики игроков Lichess.org по играм и решению задач.
|
||||
|
||||
## Возможности
|
||||
|
||||
- Получение статистики за сегодняшний день
|
||||
- Получение статистики за вчерашний день
|
||||
- Получение статистики за последние 7 дней
|
||||
- Поддержка режимов игры: Bullet, Blitz, Rapid
|
||||
- Статистика по решению задач (пазлов)
|
||||
- Расчет изменения рейтинга
|
||||
- Подсчет побед, поражений и ничьих
|
||||
|
||||
## Запуск с Docker
|
||||
|
||||
### 1. Сборка и запуск контейнера
|
||||
|
||||
```bash
|
||||
# Сборка образа
|
||||
docker-compose build
|
||||
|
||||
# Запуск сервиса
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Проверка работы
|
||||
|
||||
Сервис будет доступен по адресу: http://localhost:8000
|
||||
|
||||
- Документация API: http://localhost:8000/docs
|
||||
- Проверка здоровья: http://localhost:8000/health
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Получить статистику за сегодня
|
||||
```
|
||||
GET /stats/{username}/today
|
||||
```
|
||||
|
||||
### Получить статистику за вчера
|
||||
```
|
||||
GET /stats/{username}/yesterday
|
||||
```
|
||||
|
||||
### Получить статистику за неделю
|
||||
```
|
||||
GET /stats/{username}/week
|
||||
```
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Статистика за сегодня
|
||||
```bash
|
||||
curl http://localhost:8000/stats/vrubelroman/today
|
||||
```
|
||||
|
||||
### Статистика за вчера
|
||||
```bash
|
||||
curl http://localhost:8000/stats/vrubelroman/yesterday
|
||||
```
|
||||
|
||||
### Статистика за неделю
|
||||
```bash
|
||||
curl http://localhost:8000/stats/vrubelroman/week
|
||||
```
|
||||
|
||||
## Формат ответа
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Статистика за сегодняшний день",
|
||||
"data": {
|
||||
"username": "vrubelroman",
|
||||
"tasks": {
|
||||
"total": 28,
|
||||
"solved": 25,
|
||||
"unsolved": 3
|
||||
},
|
||||
"games": {
|
||||
"bullet": {
|
||||
"games_played": 7,
|
||||
"rating_change": 30,
|
||||
"final_rating": 2320,
|
||||
"wins": 5,
|
||||
"losses": 0,
|
||||
"draws": 0
|
||||
},
|
||||
"blitz": {
|
||||
"games_played": 5,
|
||||
"rating_change": 32,
|
||||
"final_rating": 2224,
|
||||
"wins": 5,
|
||||
"losses": 0,
|
||||
"draws": 0
|
||||
},
|
||||
"rapid": {
|
||||
"games_played": 2,
|
||||
"rating_change": -10,
|
||||
"final_rating": 2210,
|
||||
"wins": 1,
|
||||
"losses": 1,
|
||||
"draws": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Остановка сервиса
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Логи
|
||||
|
||||
Для просмотра логов:
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
30
LichessWebServices/docker-compose.yml
Normal file
30
LichessWebServices/docker-compose.yml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Lichess Statistics API - Docker Compose
|
||||
#
|
||||
# Этот файл определяет сервисы для запуска Lichess Statistics API
|
||||
# в контейнере Docker с помощью Docker Compose.
|
||||
|
||||
services:
|
||||
# Основной сервис API
|
||||
lichess-api:
|
||||
# Собираем образ из Dockerfile в текущей директории
|
||||
build: .
|
||||
|
||||
# Маппинг портов: хост:контейнер
|
||||
# 8001 на хосте -> 8000 в контейнере
|
||||
# Изменено с 8000:8000 из-за конфликта портов
|
||||
ports:
|
||||
- "8001:8000"
|
||||
|
||||
# Переменные окружения
|
||||
environment:
|
||||
# Отключаем буферизацию Python для корректного вывода логов
|
||||
- PYTHONUNBUFFERED=1
|
||||
|
||||
# Монтируем текущую директорию в контейнер для разработки
|
||||
# Это позволяет видеть изменения кода без пересборки образа
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
# Политика перезапуска: перезапускать контейнер при сбое
|
||||
# (кроме случаев ручной остановки)
|
||||
restart: unless-stopped
|
||||
250
LichessWebServices/lichess_client.py
Normal file
250
LichessWebServices/lichess_client.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
"""
|
||||
Lichess Statistics API - Клиент для работы с Lichess API
|
||||
|
||||
Этот модуль содержит класс LichessClient для взаимодействия с официальным API Lichess.
|
||||
Обеспечивает:
|
||||
- Получение активности пользователей
|
||||
- Получение игр за период
|
||||
- Получение активности по решению задач (пазлов)
|
||||
- Обработку ошибок и таймаутов
|
||||
- Парсинг NDJSON формата
|
||||
|
||||
Автор: Lichess Web Services Team
|
||||
Версия: 1.0.0
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import json
|
||||
|
||||
# Настройка логирования для модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class LichessClient:
|
||||
"""
|
||||
Клиент для взаимодействия с Lichess API.
|
||||
|
||||
Предоставляет методы для получения различных данных от Lichess:
|
||||
- Активность пользователей
|
||||
- Игры за период
|
||||
- Статистика решения задач
|
||||
|
||||
Все методы асинхронные и используют httpx для HTTP запросов.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Инициализация клиента Lichess API.
|
||||
|
||||
Создает HTTP клиент с таймаутом 30 секунд для всех запросов.
|
||||
"""
|
||||
self.base_url = "https://lichess.org/api" # Базовый URL Lichess API
|
||||
self.client = httpx.AsyncClient(timeout=30.0) # HTTP клиент с таймаутом
|
||||
|
||||
async def get_user_activity(self, username: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
Получает активность пользователя за последние 7 активных дней.
|
||||
|
||||
Args:
|
||||
username: Имя пользователя на Lichess
|
||||
|
||||
Returns:
|
||||
Список активностей пользователя или None, если пользователь не найден
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: При ошибках HTTP (кроме 404)
|
||||
Exception: При других ошибках
|
||||
"""
|
||||
try:
|
||||
# Формируем URL для получения активности пользователя
|
||||
url = f"{self.base_url}/user/{username}/activity"
|
||||
logger.info(f"Запрос активности пользователя {username}")
|
||||
|
||||
# Выполняем HTTP GET запрос
|
||||
response = await self.client.get(url)
|
||||
response.raise_for_status() # Проверяем статус ответа
|
||||
|
||||
# Возвращаем JSON данные
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
# Пользователь не найден - это нормальная ситуация
|
||||
logger.warning(f"Пользователь {username} не найден")
|
||||
return None
|
||||
else:
|
||||
# Другие HTTP ошибки - логируем и пробрасываем
|
||||
logger.error(f"HTTP ошибка при получении активности пользователя {username}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
# Обрабатываем все остальные ошибки
|
||||
logger.error(f"Ошибка при получении активности пользователя {username}: {e}")
|
||||
raise
|
||||
|
||||
async def get_games_of_period(self, username: str, since_ms: int, until_ms: int, rated_only: bool = True) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
Получает игры пользователя за определенный период.
|
||||
|
||||
Lichess API возвращает игры в формате NDJSON (Newline Delimited JSON),
|
||||
где каждая строка содержит JSON объект с информацией об игре.
|
||||
|
||||
Args:
|
||||
username: Имя пользователя на Lichess
|
||||
since_ms: Начало периода в миллисекундах (Unix timestamp * 1000)
|
||||
until_ms: Конец периода в миллисекундах (Unix timestamp * 1000)
|
||||
rated_only: Только рейтинговые игры (по умолчанию True)
|
||||
|
||||
Returns:
|
||||
Список игр в формате JSON или None при ошибке
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: При ошибках HTTP
|
||||
Exception: При других ошибках
|
||||
"""
|
||||
try:
|
||||
# Формируем URL для получения игр пользователя
|
||||
url = f"{self.base_url}/games/user/{username}"
|
||||
|
||||
# Параметры запроса
|
||||
params = {
|
||||
'since': since_ms, # Начало периода
|
||||
'until': until_ms, # Конец периода
|
||||
'max': 1000 # Максимум игр за запрос (лимит Lichess API)
|
||||
}
|
||||
|
||||
# Добавляем фильтр по рейтинговым играм, если нужно
|
||||
if rated_only:
|
||||
params['rated'] = 'true'
|
||||
|
||||
# Заголовки для получения NDJSON формата
|
||||
headers = {
|
||||
'Accept': 'application/x-ndjson' # Запрашиваем NDJSON формат
|
||||
}
|
||||
|
||||
logger.info(f"Запрос игр для {username} с {since_ms} по {until_ms}")
|
||||
|
||||
# Выполняем HTTP GET запрос
|
||||
response = await self.client.get(url, params=params, headers=headers)
|
||||
response.raise_for_status() # Проверяем статус ответа
|
||||
|
||||
# Парсим NDJSON (Newline Delimited JSON)
|
||||
# Каждая строка содержит отдельный JSON объект
|
||||
games = []
|
||||
content = response.text.strip()
|
||||
|
||||
if content:
|
||||
for line in content.split('\n'):
|
||||
if line.strip():
|
||||
try:
|
||||
# Парсим каждую строку как отдельный JSON объект
|
||||
game = json.loads(line)
|
||||
games.append(game)
|
||||
except json.JSONDecodeError as e:
|
||||
# Логируем ошибки парсинга, но продолжаем обработку
|
||||
logger.warning(f"Ошибка парсинга JSON строки: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"Получено {len(games)} игр для пользователя {username}")
|
||||
return games
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
# Пользователь не найден - это нормальная ситуация
|
||||
logger.warning(f"Пользователь {username} не найден")
|
||||
return None
|
||||
else:
|
||||
# Другие HTTP ошибки - логируем и пробрасываем
|
||||
logger.error(f"HTTP ошибка при получении игр пользователя {username}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
# Обрабатываем все остальные ошибки
|
||||
logger.error(f"Ошибка при получении игр пользователя {username}: {e}")
|
||||
raise
|
||||
|
||||
async def get_puzzle_activity(self, token: str, max_puzzles: int = 50) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
Получает активность пользователя по решению задач (пазлов).
|
||||
|
||||
Требует авторизации через Bearer токен. Lichess API возвращает данные
|
||||
в формате NDJSON (Newline Delimited JSON).
|
||||
|
||||
Args:
|
||||
token: Bearer токен авторизации от Lichess
|
||||
max_puzzles: Максимальное количество задач для получения (по умолчанию 50)
|
||||
|
||||
Returns:
|
||||
Список активностей по задачам в формате JSON или None при ошибке
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: При ошибках HTTP
|
||||
Exception: При других ошибках
|
||||
"""
|
||||
try:
|
||||
# Формируем URL для получения активности по задачам
|
||||
url = f"{self.base_url}/puzzle/activity"
|
||||
|
||||
# Параметры запроса
|
||||
params = {
|
||||
'max': max_puzzles # Максимальное количество задач
|
||||
}
|
||||
|
||||
# Заголовки с авторизацией и форматом данных
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}', # Bearer токен авторизации
|
||||
'Accept': 'application/x-ndjson' # Запрашиваем NDJSON формат
|
||||
}
|
||||
|
||||
logger.info(f"Запрос активности по задачам, max={max_puzzles}")
|
||||
|
||||
# Выполняем HTTP GET запрос
|
||||
response = await self.client.get(url, params=params, headers=headers)
|
||||
response.raise_for_status() # Проверяем статус ответа
|
||||
|
||||
# Парсим NDJSON (Newline Delimited JSON)
|
||||
# Каждая строка содержит отдельный JSON объект с активностью
|
||||
activities = []
|
||||
content = response.text.strip()
|
||||
|
||||
if content:
|
||||
for line in content.split('\n'):
|
||||
if line.strip():
|
||||
try:
|
||||
# Парсим каждую строку как отдельный JSON объект
|
||||
activity = json.loads(line)
|
||||
activities.append(activity)
|
||||
except json.JSONDecodeError as e:
|
||||
# Логируем ошибки парсинга, но продолжаем обработку
|
||||
logger.warning(f"Ошибка парсинга JSON строки: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"Получено {len(activities)} активностей по задачам")
|
||||
return activities
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
# Неверный токен авторизации
|
||||
logger.warning("Неверный токен авторизации")
|
||||
return None
|
||||
elif e.response.status_code == 403:
|
||||
# Доступ запрещен (недостаточно прав)
|
||||
logger.warning("Доступ запрещен")
|
||||
return None
|
||||
else:
|
||||
# Другие HTTP ошибки - логируем и пробрасываем
|
||||
logger.error(f"HTTP ошибка при получении активности по задачам: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
# Обрабатываем все остальные ошибки
|
||||
logger.error(f"Ошибка при получении активности по задачам: {e}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Закрывает HTTP клиент.
|
||||
|
||||
Освобождает ресурсы и корректно закрывает соединения.
|
||||
Должен вызываться при завершении работы с клиентом.
|
||||
"""
|
||||
await self.client.aclose()
|
||||
705
LichessWebServices/main.py
Normal file
705
LichessWebServices/main.py
Normal file
|
|
@ -0,0 +1,705 @@
|
|||
"""
|
||||
Lichess Statistics API - Основной модуль FastAPI приложения
|
||||
|
||||
Этот модуль содержит все API эндпоинты для получения статистики игроков Lichess.
|
||||
Включает в себя:
|
||||
- Статистику игр за разные периоды (сегодня, вчера, неделя)
|
||||
- Детальную статистику игр за произвольный период
|
||||
- Статистику решения задач (пазлов) за период
|
||||
- Health check и информационные эндпоинты
|
||||
|
||||
Автор: Lichess Web Services Team
|
||||
Версия: 1.0.0
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Path, Query, Header
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from stats_service import StatsService
|
||||
from models import ActivityResponse, ErrorResponse, HealthResponse, GamesOfPeriodResponse, PuzzleOfPeriodResponse
|
||||
|
||||
# =============================================================================
|
||||
# НАСТРОЙКА ЛОГИРОВАНИЯ
|
||||
# =============================================================================
|
||||
# Настройка базового логирования для всего приложения
|
||||
# Уровень INFO позволяет видеть все важные события и ошибки
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =============================================================================
|
||||
# ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
|
||||
# =============================================================================
|
||||
# Глобальный экземпляр сервиса статистики
|
||||
# Инициализируется при запуске приложения и используется во всех эндпоинтах
|
||||
stats_service = None
|
||||
|
||||
# =============================================================================
|
||||
# УПРАВЛЕНИЕ ЖИЗНЕННЫМ ЦИКЛОМ ПРИЛОЖЕНИЯ
|
||||
# =============================================================================
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""
|
||||
Контекстный менеджер для управления жизненным циклом FastAPI приложения.
|
||||
|
||||
Выполняется при запуске и остановке приложения:
|
||||
- При запуске: инициализирует сервис статистики
|
||||
- При остановке: корректно закрывает все соединения
|
||||
|
||||
Args:
|
||||
app: Экземпляр FastAPI приложения
|
||||
"""
|
||||
# ========== STARTUP (Запуск приложения) ==========
|
||||
global stats_service
|
||||
# Создаем экземпляр сервиса статистики, который будет использоваться во всех эндпоинтах
|
||||
stats_service = StatsService()
|
||||
logger.info("Lichess API сервис запущен")
|
||||
|
||||
# Передаем управление приложению
|
||||
yield
|
||||
|
||||
# ========== SHUTDOWN (Остановка приложения) ==========
|
||||
# Корректно закрываем все соединения и освобождаем ресурсы
|
||||
if stats_service:
|
||||
await stats_service.close()
|
||||
logger.info("Lichess API сервис остановлен")
|
||||
|
||||
# =============================================================================
|
||||
# СОЗДАНИЕ FASTAPI ПРИЛОЖЕНИЯ
|
||||
# =============================================================================
|
||||
app = FastAPI(
|
||||
title="Lichess Statistics API",
|
||||
description="""
|
||||
## Lichess Statistics API
|
||||
|
||||
REST API для получения детальной статистики игроков платформы Lichess.
|
||||
|
||||
### Возможности:
|
||||
* 📊 Получение статистики игр по режимам (Bullet, Blitz, Rapid)
|
||||
* 🧩 Статистика решения задач (пазлов)
|
||||
* 📅 Статистика за разные периоды (сегодня, вчера, неделя)
|
||||
* 🎯 Отслеживание изменений рейтинга
|
||||
* 📈 Подробная аналитика результатов игр
|
||||
|
||||
### Режимы игр:
|
||||
- **Bullet**: Быстрые игры (1-3 минуты)
|
||||
- **Blitz**: Блиц игры (3-10 минут)
|
||||
- **Rapid**: Рапид игры (10+ минут)
|
||||
|
||||
### Примеры использования:
|
||||
- Получить статистику за сегодня: `GET /stats/{username}/today`
|
||||
- Получить статистику за вчера: `GET /stats/{username}/yesterday`
|
||||
- Получить статистику за неделю: `GET /stats/{username}/week`
|
||||
""",
|
||||
version="1.0.0",
|
||||
contact={
|
||||
"name": "Lichess Statistics API Support",
|
||||
"url": "https://github.com/vrubelroman/LichessWebServices",
|
||||
},
|
||||
license_info={
|
||||
"name": "MIT",
|
||||
},
|
||||
lifespan=lifespan,
|
||||
openapi_tags=[
|
||||
{
|
||||
"name": "health",
|
||||
"description": "Проверка состояния сервиса"
|
||||
},
|
||||
{
|
||||
"name": "statistics",
|
||||
"description": "Получение статистики игроков Lichess"
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"description": "Информация об API"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# НАСТРОЙКА CORS (Cross-Origin Resource Sharing)
|
||||
# =============================================================================
|
||||
# CORS middleware позволяет веб-приложениям делать запросы к API с других доменов
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # В продакшене следует ограничить домены для безопасности
|
||||
allow_credentials=True, # Разрешаем отправку cookies и авторизационных заголовков
|
||||
allow_methods=["*"], # Разрешаем все HTTP методы (GET, POST, PUT, DELETE и т.д.)
|
||||
allow_headers=["*"], # Разрешаем все заголовки
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# API ЭНДПОИНТЫ
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/", tags=["info"])
|
||||
async def root():
|
||||
"""
|
||||
## Корневой endpoint
|
||||
|
||||
Возвращает основную информацию об API и доступных эндпоинтах.
|
||||
Используется для получения базовой информации о сервисе.
|
||||
|
||||
### Возвращает:
|
||||
- Название API
|
||||
- Версию
|
||||
- Список доступных эндпоинтов
|
||||
- Ссылки на документацию
|
||||
"""
|
||||
return {
|
||||
"message": "Lichess Statistics API",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API для получения статистики игроков Lichess",
|
||||
"endpoints": {
|
||||
"today": "/stats/{username}/today", # Статистика за сегодня
|
||||
"yesterday": "/stats/{username}/yesterday", # Статистика за вчера
|
||||
"week": "/stats/{username}/week", # Статистика за неделю
|
||||
"games_period": "/games/{username}/period?since={timestamp}&until={timestamp}", # Статистика игр за период
|
||||
"puzzle_period": "/puzzle/period?since={timestamp}&until={timestamp}&max={max}" # Статистика задач за период
|
||||
},
|
||||
"documentation": "/docs", # Swagger UI документация
|
||||
"openapi_schema": "/openapi.json" # OpenAPI схема
|
||||
}
|
||||
|
||||
@app.get("/health",
|
||||
response_model=HealthResponse,
|
||||
tags=["health"],
|
||||
summary="Health Check",
|
||||
description="Проверка состояния сервиса")
|
||||
async def health_check():
|
||||
"""
|
||||
## Проверка здоровья сервиса
|
||||
|
||||
Простой endpoint для проверки работоспособности API.
|
||||
Используется для мониторинга и health checks в production среде.
|
||||
|
||||
### Возвращает:
|
||||
- Статус сервиса (healthy/unhealthy)
|
||||
- Время проверки в ISO формате
|
||||
- Название сервиса
|
||||
|
||||
### Использование:
|
||||
- Мониторинг системы
|
||||
- Load balancer health checks
|
||||
- Kubernetes liveness/readiness probes
|
||||
"""
|
||||
return HealthResponse(
|
||||
status="healthy", # Всегда возвращает healthy, если сервис запущен
|
||||
timestamp=datetime.now().isoformat(), # Текущее время в ISO формате
|
||||
service="Lichess Statistics API"
|
||||
)
|
||||
|
||||
@app.get("/stats/{username}/today",
|
||||
response_model=ActivityResponse,
|
||||
tags=["statistics"],
|
||||
summary="Статистика за сегодня",
|
||||
description="Получает детальную статистику игрока за сегодняшний день",
|
||||
responses={
|
||||
200: {
|
||||
"description": "Статистика успешно получена",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "Статистика за сегодняшний день",
|
||||
"data": {
|
||||
"username": "magnus",
|
||||
"tasks": {
|
||||
"total": 15,
|
||||
"solved": 12,
|
||||
"unsolved": 3
|
||||
},
|
||||
"games": {
|
||||
"bullet": {
|
||||
"games_played": 8,
|
||||
"rating_change": 15,
|
||||
"final_rating": 2850,
|
||||
"wins": 5,
|
||||
"losses": 2,
|
||||
"draws": 1
|
||||
},
|
||||
"blitz": {
|
||||
"games_played": 3,
|
||||
"rating_change": -5,
|
||||
"final_rating": 2750,
|
||||
"wins": 1,
|
||||
"losses": 2,
|
||||
"draws": 0
|
||||
},
|
||||
"rapid": {
|
||||
"games_played": 0,
|
||||
"rating_change": 0,
|
||||
"final_rating": 0,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
404: {
|
||||
"description": "Пользователь не найден",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "Пользователь magnus не найден или неактивен"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
500: {
|
||||
"description": "Внутренняя ошибка сервера",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"detail": "Внутренняя ошибка сервера: Connection timeout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
async def get_today_stats(
|
||||
username: str = Path(...,
|
||||
description="Имя пользователя на Lichess",
|
||||
example="magnus",
|
||||
min_length=1,
|
||||
max_length=50)
|
||||
):
|
||||
"""
|
||||
## Статистика за сегодняшний день
|
||||
|
||||
Получает детальную статистику игрока за сегодняшний день, включая:
|
||||
|
||||
### Статистика игр:
|
||||
- **Bullet**: Быстрые игры (1-3 минуты)
|
||||
- **Blitz**: Блиц игры (3-10 минут)
|
||||
- **Rapid**: Рапид игры (10+ минут)
|
||||
|
||||
### Для каждого режима:
|
||||
- Количество сыгранных игр
|
||||
- Изменение рейтинга
|
||||
- Текущий рейтинг
|
||||
- Количество побед, поражений, ничьих
|
||||
|
||||
### Статистика задач:
|
||||
- Общее количество решенных задач
|
||||
- Количество решенных задач
|
||||
- Количество нерешенных задач
|
||||
|
||||
### Параметры:
|
||||
- **username**: Имя пользователя на Lichess (обязательно)
|
||||
|
||||
### Возможные ошибки:
|
||||
- **404**: Пользователь не найден или неактивен
|
||||
- **500**: Внутренняя ошибка сервера
|
||||
"""
|
||||
# Проверяем, что сервис статистики инициализирован
|
||||
if not stats_service:
|
||||
raise HTTPException(status_code=500, detail="Сервис не инициализирован")
|
||||
|
||||
try:
|
||||
# Получаем статистику за сегодняшний день через сервис
|
||||
result = await stats_service.get_today_stats(username)
|
||||
return result
|
||||
except Exception as e:
|
||||
# Логируем ошибку и возвращаем HTTP 500
|
||||
logger.error(f"Ошибка в endpoint get_today_stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
@app.get("/stats/{username}/yesterday",
|
||||
response_model=ActivityResponse,
|
||||
tags=["statistics"],
|
||||
summary="Статистика за вчера",
|
||||
description="Получает детальную статистику игрока за вчерашний день")
|
||||
async def get_yesterday_stats(
|
||||
username: str = Path(...,
|
||||
description="Имя пользователя на Lichess",
|
||||
example="magnus",
|
||||
min_length=1,
|
||||
max_length=50)
|
||||
):
|
||||
"""
|
||||
## Статистика за вчерашний день
|
||||
|
||||
Получает детальную статистику игрока за вчерашний день.
|
||||
|
||||
### Возвращает:
|
||||
- Статистику игр по всем режимам (Bullet, Blitz, Rapid)
|
||||
- Статистику решения задач (пазлов)
|
||||
- Изменения рейтинга
|
||||
- Результаты игр (победы, поражения, ничьи)
|
||||
|
||||
### Параметры:
|
||||
- **username**: Имя пользователя на Lichess (обязательно)
|
||||
"""
|
||||
if not stats_service:
|
||||
raise HTTPException(status_code=500, detail="Сервис не инициализирован")
|
||||
|
||||
try:
|
||||
result = await stats_service.get_yesterday_stats(username)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в endpoint get_yesterday_stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
@app.get("/stats/{username}/week",
|
||||
response_model=ActivityResponse,
|
||||
tags=["statistics"],
|
||||
summary="Статистика за неделю",
|
||||
description="Получает агрегированную статистику игрока за последние 7 дней")
|
||||
async def get_week_stats(
|
||||
username: str = Path(...,
|
||||
description="Имя пользователя на Lichess",
|
||||
example="magnus",
|
||||
min_length=1,
|
||||
max_length=50)
|
||||
):
|
||||
"""
|
||||
## Статистика за последние 7 дней
|
||||
|
||||
Получает агрегированную статистику игрока за последние 7 дней.
|
||||
|
||||
### Особенности:
|
||||
- Суммирует все игры и задачи за неделю
|
||||
- Показывает общее изменение рейтинга
|
||||
- Отображает финальный рейтинг на конец периода
|
||||
|
||||
### Возвращает:
|
||||
- Общую статистику игр по всем режимам
|
||||
- Суммарную статистику решения задач
|
||||
- Агрегированные изменения рейтинга
|
||||
|
||||
### Параметры:
|
||||
- **username**: Имя пользователя на Lichess (обязательно)
|
||||
"""
|
||||
if not stats_service:
|
||||
raise HTTPException(status_code=500, detail="Сервис не инициализирован")
|
||||
|
||||
try:
|
||||
result = await stats_service.get_week_stats(username)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в endpoint get_week_stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
@app.get("/games/{username}/period",
|
||||
response_model=GamesOfPeriodResponse,
|
||||
tags=["statistics"],
|
||||
summary="Статистика игр за период",
|
||||
description="Получает детальную статистику игр пользователя за указанный период",
|
||||
responses={
|
||||
200: {
|
||||
"description": "Статистика игр успешно получена",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "Статистика игр за период",
|
||||
"username": "magnus",
|
||||
"period_start": 1640995200000,
|
||||
"period_end": 1641081600000,
|
||||
"games_count": 25,
|
||||
"data": {
|
||||
"bullet": {
|
||||
"games_played": 10,
|
||||
"wins": 6,
|
||||
"losses": 3,
|
||||
"draws": 1,
|
||||
"rating_change": 15,
|
||||
"rating": 2850
|
||||
},
|
||||
"blitz": {
|
||||
"games_played": 8,
|
||||
"wins": 5,
|
||||
"losses": 2,
|
||||
"draws": 1,
|
||||
"rating_change": 12,
|
||||
"rating": 2750
|
||||
},
|
||||
"rapid": {
|
||||
"games_played": 5,
|
||||
"wins": 3,
|
||||
"losses": 1,
|
||||
"draws": 1,
|
||||
"rating_change": 8,
|
||||
"rating": 2600
|
||||
},
|
||||
"classical": {
|
||||
"games_played": 2,
|
||||
"wins": 1,
|
||||
"losses": 1,
|
||||
"draws": 0,
|
||||
"rating_change": 0,
|
||||
"rating": 2400
|
||||
},
|
||||
"correspondence": {
|
||||
"games_played": 0,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 0,
|
||||
"rating_change": 0,
|
||||
"rating": None
|
||||
},
|
||||
"total": {
|
||||
"games_played": 25,
|
||||
"wins": 15,
|
||||
"losses": 7,
|
||||
"draws": 3,
|
||||
"rating_change": 35,
|
||||
"rating": 2850
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
400: {
|
||||
"description": "Некорректные параметры запроса",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"detail": "Параметр 'since' должен быть меньше 'until'"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
404: {
|
||||
"description": "Пользователь не найден",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "Пользователь magnus не найден"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
500: {
|
||||
"description": "Внутренняя ошибка сервера",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"detail": "Внутренняя ошибка сервера: Connection timeout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
async def get_games_of_period(
|
||||
username: str = Path(...,
|
||||
description="Имя пользователя на Lichess",
|
||||
example="magnus",
|
||||
min_length=1,
|
||||
max_length=50),
|
||||
since: int = Query(...,
|
||||
description="Начало периода (Unix timestamp в миллисекундах)",
|
||||
example=1640995200000),
|
||||
until: int = Query(...,
|
||||
description="Конец периода (Unix timestamp в миллисекундах)",
|
||||
example=1641081600000),
|
||||
rated_only: bool = Query(True,
|
||||
description="Только рейтинговые игры (по умолчанию true - рекомендуется)",
|
||||
example=True)
|
||||
):
|
||||
"""
|
||||
## Статистика игр за период
|
||||
|
||||
Получает детальную статистику игр пользователя за указанный период времени.
|
||||
Этот эндпоинт позволяет получить подробную аналитику по играм за любой период.
|
||||
|
||||
### Возможности:
|
||||
- **Фильтрация по времени**: точный период с Unix timestamp
|
||||
- **Типы игр**: Bullet, Blitz, Rapid, Classical, Correspondence
|
||||
- **Статистика результатов**: победы, поражения, ничьи
|
||||
- **Рейтинговые изменения**: суммарные изменения рейтинга
|
||||
- **Итоговый рейтинг**: рейтинг после последней игры в каждом режиме
|
||||
- **Фильтр рейтинговых игр**: только рейтинговые или все игры
|
||||
|
||||
### Параметры:
|
||||
- **username**: Имя пользователя на Lichess (обязательно)
|
||||
- **since**: Начало периода в Unix timestamp (миллисекунды) (обязательно)
|
||||
- **until**: Конец периода в Unix timestamp (миллисекунды) (обязательно)
|
||||
- **rated_only**: Только рейтинговые игры (по умолчанию true - рекомендуется)
|
||||
|
||||
### Примеры использования:
|
||||
- Статистика за последние 7 дней: `since=1640995200000&until=1641081600000` (по умолчанию только рейтинговые)
|
||||
- Только рейтинговые игры: `rated_only=true` (рекомендуется)
|
||||
- Все игры: `rated_only=false`
|
||||
|
||||
### Возможные ошибки:
|
||||
- **400**: Некорректные параметры (since >= until)
|
||||
- **404**: Пользователь не найден
|
||||
- **500**: Внутренняя ошибка сервера
|
||||
"""
|
||||
# Проверяем, что сервис статистики инициализирован
|
||||
if not stats_service:
|
||||
raise HTTPException(status_code=500, detail="Сервис не инициализирован")
|
||||
|
||||
# Валидация параметров времени
|
||||
if since >= until:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Параметр 'since' должен быть меньше 'until'"
|
||||
)
|
||||
|
||||
# Проверяем разумность периода (не более 1 года в миллисекундах)
|
||||
if until - since > 365 * 24 * 3600 * 1000:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Период не может превышать 1 год"
|
||||
)
|
||||
|
||||
try:
|
||||
# Конвертируем миллисекунды в секунды для внутренней логики
|
||||
since_seconds = since // 1000
|
||||
until_seconds = until // 1000
|
||||
result = await stats_service.get_games_of_period(username, since_seconds, until_seconds, rated_only)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в endpoint get_games_of_period: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
@app.get("/puzzle/period",
|
||||
response_model=PuzzleOfPeriodResponse,
|
||||
tags=["statistics"],
|
||||
summary="Статистика решения задач за период",
|
||||
description="Получает статистику решения задач (пазлов) за указанный период времени. Требует авторизации через Bearer токен.",
|
||||
responses={
|
||||
200: {
|
||||
"description": "Статистика решения задач успешно получена",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "Статистика решения задач за период",
|
||||
"period_start": 1640995200000,
|
||||
"period_end": 1641081600000,
|
||||
"max_puzzles": 50,
|
||||
"puzzles_in_period": 15,
|
||||
"data": {
|
||||
"total_attempts": 15,
|
||||
"solved": 12,
|
||||
"failed": 3,
|
||||
"success_rate": 80.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
400: {
|
||||
"description": "Некорректные параметры запроса",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"detail": "Параметр 'since' должен быть меньше 'until'"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
401: {
|
||||
"description": "Неверный токен авторизации",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "Неверный токен авторизации или доступ запрещен"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
500: {
|
||||
"description": "Внутренняя ошибка сервера",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"detail": "Внутренняя ошибка сервера: Connection timeout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
async def get_puzzle_of_period(
|
||||
since: int = Query(...,
|
||||
description="Начало периода (Unix timestamp в миллисекундах)",
|
||||
example=1640995200000),
|
||||
until: int = Query(...,
|
||||
description="Конец периода (Unix timestamp в миллисекундах)",
|
||||
example=1641081600000),
|
||||
max: int = Query(50,
|
||||
description="Максимальное количество задач для получения от Lichess API. Внимание: если в периоде было больше задач, чем указано в max, будут показаны только последние N активностей",
|
||||
example=50,
|
||||
ge=1,
|
||||
le=1000),
|
||||
authorization: str = Header(...,
|
||||
description="Bearer токен авторизации",
|
||||
example="Bearer your_token_here")
|
||||
):
|
||||
"""
|
||||
## Статистика решения задач за период
|
||||
|
||||
Получает статистику решения задач (пазлов) за указанный период времени.
|
||||
Требует авторизации через Bearer токен от Lichess.
|
||||
|
||||
### Возможности:
|
||||
- **Фильтрация по времени**: точный период с Unix timestamp в миллисекундах
|
||||
- **Статистика решений**: количество решенных и нерешенных задач
|
||||
- **Процент успеха**: автоматический расчет процента успешных решений
|
||||
- **Настраиваемый лимит**: максимальное количество задач для анализа
|
||||
|
||||
### Параметры:
|
||||
- **since**: Начало периода в Unix timestamp (миллисекунды) (обязательно)
|
||||
- **until**: Конец периода в Unix timestamp (миллисекунды) (обязательно)
|
||||
- **max**: Максимальное количество задач (по умолчанию 50, максимум 1000)
|
||||
- **Authorization**: Bearer токен в заголовке (обязательно)
|
||||
|
||||
### Примеры использования:
|
||||
- Статистика за последние 7 дней: `since=1640995200000&until=1641081600000`
|
||||
- Больше задач: `max=100`
|
||||
- Заголовок: `Authorization: Bearer your_token_here`
|
||||
|
||||
### Получение токена:
|
||||
1. Зайдите на https://lichess.org/account/oauth/token/create
|
||||
2. Создайте новый токен с правами на чтение активности
|
||||
3. Используйте токен в заголовке Authorization
|
||||
|
||||
### Возможные ошибки:
|
||||
- **400**: Некорректные параметры (since >= until, неверный max)
|
||||
- **401**: Неверный токен авторизации
|
||||
- **500**: Внутренняя ошибка сервера
|
||||
"""
|
||||
# Проверяем, что сервис статистики инициализирован
|
||||
if not stats_service:
|
||||
raise HTTPException(status_code=500, detail="Сервис не инициализирован")
|
||||
|
||||
# Валидация параметров
|
||||
if since >= until:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Параметр 'since' должен быть меньше 'until'"
|
||||
)
|
||||
|
||||
# Проверяем разумность периода (не более 1 года в миллисекундах)
|
||||
if until - since > 365 * 24 * 3600 * 1000:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Период не может превышать 1 год"
|
||||
)
|
||||
|
||||
# Извлекаем токен из заголовка Authorization
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Неверный формат токена. Используйте 'Bearer your_token'"
|
||||
)
|
||||
|
||||
token = authorization[7:] # Убираем "Bearer " из начала
|
||||
|
||||
try:
|
||||
result = await stats_service.get_puzzle_of_period(token, since, until, max)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в endpoint get_puzzle_of_period: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
236
LichessWebServices/models.py
Normal file
236
LichessWebServices/models.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"""
|
||||
Lichess Statistics API - Модели данных
|
||||
|
||||
Этот модуль содержит все Pydantic модели для валидации и сериализации данных.
|
||||
Модели используются для:
|
||||
- Валидации входных параметров API
|
||||
- Сериализации ответов API
|
||||
- Документации в Swagger UI
|
||||
- Типизации данных в коде
|
||||
|
||||
Автор: Lichess Web Services Team
|
||||
Версия: 1.0.0
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, 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+ минут)
|
||||
"""
|
||||
bullet: GameModeStats = Field(..., description="Статистика Bullet игр (1-3 минуты)")
|
||||
blitz: GameModeStats = Field(..., description="Статистика Blitz игр (3-10 минут)")
|
||||
rapid: GameModeStats = Field(..., description="Статистика Rapid игр (10+ минут)")
|
||||
|
||||
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="Изменение рейтинга")
|
||||
|
||||
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 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)
|
||||
|
||||
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="Статистика игр за период")
|
||||
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)
|
||||
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="Статистика решения задач за период")
|
||||
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="Данные статистики решения задач")
|
||||
20
LichessWebServices/requirements.txt
Normal file
20
LichessWebServices/requirements.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Lichess Statistics API - Зависимости Python
|
||||
#
|
||||
# Этот файл содержит все необходимые Python пакеты для работы API.
|
||||
# Установка: pip install -r requirements.txt
|
||||
|
||||
# FastAPI - современный веб-фреймворк для создания API
|
||||
fastapi==0.104.1
|
||||
|
||||
# Uvicorn - ASGI сервер для запуска FastAPI приложения
|
||||
# [standard] включает дополнительные зависимости для production
|
||||
uvicorn[standard]==0.24.0
|
||||
|
||||
# HTTPX - асинхронный HTTP клиент для запросов к Lichess API
|
||||
httpx==0.25.2
|
||||
|
||||
# Pydantic - библиотека для валидации данных и сериализации
|
||||
pydantic==2.5.0
|
||||
|
||||
# Python-multipart - поддержка multipart/form-data для загрузки файлов
|
||||
python-multipart==0.0.6
|
||||
733
LichessWebServices/stats_service.py
Normal file
733
LichessWebServices/stats_service.py
Normal file
|
|
@ -0,0 +1,733 @@
|
|||
"""
|
||||
Lichess Statistics API - Сервис обработки статистики
|
||||
|
||||
Этот модуль содержит класс StatsService для обработки и агрегации данных
|
||||
от Lichess API. Включает в себя:
|
||||
- Парсинг и обработку активности пользователей
|
||||
- Агрегацию статистики игр по режимам
|
||||
- Обработку статистики решения задач (пазлов)
|
||||
- Фильтрацию данных по временным периодам
|
||||
- Расчет рейтинговых изменений
|
||||
|
||||
Автор: Lichess Web Services Team
|
||||
Версия: 1.0.0
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timedelta, date
|
||||
from lichess_client import LichessClient
|
||||
from models import UserStats, TaskStats, GameModeStats, GamesStats, ActivityResponse, GameStats, GamesOfPeriodStats, GamesOfPeriodResponse, PuzzleStats, PuzzleOfPeriodResponse
|
||||
import logging
|
||||
|
||||
# Настройка логирования для модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StatsService:
|
||||
"""
|
||||
Сервис для обработки и агрегации статистики Lichess.
|
||||
|
||||
Предоставляет методы для:
|
||||
- Получения статистики за разные периоды (сегодня, вчера, неделя)
|
||||
- Обработки игр за произвольный период
|
||||
- Анализа активности по решению задач
|
||||
- Агрегации данных по режимам игр
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Инициализация сервиса статистики.
|
||||
|
||||
Создает экземпляр LichessClient для взаимодействия с API.
|
||||
"""
|
||||
self.lichess_client = LichessClient()
|
||||
|
||||
# =============================================================================
|
||||
# ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ДЛЯ ОБРАБОТКИ ДАННЫХ
|
||||
# =============================================================================
|
||||
|
||||
def _parse_lichess_interval(self, interval: Dict[str, int]) -> date:
|
||||
"""
|
||||
Парсит дату из временного интервала Lichess.
|
||||
|
||||
Lichess API возвращает временные интервалы в миллисекундах,
|
||||
этот метод конвертирует их в объект date.
|
||||
|
||||
Args:
|
||||
interval: Словарь с ключом 'start' содержащим timestamp в миллисекундах
|
||||
|
||||
Returns:
|
||||
Объект date с датой активности
|
||||
"""
|
||||
# Lichess использует миллисекунды, конвертируем в секунды
|
||||
timestamp = interval['start'] / 1000
|
||||
return datetime.fromtimestamp(timestamp).date()
|
||||
|
||||
def _is_date_in_range(self, target_date: date, activity_date: date, days_back: int) -> bool:
|
||||
"""
|
||||
Проверяет, попадает ли дата активности в нужный диапазон.
|
||||
|
||||
Args:
|
||||
target_date: Целевая дата (обычно сегодня)
|
||||
activity_date: Дата активности из Lichess
|
||||
days_back: Количество дней назад для проверки
|
||||
|
||||
Returns:
|
||||
True, если дата активности попадает в диапазон
|
||||
"""
|
||||
today = date.today()
|
||||
start_date = today - timedelta(days=days_back-1)
|
||||
return start_date <= activity_date <= today
|
||||
|
||||
def _calculate_rating_change(self, mode_data: Dict[str, Any]) -> int:
|
||||
"""
|
||||
Вычисляет изменение рейтинга для режима игры.
|
||||
|
||||
Args:
|
||||
mode_data: Данные режима игры из Lichess API
|
||||
|
||||
Returns:
|
||||
Изменение рейтинга (может быть отрицательным)
|
||||
"""
|
||||
rp = mode_data.get('rp', {})
|
||||
before = rp.get('before', 0) # Рейтинг до периода
|
||||
after = rp.get('after', 0) # Рейтинг после периода
|
||||
return after - before
|
||||
|
||||
def _get_final_rating(self, mode_data: Dict[str, Any]) -> int:
|
||||
"""
|
||||
Получает финальный рейтинг для режима игры.
|
||||
|
||||
Args:
|
||||
mode_data: Данные режима игры из Lichess API
|
||||
|
||||
Returns:
|
||||
Финальный рейтинг игрока в данном режиме
|
||||
"""
|
||||
rp = mode_data.get('rp', {})
|
||||
return rp.get('after', 0)
|
||||
|
||||
def _count_game_results(self, mode_data: Dict[str, Any]) -> Dict[str, int]:
|
||||
"""
|
||||
Подсчитывает результаты игр для режима (победы, поражения, ничьи).
|
||||
|
||||
Args:
|
||||
mode_data: Данные режима игры из Lichess API
|
||||
|
||||
Returns:
|
||||
Словарь с количеством побед, поражений и ничьих
|
||||
"""
|
||||
wins = mode_data.get('win', 0) # Количество побед
|
||||
losses = mode_data.get('loss', 0) # Количество поражений
|
||||
draws = mode_data.get('draw', 0) # Количество ничьих
|
||||
|
||||
return {"wins": wins, "losses": losses, "draws": draws}
|
||||
|
||||
def _process_games_by_mode(self, games_data: Dict[str, Any]) -> Dict[str, GameModeStats]:
|
||||
"""
|
||||
Обрабатывает игры по режимам (bullet, blitz, rapid).
|
||||
|
||||
Преобразует сырые данные от Lichess API в структурированную статистику
|
||||
по каждому режиму игры.
|
||||
|
||||
Args:
|
||||
games_data: Сырые данные игр от Lichess API
|
||||
|
||||
Returns:
|
||||
Словарь с статистикой по каждому режиму игры
|
||||
"""
|
||||
result = {}
|
||||
|
||||
# Инициализируем все режимы нулевыми значениями
|
||||
# Это гарантирует, что все режимы будут присутствовать в результате
|
||||
for mode in ["bullet", "blitz", "rapid"]:
|
||||
result[mode] = GameModeStats(
|
||||
games_played=0,
|
||||
rating_change=0,
|
||||
final_rating=0,
|
||||
wins=0,
|
||||
losses=0,
|
||||
draws=0
|
||||
)
|
||||
|
||||
# Обрабатываем данные по режимам
|
||||
for mode_name, mode_data in games_data.items():
|
||||
if mode_name in result:
|
||||
# Извлекаем результаты игр
|
||||
wins = mode_data.get('win', 0)
|
||||
losses = mode_data.get('loss', 0)
|
||||
draws = mode_data.get('draw', 0)
|
||||
games_played = wins + losses + draws
|
||||
|
||||
# Для недельной статистики используем предвычисленные значения
|
||||
if 'rating_change' in mode_data:
|
||||
rating_change = mode_data['rating_change']
|
||||
final_rating = mode_data['final_rating']
|
||||
else:
|
||||
# Для дневной статистики вычисляем как обычно
|
||||
rating_change = self._calculate_rating_change(mode_data)
|
||||
final_rating = self._get_final_rating(mode_data)
|
||||
|
||||
# Создаем объект статистики для режима
|
||||
result[mode_name] = GameModeStats(
|
||||
games_played=games_played,
|
||||
rating_change=rating_change,
|
||||
final_rating=final_rating,
|
||||
wins=wins,
|
||||
losses=losses,
|
||||
draws=draws
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _process_tasks(self, puzzles_data: Dict[str, Any]) -> TaskStats:
|
||||
"""Обрабатывает статистику задач (пазлов)"""
|
||||
score = puzzles_data.get('score', {})
|
||||
wins = score.get('win', 0)
|
||||
losses = score.get('loss', 0)
|
||||
draws = score.get('draw', 0)
|
||||
|
||||
total = wins + losses + draws
|
||||
solved = wins
|
||||
unsolved = losses + draws
|
||||
|
||||
return TaskStats(
|
||||
total=total,
|
||||
solved=solved,
|
||||
unsolved=unsolved
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# ПУБЛИЧНЫЕ МЕТОДЫ ДЛЯ ПОЛУЧЕНИЯ СТАТИСТИКИ
|
||||
# =============================================================================
|
||||
|
||||
async def get_today_stats(self, username: str) -> ActivityResponse:
|
||||
"""
|
||||
Получает статистику за сегодняшний день.
|
||||
|
||||
Анализирует активность пользователя и возвращает статистику игр и задач
|
||||
за сегодняшний день.
|
||||
|
||||
Args:
|
||||
username: Имя пользователя на Lichess
|
||||
|
||||
Returns:
|
||||
ActivityResponse с данными статистики или сообщением об ошибке
|
||||
"""
|
||||
try:
|
||||
activity_data = await self.lichess_client.get_user_activity(username)
|
||||
if not activity_data:
|
||||
return ActivityResponse(
|
||||
message=f"Пользователь {username} не найден или неактивен"
|
||||
)
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Ищем активность за сегодня
|
||||
today_activity = None
|
||||
for activity in activity_data:
|
||||
activity_date = self._parse_lichess_interval(activity['interval'])
|
||||
if activity_date == today:
|
||||
today_activity = activity
|
||||
break
|
||||
|
||||
if not today_activity:
|
||||
return ActivityResponse(
|
||||
message=f"Активности за сегодняшний день ({today}) не было"
|
||||
)
|
||||
|
||||
# Обрабатываем данные
|
||||
games_stats = self._process_games_by_mode(today_activity.get('games', {}))
|
||||
tasks_stats = self._process_tasks(today_activity.get('puzzles', {}))
|
||||
|
||||
user_stats = UserStats(
|
||||
username=username,
|
||||
tasks=tasks_stats,
|
||||
games=GamesStats(**games_stats)
|
||||
)
|
||||
|
||||
return ActivityResponse(
|
||||
message="Статистика за сегодняшний день",
|
||||
data=user_stats
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики за сегодня: {e}")
|
||||
return ActivityResponse(
|
||||
message=f"Ошибка при получении статистики: {str(e)}"
|
||||
)
|
||||
|
||||
async def get_yesterday_stats(self, username: str) -> ActivityResponse:
|
||||
"""
|
||||
Получает статистику за вчерашний день.
|
||||
|
||||
Анализирует активность пользователя и возвращает статистику игр и задач
|
||||
за вчерашний день.
|
||||
|
||||
Args:
|
||||
username: Имя пользователя на Lichess
|
||||
|
||||
Returns:
|
||||
ActivityResponse с данными статистики или сообщением об ошибке
|
||||
"""
|
||||
try:
|
||||
activity_data = await self.lichess_client.get_user_activity(username)
|
||||
if not activity_data:
|
||||
return ActivityResponse(
|
||||
message=f"Пользователь {username} не найден или неактивен"
|
||||
)
|
||||
|
||||
yesterday = date.today() - timedelta(days=1)
|
||||
|
||||
# Ищем активность за вчера
|
||||
yesterday_activity = None
|
||||
for activity in activity_data:
|
||||
activity_date = self._parse_lichess_interval(activity['interval'])
|
||||
if activity_date == yesterday:
|
||||
yesterday_activity = activity
|
||||
break
|
||||
|
||||
if not yesterday_activity:
|
||||
return ActivityResponse(
|
||||
message=f"Активности за вчерашний день ({yesterday}) не было"
|
||||
)
|
||||
|
||||
# Обрабатываем данные
|
||||
games_stats = self._process_games_by_mode(yesterday_activity.get('games', {}))
|
||||
tasks_stats = self._process_tasks(yesterday_activity.get('puzzles', {}))
|
||||
|
||||
user_stats = UserStats(
|
||||
username=username,
|
||||
tasks=tasks_stats,
|
||||
games=GamesStats(**games_stats)
|
||||
)
|
||||
|
||||
return ActivityResponse(
|
||||
message="Статистика за вчерашний день",
|
||||
data=user_stats
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики за вчера: {e}")
|
||||
return ActivityResponse(
|
||||
message=f"Ошибка при получении статистики: {str(e)}"
|
||||
)
|
||||
|
||||
async def get_week_stats(self, username: str) -> ActivityResponse:
|
||||
"""
|
||||
Получает статистику за последние 7 дней.
|
||||
|
||||
Анализирует активность пользователя и возвращает агрегированную статистику
|
||||
игр и задач за последние 7 дней.
|
||||
|
||||
Args:
|
||||
username: Имя пользователя на Lichess
|
||||
|
||||
Returns:
|
||||
ActivityResponse с данными статистики или сообщением об ошибке
|
||||
"""
|
||||
try:
|
||||
activity_data = await self.lichess_client.get_user_activity(username)
|
||||
if not activity_data:
|
||||
return ActivityResponse(
|
||||
message=f"Пользователь {username} не найден или неактивен"
|
||||
)
|
||||
|
||||
today = date.today()
|
||||
week_activities = []
|
||||
|
||||
# Фильтруем активности за последние 7 дней
|
||||
for activity in activity_data:
|
||||
activity_date = self._parse_lichess_interval(activity['interval'])
|
||||
if self._is_date_in_range(activity_date, activity_date, 7):
|
||||
week_activities.append(activity)
|
||||
|
||||
if not week_activities:
|
||||
return ActivityResponse(
|
||||
message="Активности за последние 7 дней не было"
|
||||
)
|
||||
|
||||
# Объединяем все игры и задачи за неделю
|
||||
combined_games = {}
|
||||
combined_puzzles = {}
|
||||
|
||||
for activity in week_activities:
|
||||
# Суммируем игры по режимам
|
||||
for mode, mode_data in activity.get('games', {}).items():
|
||||
if mode not in combined_games:
|
||||
combined_games[mode] = {
|
||||
'win': 0, 'loss': 0, 'draw': 0,
|
||||
'rating_change': 0, # Суммируем изменения рейтинга
|
||||
'final_rating': 0 # Берем последний рейтинг
|
||||
}
|
||||
|
||||
combined_games[mode]['win'] += mode_data.get('win', 0)
|
||||
combined_games[mode]['loss'] += mode_data.get('loss', 0)
|
||||
combined_games[mode]['draw'] += mode_data.get('draw', 0)
|
||||
|
||||
# Суммируем изменения рейтинга (delta = after - before)
|
||||
rp = mode_data.get('rp', {})
|
||||
before = rp.get('before', 0)
|
||||
after = rp.get('after', 0)
|
||||
delta = after - before
|
||||
combined_games[mode]['rating_change'] += delta
|
||||
|
||||
# Для финального рейтинга берем последнее значение
|
||||
combined_games[mode]['final_rating'] = after
|
||||
|
||||
# Суммируем задачи
|
||||
puzzles_score = activity.get('puzzles', {}).get('score', {})
|
||||
if not combined_puzzles:
|
||||
combined_puzzles = {'score': {'win': 0, 'loss': 0, 'draw': 0}}
|
||||
|
||||
combined_puzzles['score']['win'] += puzzles_score.get('win', 0)
|
||||
combined_puzzles['score']['loss'] += puzzles_score.get('loss', 0)
|
||||
combined_puzzles['score']['draw'] += puzzles_score.get('draw', 0)
|
||||
|
||||
# Обрабатываем данные
|
||||
games_stats = self._process_games_by_mode(combined_games)
|
||||
tasks_stats = self._process_tasks(combined_puzzles)
|
||||
|
||||
user_stats = UserStats(
|
||||
username=username,
|
||||
tasks=tasks_stats,
|
||||
games=GamesStats(**games_stats)
|
||||
)
|
||||
|
||||
return ActivityResponse(
|
||||
message="Статистика за последние 7 дней",
|
||||
data=user_stats
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики за неделю: {e}")
|
||||
return ActivityResponse(
|
||||
message=f"Ошибка при получении статистики: {str(e)}"
|
||||
)
|
||||
|
||||
def _determine_game_result(self, game: Dict[str, Any], username: str) -> str:
|
||||
"""
|
||||
Определяет результат игры для указанного пользователя
|
||||
|
||||
Returns:
|
||||
'win', 'loss', 'draw' или 'unknown'
|
||||
"""
|
||||
winner = game.get('winner')
|
||||
players = game.get('players', {})
|
||||
|
||||
# Определяем цвет игрока
|
||||
user_color = None
|
||||
if players.get('white', {}).get('user', {}).get('name') == username:
|
||||
user_color = 'white'
|
||||
elif players.get('black', {}).get('user', {}).get('name') == username:
|
||||
user_color = 'black'
|
||||
|
||||
if user_color is None:
|
||||
return 'unknown'
|
||||
|
||||
# Определяем результат
|
||||
if winner is None:
|
||||
return 'draw'
|
||||
elif winner == user_color:
|
||||
return 'win'
|
||||
else:
|
||||
return 'loss'
|
||||
|
||||
def _get_rating_change(self, game: Dict[str, Any], username: str) -> int:
|
||||
"""
|
||||
Получает изменение рейтинга для указанного пользователя
|
||||
"""
|
||||
players = game.get('players', {})
|
||||
|
||||
# Определяем цвет игрока
|
||||
user_color = None
|
||||
if players.get('white', {}).get('user', {}).get('name') == username:
|
||||
user_color = 'white'
|
||||
elif players.get('black', {}).get('user', {}).get('name') == username:
|
||||
user_color = 'black'
|
||||
|
||||
if user_color is None:
|
||||
return 0
|
||||
|
||||
# Получаем изменение рейтинга
|
||||
rating_diff = players.get(user_color, {}).get('ratingDiff')
|
||||
return rating_diff if rating_diff is not None else 0
|
||||
|
||||
def _get_rating_info(self, game: Dict[str, Any], username: str) -> tuple[int, int]:
|
||||
"""
|
||||
Получает изменение рейтинга и итоговый рейтинг для указанного пользователя
|
||||
|
||||
Returns:
|
||||
tuple: (rating_change, final_rating)
|
||||
"""
|
||||
players = game.get('players', {})
|
||||
|
||||
# Определяем цвет игрока (без учета регистра)
|
||||
user_color = None
|
||||
white_user = players.get('white', {}).get('user', {})
|
||||
black_user = players.get('black', {}).get('user', {})
|
||||
|
||||
if white_user.get('name', '').lower() == username.lower():
|
||||
user_color = 'white'
|
||||
elif black_user.get('name', '').lower() == username.lower():
|
||||
user_color = 'black'
|
||||
|
||||
if user_color is None:
|
||||
return 0, 0
|
||||
|
||||
# Получаем рейтинг до партии и изменение рейтинга
|
||||
player_data = players.get(user_color, {})
|
||||
rating_before = player_data.get('rating', 0)
|
||||
rating_diff = player_data.get('ratingDiff', 0)
|
||||
|
||||
# Вычисляем итоговый рейтинг: rating + ratingDiff
|
||||
final_rating = rating_before + rating_diff
|
||||
|
||||
return rating_diff, final_rating
|
||||
|
||||
def _process_games_of_period(self, games: List[Dict[str, Any]], username: str) -> GamesOfPeriodStats:
|
||||
"""
|
||||
Обрабатывает игры за период и возвращает статистику
|
||||
"""
|
||||
# Инициализируем статистику для всех типов игр
|
||||
stats = {
|
||||
'bullet': {'games_played': 0, 'wins': 0, 'losses': 0, 'draws': 0, 'rating_change': 0, 'rating': None},
|
||||
'blitz': {'games_played': 0, 'wins': 0, 'losses': 0, 'draws': 0, 'rating_change': 0, 'rating': None},
|
||||
'rapid': {'games_played': 0, 'wins': 0, 'losses': 0, 'draws': 0, 'rating_change': 0, 'rating': None},
|
||||
'classical': {'games_played': 0, 'wins': 0, 'losses': 0, 'draws': 0, 'rating_change': 0, 'rating': None},
|
||||
'correspondence': {'games_played': 0, 'wins': 0, 'losses': 0, 'draws': 0, 'rating_change': 0, 'rating': None},
|
||||
'total': {'games_played': 0, 'wins': 0, 'losses': 0, 'draws': 0, 'rating_change': 0, 'rating': None}
|
||||
}
|
||||
|
||||
# Сортируем игры по времени создания (от старых к новым) для правильного вычисления итогового рейтинга
|
||||
sorted_games = sorted(games, key=lambda x: x.get('createdAt', 0))
|
||||
|
||||
for game in sorted_games:
|
||||
speed = game.get('speed', 'unknown')
|
||||
|
||||
# Пропускаем неизвестные типы игр
|
||||
if speed not in stats:
|
||||
continue
|
||||
|
||||
# Определяем результат игры
|
||||
result = self._determine_game_result(game, username)
|
||||
if result == 'unknown':
|
||||
continue
|
||||
|
||||
# Получаем изменение рейтинга и итоговый рейтинг
|
||||
rating_change, final_rating = self._get_rating_info(game, username)
|
||||
|
||||
# Обновляем статистику для конкретного типа
|
||||
stats[speed]['games_played'] += 1
|
||||
if result == 'win':
|
||||
stats[speed]['wins'] += 1
|
||||
elif result == 'loss':
|
||||
stats[speed]['losses'] += 1
|
||||
elif result == 'draw':
|
||||
stats[speed]['draws'] += 1
|
||||
stats[speed]['rating_change'] += rating_change
|
||||
# Сохраняем итоговый рейтинг после последней игры
|
||||
if final_rating is not None:
|
||||
stats[speed]['rating'] = final_rating
|
||||
|
||||
# Обновляем общую статистику
|
||||
stats['total']['games_played'] += 1
|
||||
if result == 'win':
|
||||
stats['total']['wins'] += 1
|
||||
elif result == 'loss':
|
||||
stats['total']['losses'] += 1
|
||||
elif result == 'draw':
|
||||
stats['total']['draws'] += 1
|
||||
stats['total']['rating_change'] += rating_change
|
||||
# Для общей статистики берем рейтинг из последней игры (любого типа)
|
||||
if final_rating is not None:
|
||||
stats['total']['rating'] = final_rating
|
||||
|
||||
# Создаем объекты GameStats, устанавливая rating только для режимов с играми
|
||||
def create_game_stats(mode_stats):
|
||||
# Устанавливаем rating только если были игры
|
||||
if mode_stats['games_played'] > 0 and mode_stats['rating'] is not None:
|
||||
return GameStats(**mode_stats)
|
||||
else:
|
||||
# Убираем rating для режимов без игр
|
||||
mode_stats_copy = mode_stats.copy()
|
||||
mode_stats_copy['rating'] = None
|
||||
return GameStats(**mode_stats_copy)
|
||||
|
||||
return GamesOfPeriodStats(
|
||||
bullet=create_game_stats(stats['bullet']),
|
||||
blitz=create_game_stats(stats['blitz']),
|
||||
rapid=create_game_stats(stats['rapid']),
|
||||
classical=create_game_stats(stats['classical']),
|
||||
correspondence=create_game_stats(stats['correspondence']),
|
||||
total=create_game_stats(stats['total'])
|
||||
)
|
||||
|
||||
async def get_games_of_period(self, username: str, since_timestamp: int, until_timestamp: int, rated_only: bool = True) -> GamesOfPeriodResponse:
|
||||
"""
|
||||
Получает статистику игр пользователя за определенный период.
|
||||
|
||||
Получает игры от Lichess API за указанный период, обрабатывает их
|
||||
и возвращает агрегированную статистику по режимам игр.
|
||||
|
||||
Args:
|
||||
username: Имя пользователя на Lichess
|
||||
since_timestamp: Начало периода (Unix timestamp в секундах)
|
||||
until_timestamp: Конец периода (Unix timestamp в секундах)
|
||||
rated_only: Только рейтинговые игры (по умолчанию True)
|
||||
|
||||
Returns:
|
||||
GamesOfPeriodResponse с статистикой игр
|
||||
"""
|
||||
try:
|
||||
# Конвертируем timestamp в миллисекунды для API Lichess
|
||||
since_ms = since_timestamp * 1000
|
||||
until_ms = until_timestamp * 1000
|
||||
|
||||
# Получаем игры
|
||||
games = await self.lichess_client.get_games_of_period(username, since_ms, until_ms, rated_only)
|
||||
|
||||
if games is None:
|
||||
return GamesOfPeriodResponse(
|
||||
message=f"Пользователь {username} не найден",
|
||||
username=username,
|
||||
period_start=since_timestamp,
|
||||
period_end=until_timestamp,
|
||||
games_count=0
|
||||
)
|
||||
|
||||
if not games:
|
||||
return GamesOfPeriodResponse(
|
||||
message=f"Игры за указанный период не найдены",
|
||||
username=username,
|
||||
period_start=since_timestamp,
|
||||
period_end=until_timestamp,
|
||||
games_count=0
|
||||
)
|
||||
|
||||
# Обрабатываем игры
|
||||
games_stats = self._process_games_of_period(games, username)
|
||||
|
||||
return GamesOfPeriodResponse(
|
||||
message="Статистика игр за период",
|
||||
username=username,
|
||||
period_start=since_timestamp,
|
||||
period_end=until_timestamp,
|
||||
games_count=len(games),
|
||||
data=games_stats
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики игр за период: {e}")
|
||||
return GamesOfPeriodResponse(
|
||||
message=f"Ошибка при получении статистики: {str(e)}",
|
||||
username=username,
|
||||
period_start=since_timestamp,
|
||||
period_end=until_timestamp,
|
||||
games_count=0
|
||||
)
|
||||
|
||||
def _process_puzzle_activities(self, activities: List[Dict[str, Any]], since_ms: int, until_ms: int) -> PuzzleStats:
|
||||
"""
|
||||
Обрабатывает активности по задачам и возвращает статистику за период
|
||||
"""
|
||||
puzzles_in_period = []
|
||||
|
||||
for i, activity in enumerate(activities):
|
||||
# Lichess API использует поле 'date' вместо 'createdAt'
|
||||
created_at = activity.get('date')
|
||||
if created_at is None:
|
||||
if i < 3: # Логируем только первые 3
|
||||
logger.warning(f"Активность {i} не имеет date: {list(activity.keys())}")
|
||||
continue
|
||||
|
||||
# Логируем первую активность для отладки
|
||||
if i == 0:
|
||||
logger.info(f"Первая активность: date={created_at}, since={since_ms}, until={until_ms}")
|
||||
|
||||
# Фильтруем по периоду [since_ms, until_ms)
|
||||
if since_ms <= created_at < until_ms:
|
||||
puzzles_in_period.append(activity)
|
||||
|
||||
logger.info(f"Найдено {len(puzzles_in_period)} активностей в периоде из {len(activities)}")
|
||||
|
||||
# Подсчитываем статистику
|
||||
total_attempts = len(puzzles_in_period)
|
||||
solved = sum(1 for activity in puzzles_in_period if activity.get('win', False))
|
||||
failed = total_attempts - solved
|
||||
success_rate = (solved / total_attempts * 100) if total_attempts > 0 else 0.0
|
||||
|
||||
return PuzzleStats(
|
||||
total_attempts=total_attempts,
|
||||
solved=solved,
|
||||
failed=failed,
|
||||
success_rate=round(success_rate, 2)
|
||||
)
|
||||
|
||||
async def get_puzzle_of_period(self, token: str, since_ms: int, until_ms: int, max_puzzles: int = 50) -> PuzzleOfPeriodResponse:
|
||||
"""
|
||||
Получает статистику решения задач за определенный период.
|
||||
|
||||
Получает активность по решению задач от Lichess API, фильтрует по периоду
|
||||
и возвращает агрегированную статистику решения задач.
|
||||
|
||||
Args:
|
||||
token: Bearer токен авторизации от Lichess
|
||||
since_ms: Начало периода (Unix timestamp в миллисекундах)
|
||||
until_ms: Конец периода (Unix timestamp в миллисекундах)
|
||||
max_puzzles: Максимальное количество задач для получения (по умолчанию 50)
|
||||
|
||||
Returns:
|
||||
PuzzleOfPeriodResponse с статистикой решения задач
|
||||
"""
|
||||
try:
|
||||
# Получаем активности по задачам
|
||||
activities = await self.lichess_client.get_puzzle_activity(token, max_puzzles)
|
||||
|
||||
if activities is None:
|
||||
return PuzzleOfPeriodResponse(
|
||||
message="Неверный токен авторизации или доступ запрещен",
|
||||
period_start=since_ms,
|
||||
period_end=until_ms,
|
||||
max_puzzles=max_puzzles,
|
||||
puzzles_in_period=0
|
||||
)
|
||||
|
||||
if not activities:
|
||||
return PuzzleOfPeriodResponse(
|
||||
message="Активности по задачам не найдены",
|
||||
period_start=since_ms,
|
||||
period_end=until_ms,
|
||||
max_puzzles=max_puzzles,
|
||||
puzzles_in_period=0
|
||||
)
|
||||
|
||||
# Обрабатываем активности
|
||||
puzzle_stats = self._process_puzzle_activities(activities, since_ms, until_ms)
|
||||
|
||||
return PuzzleOfPeriodResponse(
|
||||
message="Статистика решения задач за период",
|
||||
period_start=since_ms,
|
||||
period_end=until_ms,
|
||||
max_puzzles=max_puzzles,
|
||||
puzzles_in_period=puzzle_stats.total_attempts,
|
||||
data=puzzle_stats
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики решения задач за период: {e}")
|
||||
return PuzzleOfPeriodResponse(
|
||||
message=f"Ошибка при получении статистики: {str(e)}",
|
||||
period_start=since_ms,
|
||||
period_end=until_ms,
|
||||
max_puzzles=max_puzzles,
|
||||
puzzles_in_period=0
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Закрывает сервис статистики.
|
||||
|
||||
Освобождает ресурсы и корректно закрывает HTTP клиент.
|
||||
Должен вызываться при завершении работы с сервисом.
|
||||
"""
|
||||
await self.lichess_client.close()
|
||||
Loading…
Add table
Add a link
Reference in a new issue