турнирка
This commit is contained in:
202
get_data_new.py
202
get_data_new.py
@@ -9,6 +9,7 @@ import logging.config
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
import pandas as pd
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
@@ -669,29 +670,14 @@ def run_live_loop(
|
||||
lang: str,
|
||||
game_meta: dict,
|
||||
stop_event: threading.Event,
|
||||
) -> None:
|
||||
"""
|
||||
Оркестратор матча.
|
||||
|
||||
Делает:
|
||||
- создаёт отдельную сессию для лайва,
|
||||
- стартует поток рендера,
|
||||
- запускает poll_game_live (опрос live API, запись api_*.json),
|
||||
- ждёт, пока матч не кончится,
|
||||
- по окончании поднимает stop_event,
|
||||
- джойнит поток рендера.
|
||||
|
||||
Это запускается в отдельном Thread из get_data_API().
|
||||
"""
|
||||
):
|
||||
logger.info(
|
||||
f"[LIVE_THREAD] start live loop for game_id={game_id} "
|
||||
f"(league={league}, season={season})"
|
||||
f"[LIVE_THREAD] start live loop for game_id={game_id} (league={league}, season={season})"
|
||||
)
|
||||
|
||||
# отдельная сессия только для лайва
|
||||
session = create_session()
|
||||
|
||||
# поток рендера (он не демон — мы хотим дождаться финальной записи JSON)
|
||||
# поток рендера
|
||||
render_thread = threading.Thread(
|
||||
target=render_loop,
|
||||
args=(stop_event,),
|
||||
@@ -700,6 +686,15 @@ def run_live_loop(
|
||||
render_thread.start()
|
||||
logger.info("[LIVE_THREAD] render thread spawned")
|
||||
|
||||
# поток standings
|
||||
standings_thread = threading.Thread(
|
||||
target=Standing_func,
|
||||
args=(session, league, season, lang, stop_event),
|
||||
daemon=False,
|
||||
)
|
||||
standings_thread.start()
|
||||
logger.info("[LIVE_THREAD] standings thread spawned")
|
||||
|
||||
try:
|
||||
poll_game_live(
|
||||
session=session,
|
||||
@@ -713,13 +708,13 @@ def run_live_loop(
|
||||
except Exception as e:
|
||||
logger.exception(f"[LIVE_THREAD] crash in live loop for game_id={game_id}: {e}")
|
||||
finally:
|
||||
# В любом случае (матч кончился, KeyboardInterrupt, ошибка) —
|
||||
# говорим рендеру завершаться и дожидаемся.
|
||||
stop_event.set()
|
||||
logger.info(f"[LIVE_THREAD] stopping render thread for game_id={game_id}")
|
||||
render_thread.join()
|
||||
logger.info(f"[LIVE_THREAD] stop live loop for game_id={game_id}")
|
||||
logger.info(f"[LIVE_THREAD] stopping worker threads for game_id={game_id}")
|
||||
|
||||
render_thread.join()
|
||||
standings_thread.join()
|
||||
|
||||
logger.info(f"[LIVE_THREAD] stop live loop for game_id={game_id}")
|
||||
|
||||
# ============================================================================
|
||||
# 7. Постобработка статистики для вывода
|
||||
@@ -1322,6 +1317,167 @@ def Scores_Quarter(merged: dict) -> None:
|
||||
logger.error(f"Ошибка в Scores_Quarter: {e}", exc_info=True)
|
||||
|
||||
|
||||
def Standing_func(
|
||||
session: requests.Session,
|
||||
league: str,
|
||||
season: str,
|
||||
lang: str,
|
||||
stop_event: threading.Event,
|
||||
out_dir: str = "static",
|
||||
) -> None:
|
||||
"""
|
||||
Фоновый поток с турнирной таблицей (standings).
|
||||
|
||||
Что делает:
|
||||
- Периодически (не чаще, чем interval для "standings" в URLS) тянет /standings
|
||||
для лиги+сезона.
|
||||
- Для каждой подтаблицы (regular season, playoffs и т.д.) нормализует данные,
|
||||
досчитывает полезные колонки (W/L, %) и сохраняет в
|
||||
static/standings_<league>_<compName>.json
|
||||
- Останавливается, когда поднят stop_event.
|
||||
|
||||
Почему отдельный поток?
|
||||
- standings нам нужна даже во время лайва игры, но не каждую секунду.
|
||||
- Она не должна блокировать рендер и не должна блокировать poll_game_live.
|
||||
"""
|
||||
logger.info("[STANDINGS_THREAD] start standings loop")
|
||||
|
||||
# когда мы последний раз успешно обновили standings
|
||||
last_call_ts = 0
|
||||
|
||||
# как часто вообще можно дёргать standings
|
||||
interval = get_interval_by_name("standings")
|
||||
|
||||
while not stop_event.is_set():
|
||||
now = time.time()
|
||||
|
||||
# достаточно рано? если нет — просто подожди немного
|
||||
if now - last_call_ts < interval:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
try:
|
||||
# тянем свежие данные standings тем же способом, что в get_data_API
|
||||
data_standings = fetch_api_data(
|
||||
session,
|
||||
"standings",
|
||||
host=HOST,
|
||||
league=league,
|
||||
season=season,
|
||||
lang=lang,
|
||||
)
|
||||
|
||||
# fetch_api_data для standings вернёт либо:
|
||||
# - dict с "items": [...], либо
|
||||
# - сам массив items (если get_items нашёл список)
|
||||
# Мы хотим привести к единому формату, как было в твоём коде.
|
||||
if not data_standings:
|
||||
logger.debug("[STANDINGS_THREAD] standings empty")
|
||||
# не обновляем last_call_ts, чтобы через секунду попытаться снова
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# Если data_standings оказался списком, приведём к виду {"items": [...]}:
|
||||
if isinstance(data_standings, list):
|
||||
items = data_standings
|
||||
else:
|
||||
items = data_standings.get("items") or []
|
||||
|
||||
if not items:
|
||||
logger.debug("[STANDINGS_THREAD] no items in standings")
|
||||
last_call_ts = now # запрос был успешным, но пустым
|
||||
continue
|
||||
|
||||
# Обрабатываем каждый "item" внутри standings:
|
||||
for item in items:
|
||||
comp = item.get("comp", {})
|
||||
comp_name = (comp.get("name") or "unknown_comp").replace(" ", "_")
|
||||
|
||||
# 1) обычная таблица регулярки
|
||||
if item.get("standings"):
|
||||
standings_rows = item["standings"]
|
||||
|
||||
# pandas нормализация
|
||||
df = pd.json_normalize(standings_rows)
|
||||
|
||||
# убираем поле 'scores', если есть
|
||||
if "scores" in df.columns:
|
||||
df = df.drop(columns=["scores"])
|
||||
|
||||
# добавляем w_l, procent, plus_minus если есть нужные столбцы
|
||||
if (
|
||||
"totalWin" in df.columns
|
||||
and "totalDefeat" in df.columns
|
||||
and "totalGames" in df.columns
|
||||
and "totalGoalPlus" in df.columns
|
||||
and "totalGoalMinus" in df.columns
|
||||
):
|
||||
# W / L
|
||||
df["w_l"] = (
|
||||
df["totalWin"].fillna(0).astype(int).astype(str)
|
||||
+ " / "
|
||||
+ df["totalDefeat"].fillna(0).astype(int).astype(str)
|
||||
)
|
||||
|
||||
# % побед
|
||||
def calc_percent(row):
|
||||
win = row.get("totalWin", 0)
|
||||
games = row.get("totalGames", 0)
|
||||
if (
|
||||
pd.isna(win)
|
||||
or pd.isna(games)
|
||||
or games == 0
|
||||
or (row["w_l"] == "0 / 0")
|
||||
):
|
||||
return 0
|
||||
return round(win * 100 / games + 0.000005)
|
||||
|
||||
df["procent"] = df.apply(calc_percent, axis=1)
|
||||
|
||||
# +/- по очкам
|
||||
df["plus_minus"] = (
|
||||
df["totalGoalPlus"].fillna(0).astype(int)
|
||||
- df["totalGoalMinus"].fillna(0).astype(int)
|
||||
)
|
||||
|
||||
# готовим питоновский список словарей для атомарной записи
|
||||
standings_payload = df.to_dict(orient="records")
|
||||
|
||||
filename = f"standings_{league}_{comp_name}"
|
||||
atomic_write_json(standings_payload, filename, out_dir)
|
||||
logger.info(
|
||||
f"[STANDINGS_THREAD] saved {filename}.json ({len(standings_payload)} rows)"
|
||||
)
|
||||
|
||||
# 2) плейофф-пары (playoffPairs)
|
||||
elif item.get("playoffPairs"):
|
||||
playoff_rows = item["playoffPairs"]
|
||||
df = pd.json_normalize(playoff_rows)
|
||||
|
||||
standings_payload = df.to_dict(orient="records")
|
||||
|
||||
filename = f"standings_{league}_{comp_name}"
|
||||
atomic_write_json(standings_payload, filename, out_dir)
|
||||
logger.info(
|
||||
f"[STANDINGS_THREAD] saved {filename}.json (playoffPairs, {len(standings_payload)} rows)"
|
||||
)
|
||||
|
||||
# если ни standings ни playoffPairs — просто пропускаем этот блок
|
||||
else:
|
||||
continue
|
||||
|
||||
# если всё прошло без исключения — фиксируем время удачного апдейта
|
||||
last_call_ts = now
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[STANDINGS_THREAD] ошибка в турнирном положении: {e}")
|
||||
|
||||
# не жрём CPU впустую
|
||||
time.sleep(1)
|
||||
|
||||
logger.info("[STANDINGS_THREAD] stop standings loop")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 8. Суточный цикл: находим игру, следим в лайве, потом уходим спать
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user