турнирка
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 datetime import datetime, timedelta, timezone
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from typing import Any, Dict, List, Tuple, Optional
|
from typing import Any, Dict, List, Tuple, Optional
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
@@ -669,29 +670,14 @@ def run_live_loop(
|
|||||||
lang: str,
|
lang: str,
|
||||||
game_meta: dict,
|
game_meta: dict,
|
||||||
stop_event: threading.Event,
|
stop_event: threading.Event,
|
||||||
) -> None:
|
):
|
||||||
"""
|
|
||||||
Оркестратор матча.
|
|
||||||
|
|
||||||
Делает:
|
|
||||||
- создаёт отдельную сессию для лайва,
|
|
||||||
- стартует поток рендера,
|
|
||||||
- запускает poll_game_live (опрос live API, запись api_*.json),
|
|
||||||
- ждёт, пока матч не кончится,
|
|
||||||
- по окончании поднимает stop_event,
|
|
||||||
- джойнит поток рендера.
|
|
||||||
|
|
||||||
Это запускается в отдельном Thread из get_data_API().
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[LIVE_THREAD] start live loop for game_id={game_id} "
|
f"[LIVE_THREAD] start live loop for game_id={game_id} (league={league}, season={season})"
|
||||||
f"(league={league}, season={season})"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# отдельная сессия только для лайва
|
|
||||||
session = create_session()
|
session = create_session()
|
||||||
|
|
||||||
# поток рендера (он не демон — мы хотим дождаться финальной записи JSON)
|
# поток рендера
|
||||||
render_thread = threading.Thread(
|
render_thread = threading.Thread(
|
||||||
target=render_loop,
|
target=render_loop,
|
||||||
args=(stop_event,),
|
args=(stop_event,),
|
||||||
@@ -700,6 +686,15 @@ def run_live_loop(
|
|||||||
render_thread.start()
|
render_thread.start()
|
||||||
logger.info("[LIVE_THREAD] render thread spawned")
|
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:
|
try:
|
||||||
poll_game_live(
|
poll_game_live(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -713,13 +708,13 @@ def run_live_loop(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"[LIVE_THREAD] crash in live loop for game_id={game_id}: {e}")
|
logger.exception(f"[LIVE_THREAD] crash in live loop for game_id={game_id}: {e}")
|
||||||
finally:
|
finally:
|
||||||
# В любом случае (матч кончился, KeyboardInterrupt, ошибка) —
|
|
||||||
# говорим рендеру завершаться и дожидаемся.
|
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
logger.info(f"[LIVE_THREAD] stopping render thread for game_id={game_id}")
|
logger.info(f"[LIVE_THREAD] stopping worker threads for game_id={game_id}")
|
||||||
render_thread.join()
|
|
||||||
logger.info(f"[LIVE_THREAD] stop live loop 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. Постобработка статистики для вывода
|
# 7. Постобработка статистики для вывода
|
||||||
@@ -1322,6 +1317,167 @@ def Scores_Quarter(merged: dict) -> None:
|
|||||||
logger.error(f"Ошибка в Scores_Quarter: {e}", exc_info=True)
|
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. Суточный цикл: находим игру, следим в лайве, потом уходим спать
|
# 8. Суточный цикл: находим игру, следим в лайве, потом уходим спать
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user