добавил функцию по прогону данных один раз, если матч старый
This commit is contained in:
147
get_data_new.py
147
get_data_new.py
@@ -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("Для этой команды игр сегодня нет и нет завершённой последней игры.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user