2025-10-26 20:23:26 +03:00
"""
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 ) ,
2026-07-03 09:38:51 +00:00
rated_only : bool = Query ( True ,
description = " Только рейтинговые игры (по умолчанию true - рекомендуется) " ,
example = True ) ,
include_games : bool = Query ( False ,
description = " Включить построчный список отдельных партий (только blitz/rapid/classical) " ,
example = False )
2025-10-26 20:23:26 +03:00
) :
"""
## Статистика игр за период
Получает детальную статистику игр пользователя за указанный период времени .
Этот эндпоинт позволяет получить подробную аналитику по играм за любой период .
### Возможности:
- * * Фильтрация по времени * * : точный период с 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
2026-07-03 09:38:51 +00:00
result = await stats_service . get_games_of_period ( username , since_seconds , until_seconds , rated_only , include_games )
2026-07-02 19:29:06 +00:00
if not result . success :
# Реальная ошибка при обращении к Lichess — не маскируем её под "0 игр"
raise HTTPException ( status_code = 502 , detail = result . message )
2025-10-26 20:23:26 +03:00
return result
2026-07-02 19:29:06 +00:00
except HTTPException :
raise
2025-10-26 20:23:26 +03:00
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 )
2026-07-02 19:29:06 +00:00
if not result . success :
# Реальная ошибка при обращении к Lichess — не маскируем её под "0 пазлов"
raise HTTPException ( status_code = 502 , detail = result . message )
2025-10-26 20:23:26 +03:00
return result
2026-07-02 19:29:06 +00:00
except HTTPException :
raise
2025-10-26 20:23:26 +03:00
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 )