Создание единого проекта 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
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue