From c4de3c84fe763f8bb336d4575928caaeddd24633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Mon, 27 Oct 2025 16:55:15 +0300 Subject: [PATCH] test1 --- get_data_new.py | 562 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 561 insertions(+), 1 deletion(-) diff --git a/get_data_new.py b/get_data_new.py index 476e269..896324b 100644 --- a/get_data_new.py +++ b/get_data_new.py @@ -1 +1,561 @@ -test=4 \ No newline at end of file +import time +from datetime import datetime +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry +import requests +import json +import os +import tempfile +import argparse +import platform +import sys +import logging +import pandas as pd +import logging.config +from zoneinfo import ZoneInfo +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed + +HOST = "https://ref.russiabasket.org" +APP_TZ = ZoneInfo("Europe/Moscow") +MYHOST = platform.node() +TELEGRAM_BOT_TOKEN = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY" +# TELEGRAM_CHAT_ID = 228977654 +TELEGRAM_CHAT_ID = -4803699526 +if not os.path.exists("logs"): + os.makedirs("logs") + +_write_lock = threading.Lock() +URLS = [ + { + "name": "seasons", + "url": "{host}/api/abc/comps/seasons?Tag={league}&Lang={lang}", + "interval": 86400, # раз в сутки + }, + { + "name": "standings", + "url": "{host}/api/abc/comps/standings?tag={league}&season={season}&lang={lang}", + "interval": 1800, # раз в 30 минут + }, + { + "name": "calendar", + "url": "{host}/api/abc/comps/calendar?Tag={league}&Season={season}&Lang={lang}&MaxResultCount=1000", + "interval": 86400, # раз в сутки + }, + { + "name": "game", + "url": "{host}/api/abc/games/game?Id={game_id}&Lang={lang}", + "interval": 600, # раз в 10 минут + }, + { + "name": "pregame", + "url": "{host}/api/abc/games/pregame?tag={league}&season={season}&Id={game_id}&Lang={lang}", + "interval": 86400, # раз в сутки + }, + { + "name": "pregame-fullstats", + "url": "{host}/api/abc/games/pregame-fullstats?tag={league}&season={season}&id={game_id}&lang={lang}", + "interval": 600, # раз в 10 минут + }, + { + "name": "live-status", + "url": "{host}/api/abc/games/live-status?id={game_id}", + "interval": 1, # каждую секунду + }, + { + "name": "box-score", + "url": "{host}/api/abc/games/box-score?Id={game_id}", + "interval": 1, # каждую секунду + }, + { + "name": "play-by-play", + "url": "{host}/api/abc/games/play-by-play?Id={game_id}", + "interval": 1, # каждую секунду + }, +] + +LOG_CONFIG = { + "version": 1, + "handlers": { + "telegram": { + "class": "telegram_handler.TelegramHandler", + "level": "INFO", + "token": TELEGRAM_BOT_TOKEN, + "chat_id": TELEGRAM_CHAT_ID, + "formatter": "telegram", + }, + "console": { + "class": "logging.StreamHandler", + "level": "INFO", + "formatter": "simple", + "stream": "ext://sys.stdout", + }, + "file": { + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "simple", + "filename": f"logs/GFX_{MYHOST}.log", + "encoding": "utf-8", + }, + }, + "loggers": { + __name__: {"handlers": ["console", "file", "telegram"], "level": "DEBUG"}, + }, + "formatters": { + "telegram": { + "class": "telegram_handler.HtmlFormatter", + "format": f"%(levelname)s [{MYHOST.upper()}] %(message)s", + "use_emoji": "True", + }, + "simple": { + "class": "logging.Formatter", + "format": "%(asctime)s %(levelname)-8s %(funcName)s() - %(message)s", + "datefmt": "%d.%m.%Y %H:%M:%S", + }, + }, +} + +logging.config.dictConfig(LOG_CONFIG) +logger = logging.getLogger(__name__) +logger.handlers[2].formatter.use_emoji = True + + +def create_session() -> requests.Session: + session = requests.Session() + + retries = Retry( + total=3, + backoff_factor=0.5, + status_forcelist=[429, 500, 502, 503, 504], + ) + + session.mount("https://", HTTPAdapter(max_retries=retries)) + session.headers.update( + { + "Connection": "keep-alive", + "Accept": "application/json, */*", + "Accept-Encoding": "gzip, deflate, br", + "User-Agent": "game-watcher/1.0", + } + ) + + return session + + +def get_json(session, url: str, name: str): + resp = session.get(url, timeout=10) + resp.raise_for_status() + data = resp.json() + atomic_write_json(data, f"api_{name}") + return data + + +def build_url(name: str, **kwargs) -> str: + """ + Собрать конечный URL по имени ручки из URLS. + Пример: + build_url("standings", host=..., league=..., season=..., lang=...) + """ + template = next((u["url"] for u in URLS if u["name"] == name), None) + if not template: + raise ValueError(f"Unknown URL name: {name}") + + return template.format(**kwargs) + + +def atomic_write_json(data, name: str, out_dir: str = "static"): + """ + Сохраняет data в static/.json атомарно. + Потокобезопасно (глобальный lock). + """ + os.makedirs(out_dir, exist_ok=True) + filename = os.path.join(out_dir, f"{name}.json") + + with _write_lock: + with tempfile.NamedTemporaryFile( + "w", delete=False, dir=out_dir, encoding="utf-8" + ) as tmp_file: + json.dump(data, tmp_file, ensure_ascii=False, indent=2) + tmp_file.flush() + os.fsync(tmp_file.fileno()) + tmp_name = tmp_file.name + + os.replace(tmp_name, filename) + + +def get_items(data): + for k, v in data.items(): + if isinstance(v, list): + return data[k] + + +def poll_one_endpoint(session, endpoint_name, league, season, game_id, lang): + """ + Дёрнуть конкретный endpoint и вернуть (endpoint_name, data или None) + """ + if endpoint_name == "live-status": + data = fetch_api_data( + session, + "live-status", + host=HOST, + game_id=game_id, + ) + return endpoint_name, data + + if endpoint_name == "box-score": + data = fetch_api_data( + session, + "box-score", + host=HOST, + game_id=game_id, + ) + return endpoint_name, data + + if endpoint_name == "play-by-play": + data = fetch_api_data( + session, + "play-by-play", + host=HOST, + game_id=game_id, + ) + return endpoint_name, data + + if endpoint_name == "game": + data = fetch_api_data( + session, + "game", + host=HOST, + game_id=game_id, + lang=lang, + ) + return endpoint_name, data + + if endpoint_name == "pregame-fullstats": + data = fetch_api_data( + session, + "pregame-fullstats", + host=HOST, + league=league, + season=season, + game_id=game_id, + lang=lang, + ) + return endpoint_name, data + + # fallback — вдруг добавим что-то ещё + data = fetch_api_data(session, endpoint_name, host=HOST, game_id=game_id, lang=lang) + return endpoint_name, data + + +def fetch_api_data(session, name: str, name_save: str = None, **kwargs): + """ + Универсальная функция для получения данных с API: + 1. Собирает URL по имени ручки + 2. Получает JSON + 3. Возвращает основной список данных (если есть) + """ + url = build_url(name, **kwargs) + try: + json_data = get_json(session, url, name_save or name) + if json_data: + items = get_items(json_data) + return items if items is not None else json_data + return None + except Exception as ex: + logger.error(f"{url} | {ex}") + + +def parse_game_start_dt(item: dict) -> datetime: + """ + Достаёт дату/время начала матча из объекта расписания и приводит к APP_TZ. + Приоритет полей: + 1) game.defaultZoneDateTime — уже в "дефолтной зоне" лиги (например, +03:00) + 2) game.scheduledTime — ISO 8601 с оффсетом (например, 2025-09-30T19:00:00+04:00) + 3) game.startTime — если API когда-то его заполняет + 4) (fallback) game.localDate + game.localTime — считаем, что это локальное время площадки, задаём tz=APP_TZ + + Возвращает aware-datetime в APP_TZ. + """ + g = item.get("game", {}) if "game" in item else item + + raw = g.get("defaultZoneDateTime") or g.get("scheduledTime") or g.get("startTime") + if raw: + try: + dt = datetime.fromisoformat(raw) # ISO-8601 + return dt.astimezone(APP_TZ) + except Exception as e: + raise RuntimeError(f"Ошибка парсинга ISO времени '{raw}': {e}") + + # Fallback: localDate + localTime (пример: "30.09.2025" + "19:00") + ld, lt = g.get("localDate"), g.get("localTime") + if ld and lt: + try: + naive = datetime.strptime(f"{ld} {lt}", "%d.%m.%Y %H:%M") + aware = naive.replace(tzinfo=APP_TZ) + return aware + except Exception as e: + raise RuntimeError(f"Ошибка парсинга localDate/localTime '{ld} {lt}': {e}") + + raise RuntimeError( + "Не найдено ни одного подходящего поля времени (defaultZoneDateTime/scheduledTime/startTime/localDate+localTime)." + ) + + +def get_game_id(team_games: list[dict], team: str) -> tuple[dict | None, dict | None]: + """ + Принимаем все расписание и ищем для домашней команды game_id. + Если сегодня нет матча, то берем game_id прошлой игры. + """ + now = datetime.now(APP_TZ) + today = now.date() + today_game = None + last_played = None + + for g in team_games: + start = parse_game_start_dt(g) + status = g.get("game", {}).get("gameStatus", "").lower() + if ( + start.date() == today + and today_game is None + and g["team1"]["name"].lower() == team.lower() + ): + today_game = g + last_played = None + elif ( + start <= now + and status == "resultconfirmed" + and g["team1"]["name"].lower() == team.lower() + ): + today_game = None + last_played = g + return today_game, last_played + + +def is_game_live(game_obj: dict) -> bool: + """ + Пытаемся понять, идёт ли сейчас матч. + game_obj ожидается как today_game["game"] (из calendar). + """ + status = (game_obj.get("gameStatus") or "").lower() + + # эвристика: + # - "resultconfirmed" -> матч кончился + # - "scheduled" / "notstarted" -> ещё не начался + # всё остальное считаем лайвом + if status in ("resultconfirmed", "finished", "final"): + return False + if status in ("scheduled", "notstarted", "draft"): + return False + return True + + +def get_interval_by_name(name: str) -> int: + """ + вернуть interval из URLS по имени ручки + """ + for u in URLS: + if u["name"] == name: + return u["interval"] + raise ValueError(f"interval not found for {name}") + + +def poll_game_live(session, league: str, season: str, game_id: int, lang: str, game_meta: dict): + """ + Онлайн-цикл: + - "game" и "pregame-fullstats" раз в 600 сек + - "live-status", "box-score", "play-by-play" раз в 1 сек + + Всё, что надо сейчас дернуть, дергаем параллельно. + Цикл выходит, когда матч перестаёт быть live. + """ + + slow_endpoints = ["game",] #"pregame-fullstats"] + fast_endpoints = ["live-status", "box-score", "play-by-play"] + + last_call = {} + now = time.time() + for name in slow_endpoints + fast_endpoints: + last_call[name] = 0 # форсим первый вызов сразу + + # пул потоков: 5 нам хватит (у нас максимум 5 ручек одновременно) + with ThreadPoolExecutor(max_workers=5) as executor: + while True: + now = time.time() + + # собрать, какие ручки надо дёрнуть прямо сейчас + to_run = [] + for ep in fast_endpoints + slow_endpoints: + interval = get_interval_by_name(ep) + if now - last_call[ep] >= interval: + to_run.append(ep) + + futures = [] + if to_run: + for ep in to_run: + futures.append( + executor.submit( + poll_one_endpoint, + session, + ep, + league, + season, + game_id, + lang, + ) + ) + + # будем смотреть на ответы, особенно live-status + game_finished = False + for fut in as_completed(futures): + try: + ep_name, data = fut.result() + last_call[ep_name] = now # помечаем как обновлённый + + if ep_name == "live-status": + # проверяем статус, не закончилась ли игра + if isinstance(data, dict): + st = ( + data.get("status") + or data.get("gameStatus") + or "" + ).lower() + if st in ("resultconfirmed", "finished", "final"): + logger.info( + f"Game {game_id} finished by live-status -> stop loop" + ) + game_finished = True + + except Exception as e: + logger.exception(f"poll endpoint error: {e}") + + # вторая страховка (инфо из календаря) + if not is_game_live(game_meta): + logger.info(f"Game {game_id} no longer live by calendar meta -> stop loop") + break + + if game_finished: + break + + # чуть притормозим, чтобы не жарить CPU + time.sleep(0.2) + logger.debug("live poll tick ok") + + +def get_data_API2(session, league: str, team: str, lang: str): + json_seasons = fetch_api_data( + session, "seasons", host=HOST, league=league, lang=lang + ) + if not json_seasons: + print("Не удалось получить список сезонов") + return + + season = json_seasons[0]["season"] + fetch_api_data( + session, "standings", host=HOST, league=league, season=season, lang=lang + ) + json_calendar = fetch_api_data( + session, "calendar", host=HOST, league=league, season=season, lang=lang + ) + if not json_calendar: + print("Не удалось получить список матчей") + return + + today_game, last_played = get_game_id(json_calendar, team) + if last_played: + game_id = last_played["game"]["id"] + fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang) + elif today_game: + print("ОНЛАЙН") + game_id = today_game["game"]["id"] + fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang) + fetch_api_data( + session, + "pregame", + host=HOST, + league=league, + season=season, + game_id=game_id, + lang=lang, + ) + + +def get_data_API(session, league: str, team: str, lang: str): + json_seasons = fetch_api_data( + session, "seasons", host=HOST, league=league, lang=lang + ) + if not json_seasons: + logger.error("Не удалось получить список сезонов") + return + + season = json_seasons[0]["season"] + + # standings и calendar просто кешируем в файлы + fetch_api_data( + session, "standings", host=HOST, league=league, season=season, lang=lang + ) + json_calendar = fetch_api_data( + session, "calendar", host=HOST, league=league, season=season, lang=lang + ) + if not json_calendar: + logger.error("Не удалось получить список матчей") + return + + today_game, last_played = get_game_id(json_calendar, team) + + # если есть завершённая последняя игра — просто сохраним её статические данные + if last_played and not today_game: + game_id = last_played["game"]["id"] + logger.info(f"Последний завершённый матч id={game_id}") + fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang) + return + + # если есть матч сегодня + if today_game: + game_id = today_game["game"]["id"] + logger.info(f"Онлайн матч id={game_id}") + + # сразу получить стартовые данные перед циклом + fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang) + # fetch_api_data( + # session, + # "pregame-fullstats", + # host=HOST, + # league=league, + # season=season, + # game_id=game_id, + # lang=lang, + # ) + + # если матч реально идёт -> запускаем быстрый опрос + if is_game_live(today_game["game"]): + poll_game_live( + session=session, + league=league, + season=season, + game_id=game_id, + lang=lang, + game_meta=today_game["game"], + ) + else: + logger.info( + "Матч ещё не стартовал, но сегодня. Просто сохранили стартовые данные." + ) + return + + logger.info("Для этой команды игр сегодня нет и нет завершённой последней игры.") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--league", default="vtb") + parser.add_argument("--team", required=True) + parser.add_argument("--lang", default="en") + args = parser.parse_args() + + session = create_session() + + get_data_API(session, args.league, args.team, args.lang) + + +if __name__ == "__main__": + main()