diff --git a/get_data_new.py b/get_data_new.py index 3ecb591..cc3a992 100644 --- a/get_data_new.py +++ b/get_data_new.py @@ -143,6 +143,7 @@ logger.handlers[2].formatter.use_emoji = True # 3. I/O вспомогательные функции # ============================================================================ + def atomic_write_json(data: Any, name: str, out_dir: str = "static") -> None: """ Потокобезопасная запись JSON в static/.json. @@ -200,6 +201,7 @@ def _now_iso() -> str: # 4. Работа с HTTP / API # ============================================================================ + def create_session() -> requests.Session: """ Создаёт requests.Session с ретраями и дефолтными заголовками. @@ -264,7 +266,9 @@ def get_items(data: dict) -> Optional[list]: return None -def fetch_api_data(session: requests.Session, name: str, name_save: str = None, **kwargs) -> Any: +def fetch_api_data( + session: requests.Session, name: str, name_save: str = None, **kwargs +) -> Any: """ Универсальный обёртчик над API: - строит URL по имени ручки, @@ -312,9 +316,7 @@ def poll_one_endpoint( return endpoint_name, data if endpoint_name == "game": - data = fetch_api_data( - session, "game", host=HOST, game_id=game_id, lang=lang - ) + data = fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang) return endpoint_name, data if endpoint_name == "pregame-fullstats": @@ -349,6 +351,7 @@ def get_interval_by_name(name: str) -> int: # 5. Работа с расписанием / статусом матча # ============================================================================ + def parse_game_start_dt(item: dict) -> datetime: """ Достаёт дату/время начала матча из объекта календаря и нормализует в APP_TZ. @@ -455,6 +458,7 @@ def is_game_live(game_obj: dict) -> bool: # 6. Лайв-петля: опрос API и поток рендера # ============================================================================ + def poll_game_live( session: requests.Session, league: str, @@ -487,7 +491,9 @@ def poll_game_live( while True: # внешний стоп: операторская остановка или завершение run_live_loop if stop_event.is_set(): - logger.info(f"[POLL] stop_event set -> break live poll for game {game_id}") + logger.info( + f"[POLL] stop_event set -> break live poll for game {game_id}" + ) break now = time.time() @@ -525,10 +531,7 @@ def poll_game_live( # проверяем статус лайва if ep_name == "live-status": if isinstance(data, dict): - st = ( - data.get("result").get("gameStatus") - or "" - ).lower() + st = (data.get("result").get("gameStatus") or "").lower() if st in ("resultconfirmed", "finished", "result"): logger.info( f"[POLL] Game {game_id} finished by live-status" @@ -550,7 +553,9 @@ def poll_game_live( # вторая точка выхода по stop_event после sleep if stop_event.is_set(): - logger.info(f"[POLL] stop_event set after sleep -> break live poll for game {game_id}") + logger.info( + f"[POLL] stop_event set after sleep -> break live poll for game {game_id}" + ) break @@ -603,9 +608,7 @@ def build_render_state() -> dict: player["stats"] = stat team["total"] = box_team.get("total", {}) - game_data["scoreByPeriods"] = box_score_data["result"].get( - "scoreByPeriods", [] - ) + game_data["scoreByPeriods"] = box_score_data["result"].get("scoreByPeriods", []) game_data["fullScore"] = box_score_data["result"].get("fullScore", {}) # плей-бай-плей и live_status @@ -693,7 +696,6 @@ def run_live_loop( ) standings_thread.start() logger.info("[LIVE_THREAD] standings thread spawned") - try: poll_game_live( @@ -715,7 +717,8 @@ def run_live_loop( standings_thread.join() logger.info(f"[LIVE_THREAD] stop live loop for game_id={game_id}") - + + def Referee(merged: dict, *, out_dir: str = "static") -> None: """ Поток, создающий JSON-файл с информацией о судьях матча. @@ -776,10 +779,40 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None: logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True) +def render_once_after_game(out_name: str = "game") -> None: + """ + Одноразовая генерация всех выходных json-файлов (team_stats.json, + team1.json, team2.json, scores.json, live_status.json, game.json и т.д.) + без запуска вечного render_loop. + + Используется, когда: + - матч уже завершён (resultconfirmed/finished), + - или матч не идёт (нет лайва), но мы хотим иметь конечные данные по нему. + """ + try: + state = build_render_state() + + # основные сводки по матчу + Team_Both_Stat(state) + Json_Team_Generation(state, who="team1") + Json_Team_Generation(state, who="team2") + Scores_Quarter(state) + Referee(state) + + # live_status отдельно, + сам матч + atomic_write_json([state["result"]["live_status"]], "live_status") + atomic_write_json(state["result"], out_name) + + logger.info("[RENDER_ONCE] финальные json сохранены после матча") + except Exception as ex: + logger.exception(f"[RENDER_ONCE] error while building final state: {ex}") + + # ============================================================================ # 7. Постобработка статистики для вывода # ============================================================================ + def format_time(seconds: float | int) -> str: """ Удобный формат времени для игроков: @@ -878,10 +911,13 @@ def Json_Team_Generation( if item.get("countryId") is None and item.get("countryName") == "Russia" else ( - "" if item.get("countryId") is None - else (item.get("countryId") or "").lower() - if item.get("countryName") is not None - else "" + "" + if item.get("countryId") is None + else ( + (item.get("countryId") or "").lower() + if item.get("countryName") is not None + else "" + ) ) ) + ".svg" @@ -1162,6 +1198,7 @@ def add_new_team_stat( Возвращает обновлённый словарь. """ + def safe_int(v): try: return int(v) @@ -1185,21 +1222,17 @@ def add_new_team_stat( "pt-2": f"{goal2}/{shot2}", "pt-3": f"{goal3}/{shot3}", "fg": f"{goal2 + goal3}/{shot2 + shot3}", - "pt-1_pro": format_percent(goal1, shot1), "pt-2_pro": format_percent(goal2, shot2), "pt-3_pro": format_percent(goal3, shot3), "fg_pro": format_percent(goal2 + goal3, shot2 + shot3), - "Reb": str(def_reb + off_reb), - "avgAge": str(avg_age), "ptsStart": str(points[0]), "ptsStart_pro": str(points[1]), "ptsBench": str(points[2]), "ptsBench_pro": str(points[3]), "avgHeight": f"{avg_height} cm", - "timeout_left": str(timeout_left), "timeout_str": str(timeout_str), } @@ -1495,10 +1528,9 @@ def Standing_func( df["procent"] = df.apply(calc_percent, axis=1) # +/- по очкам - df["plus_minus"] = ( - df["totalGoalPlus"].fillna(0).astype(int) - - df["totalGoalMinus"].fillna(0).astype(int) - ) + df["plus_minus"] = df["totalGoalPlus"].fillna(0).astype( + int + ) - df["totalGoalMinus"].fillna(0).astype(int) # готовим питоновский список словарей для атомарной записи standings_payload = df.to_dict(orient="records") @@ -1542,6 +1574,7 @@ def Standing_func( # 8. Суточный цикл: находим игру, следим в лайве, потом уходим спать # ============================================================================ + def get_data_API( session: requests.Session, league: str, @@ -1554,14 +1587,16 @@ def get_data_API( 1. Узнать текущий сезон 2. Обновить standings и calendar 3. Найти игру для нашей команды сегодня (today_game) или последнюю законченную (last_played) - 4. Если есть last_played и нет игры сегодня -> просто забираем /game и пишем api_game.json + 4. Если есть last_played и нет игры сегодня: + - забрать /game по last_played + - один раз сгенерировать финальные JSON (render_once_after_game) 5. Если есть игра сегодня: - - пишем /game сессии - - если статус игры live -> запускаем run_live_loop() в отдельном потоке - и ждём его завершения (до конца матча) + - забрать /game по today_game + - если статус игры live: + · запустить live-петлю (run_live_loop) с рендером в реальном времени + иначе (игра уже финальная, не live): + · один раз сгенерировать финальные JSON (render_once_after_game) 6. Если нет ничего -> просто логируем и выходим - - Эта функция НЕ усыпляет процесс — цикл сна делает main(). """ # 1. сезоны json_seasons = fetch_api_data( @@ -1574,9 +1609,21 @@ def get_data_API( season = json_seasons[0]["season"] # 2. standings и calendar - fetch_api_data(session, "standings", host=HOST, league=league, season=season, lang=lang) + 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 + session, + "calendar", + host=HOST, + league=league, + season=season, + lang=lang, ) if not json_calendar: logger.error("Не удалось получить список матчей") @@ -1585,22 +1632,39 @@ def get_data_API( # 3. какая игра нас интересует? today_game, last_played = get_game_id(json_calendar, team) - # 4. уже сыграли, но сегодня не играем -> просто пишем /game последнего матча + # 4. уже сыграли матч, а сегодня не играем 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) + + # плюс box-score / play-by-play / live-status, + # чтобы render_once_after_game мог всё посчитать + fetch_api_data(session, "box-score", host=HOST, game_id=game_id) + fetch_api_data(session, "play-by-play", host=HOST, game_id=game_id) + fetch_api_data(session, "live-status", host=HOST, game_id=game_id) + + # одноразовый рендер финальных стейтов + render_once_after_game() + return - # 5. матч сегодня + # 5. матч сегодня есть if today_game: game_id = today_game["game"]["id"] logger.info(f"Онлайн матч id={game_id}") - # всегда пишем /game прямо сейчас (обновим api_game.json) + # Всегда обновляем /game прямо сейчас fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang) - # если матч идёт — стартуем live + # и эти ручки тоже сразу дергаем, чтобы у нас были свежие api_*.json + fetch_api_data(session, "box-score", host=HOST, game_id=game_id) + fetch_api_data(session, "play-by-play", host=HOST, game_id=game_id) + fetch_api_data(session, "live-status", host=HOST, game_id=game_id) + + # если матч идёт — запускаем live поток, он сам будет рендерить в цикле if is_game_live(today_game["game"]): t = threading.Thread( target=run_live_loop, @@ -1619,9 +1683,14 @@ def get_data_API( logger.info("live thread finished") + else: + # матч сегодня, но он уже финальный (resultconfirmed / finished) + # значит просто один раз считаем все json-ы + render_once_after_game() + return - # 6. ничего подходящего + # 6. ничего подходящего вообще logger.info("Для этой команды игр сегодня нет и нет завершённой последней игры.")