добавил функцию по прогону данных один раз, если матч старый

This commit is contained in:
2025-10-27 20:17:24 +03:00
parent 4ad11815a5
commit 6278a8e3df

View File

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