LichessStatTgWeb/LichessWebServices/main.py

706 lines
34 KiB
Python
Raw Normal View History

"""
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)