в процессе логирования

This commit is contained in:
2025-11-01 11:48:32 +03:00
parent 02479f8046
commit 6ffb7df0f7

View File

@@ -9,13 +9,14 @@ import time
import queue
import argparse
import uvicorn
from pprint import pprint
import os
import pandas as pd
import json
from datetime import datetime, time as dtime, timedelta
from fastapi.responses import Response
import logging
import logging.config
import platform
# передадим параметры через аргументы или глобальные переменные
@@ -26,16 +27,72 @@ parser.add_argument("--team", required=True)
parser.add_argument("--lang", default="en")
args = parser.parse_args()
MYHOST = platform.node()
if not os.path.exists("logs"):
os.makedirs("logs")
telegram_bot_token = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY"
# TELEGRAM_CHAT_ID = 228977654
telegram_chat_id = -4803699526
log_config = {
"version": 1,
"handlers": {
"telegram": {
"class": "telegram_handler.TelegramHandler",
"level": "INFO",
"token": telegram_bot_token,
"chat_id": telegram_chat_id,
"formatter": "telegram",
},
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.FileHandler",
"level": "DEBUG",
"formatter": "simple",
"filename": f"logs/GFX_{MYHOST}.log",
"encoding": "utf-8",
},
},
"loggers": {
__name__: {"handlers": ["console", "file", "telegram"], "level": "DEBUG"},
},
"formatters": {
"telegram": {
"class": "telegram_handler.HtmlFormatter",
"format": f"%(levelname)s [{MYHOST.upper()}] %(message)s",
"use_emoji": "True",
},
"simple": {
"class": "logging.Formatter",
"format": "%(asctime)s %(levelname)-8s %(funcName)s() - %(message)s",
"datefmt": "%d.%m.%Y %H:%M:%S",
},
},
}
logging.config.dictConfig(log_config)
logger = logging.getLogger(__name__)
logger.handlers[2].formatter.use_emoji = True
LEAGUE = args.league
TEAM = args.team
LANG = args.lang
HOST = "https://pro.russiabasket.org"
HOST = "https://deti.russiabasket.org"
STATUS = False
GAME_ID = None
SEASON = None
GAME_START_DT = None # datetime начала матча (локальная из календаря)
GAME_TODAY = False # флаг: игра сегодня
GAME_SOON = False # флаг: игра сегодня и скоро (<1 часа)
GAME_START_DT = None # datetime начала матча (локальная из календаря)
GAME_TODAY = False # флаг: игра сегодня
GAME_SOON = False # флаг: игра сегодня и скоро (<1 часа)
URLS = {
"seasons": "{host}/api/abc/comps/seasons?Tag={league}",
@@ -50,6 +107,158 @@ URLS = {
}
def start_offline_threads(season, game_id):
"""Запускаем редкие запросы, когда матча нет или он уже сыгран."""
global threads_offline, CURRENT_THREADS_MODE, stop_event_offline
# если уже работаем в офлайне — не дублируем
if CURRENT_THREADS_MODE == "offline":
return
# на всякий случай гасим лайв
stop_live_threads()
stop_event_offline.clear()
threads_offline = [
threading.Thread(
target=get_data_from_API,
args=(
"game",
URLS["game"].format(host=HOST, game_id=game_id, lang=LANG),
1, # раз в секунду/реже
stop_event_offline,
),
daemon=True,
)
]
for t in threads_offline:
t.start()
CURRENT_THREADS_MODE = "offline"
print("[threads] OFFLINE threads started")
def start_live_threads(season, game_id):
"""Запускаем частые онлайн-запросы, когда матч идёт/вот-вот."""
global threads_live, CURRENT_THREADS_MODE, stop_event_live
# если уже в лайве — не дублируем
if CURRENT_THREADS_MODE == "live":
return
# на всякий случай гасим офлайн
stop_offline_threads()
stop_event_live.clear()
threads_live = [
threading.Thread(
target=get_data_from_API,
args=(
"pregame",
URLS["pregame"].format(
host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
),
0.0016667,
stop_event_live,
),
daemon=True,
),
threading.Thread(
target=get_data_from_API,
args=(
"pregame-full-stats",
URLS["pregame-full-stats"].format(
host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
),
0.0016667,
stop_event_live,
),
daemon=True,
),
threading.Thread(
target=get_data_from_API,
args=(
"actual-standings",
URLS["actual-standings"].format(
host=HOST, league=LEAGUE, season=season, lang=LANG
),
0.0016667,
stop_event_live,
),
daemon=True,
),
threading.Thread(
target=get_data_from_API,
args=(
"game",
URLS["game"].format(host=HOST, game_id=game_id, lang=LANG),
0.00016, # часто
stop_event_live,
),
daemon=True,
),
threading.Thread(
target=get_data_from_API,
args=(
"live-status",
URLS["live-status"].format(host=HOST, game_id=game_id),
1,
stop_event_live,
),
daemon=True,
),
threading.Thread(
target=get_data_from_API,
args=(
"box-score",
URLS["box-score"].format(host=HOST, game_id=game_id),
1,
stop_event_live,
),
daemon=True,
),
threading.Thread(
target=get_data_from_API,
args=(
"play-by-play",
URLS["play-by-play"].format(host=HOST, game_id=game_id),
1,
stop_event_live,
),
daemon=True,
),
]
for t in threads_live:
t.start()
CURRENT_THREADS_MODE = "live"
print("[threads] LIVE threads started")
def stop_live_threads():
"""Гасим только live-треды."""
global threads_live
if not threads_live:
return
stop_event_live.set()
for t in threads_live:
t.join(timeout=1)
threads_live = []
print("[threads] LIVE threads stopped")
def stop_offline_threads():
"""Гасим только offline-треды."""
global threads_offline
if not threads_offline:
return
stop_event_offline.set()
for t in threads_offline:
t.join(timeout=1)
threads_offline = []
print("[threads] OFFLINE threads stopped")
# общая очередь
results_q = queue.Queue()
# тут будем хранить последние данные
@@ -57,6 +266,17 @@ latest_data = {}
# событие для остановки потоков
stop_event = threading.Event()
# отдельные события для разных наборов потоков
stop_event_live = threading.Event()
stop_event_offline = threading.Event()
# чтобы из consumer можно было их гасить
threads_live = []
threads_offline = []
# какой режим сейчас запущен: "live" / "offline" / None
CURRENT_THREADS_MODE = None
# Функция запускаемая в потоках
def get_data_from_API(
@@ -133,7 +353,9 @@ def results_consumer():
and "teams" in payload["result"]
):
# обновляем команды
game["data"]["result"]["game"]["fullScore"] = payload["result"]["fullScore"]
game["data"]["result"]["game"]["fullScore"] = payload["result"][
"fullScore"
]
for team in game["data"]["result"]["teams"]:
if team["teamNumber"] != 0:
box_team = [
@@ -169,17 +391,13 @@ def results_consumer():
}
elif "live-status" in source:
# просто сохраним, как и остальные
latest_data[source] = {
"ts": msg["ts"],
"data": payload,
}
# попытка ДОПОЛНИТЕЛЬНО обновить глобальный STATUS по live-status
try:
ls_data = payload.get("result") or payload # иногда сразу result
# тут нужно посмотреть, какое именно поле у тебя в live-status
# допустим, там есть что-то вроде "status" или "gameStatus"
ls_data = payload.get("result") or payload
raw_ls_status = (
ls_data.get("status")
or ls_data.get("gameStatus")
@@ -187,27 +405,42 @@ def results_consumer():
)
if raw_ls_status:
raw_ls_status = str(raw_ls_status).lower()
raw_ls_status_low = str(raw_ls_status).lower()
# варианты, которые считаем "матч окончен"
finished_markers = [
"finished",
"result",
"resultconfirmed",
"ended",
"game over",
"final",
"game over",
]
if any(m in raw_ls_status for m in finished_markers):
# перезатираем глобальный статус — он более актуальный
# если матч сегодня — делаем finished_today, иначе просто finished
from datetime import datetime
if GAME_START_DT and GAME_START_DT.date() == datetime.now().date():
# матч ЗАКОНЧЕН → гасим live и включаем offline
if any(m in raw_ls_status_low for m in finished_markers):
print("[status] match finished → switch to OFFLINE")
if (
GAME_START_DT
and GAME_START_DT.date() == datetime.now().date()
):
globals()["STATUS"] = "finished_today"
else:
globals()["STATUS"] = "finished"
stop_live_threads()
start_offline_threads(SEASON, GAME_ID)
# матч СТАЛ онлайном (напр., из Scheduled → Online)
elif (
"online" in raw_ls_status_low or "live" in raw_ls_status_low
):
if globals().get("STATUS") not in ["live", "live_soon"]:
print(
"[status] match became LIVE → switch to LIVE threads"
)
globals()["STATUS"] = "live"
start_live_threads(SEASON, GAME_ID)
except Exception as e:
print("results_consumer: live-status postprocess error:", e)
@@ -238,7 +471,9 @@ def results_consumer():
}
else:
# 👉 уже есть какой-то game — неполным НЕ затираем
print("results_consumer: got partial game, keeping previous one")
print(
"results_consumer: got partial game, keeping previous one"
)
# и обязательно continue/return из этого elif/if
else:
@@ -253,6 +488,7 @@ def results_consumer():
print("results_consumer error:", repr(e))
continue
def get_items(data: dict) -> list:
"""
Мелкий хелпер: берём первый список в ответе API.
@@ -268,6 +504,7 @@ def get_items(data: dict) -> list:
from datetime import datetime
def pick_game_for_team(calendar_json):
"""
Возвращает:
@@ -292,7 +529,11 @@ def pick_game_for_team(calendar_json):
continue
gdt = extract_game_datetime(game)
gdate = gdt.date() if gdt else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
gdate = (
gdt.date()
if gdt
else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
)
if gdate == today:
cal_status = game["game"].get("gameStatus")
@@ -308,7 +549,11 @@ def pick_game_for_team(calendar_json):
continue
gdt = extract_game_datetime(game)
gdate = gdt.date() if gdt else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
gdate = (
gdt.date()
if gdt
else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
)
if gdate <= today:
last_id = game["game"]["id"]
@@ -319,7 +564,6 @@ def pick_game_for_team(calendar_json):
return last_id, last_dt, False, last_status
def extract_game_datetime(game_item: dict) -> datetime | None:
"""
Из элемента календаря достаём datetime матча.
@@ -336,7 +580,7 @@ def extract_game_datetime(game_item: dict) -> datetime | None:
dt_time = dtime(hour=0, minute=0)
return datetime.combine(dt_date, dt_time)
except Exception:
return None
return None
@asynccontextmanager
@@ -345,7 +589,9 @@ async def lifespan(app: FastAPI):
# 1. проверяем API: seasons
try:
seasons_resp = requests.get(URLS["seasons"].format(host=HOST, league=LEAGUE)).json()
seasons_resp = requests.get(
URLS["seasons"].format(host=HOST, league=LEAGUE)
).json()
season = seasons_resp["items"][0]["season"]
except Exception:
now = datetime.now()
@@ -367,7 +613,9 @@ async def lifespan(app: FastAPI):
calendar = None
# 3. определяем игру
game_id, game_dt, is_today, cal_status = pick_game_for_team(calendar) if calendar else (None, None, False, None)
game_id, game_dt, is_today, cal_status = (
pick_game_for_team(calendar) if calendar else (None, None, False, None)
)
GAME_ID = game_id
GAME_START_DT = game_dt
GAME_TODAY = is_today
@@ -381,7 +629,7 @@ async def lifespan(app: FastAPI):
# 5. Подготовим онлайн и офлайн наборы (как у тебя)
threads_live = [
threading.Thread(
threading.Thread(
target=get_data_from_API,
args=(
"pregame",
@@ -417,7 +665,6 @@ async def lifespan(app: FastAPI):
),
daemon=True,
),
threading.Thread(
target=get_data_from_API,
args=(
@@ -472,66 +719,43 @@ async def lifespan(app: FastAPI):
)
]
# 6. Решение: сегодня / не сегодня
# 5. решаем, что запускать
if not is_today:
# ИГРЫ СЕГОДНЯ НЕТ → крутим только офлайн
STATUS = "no_game_today"
for t in threads_offline:
t.start()
start_offline_threads(SEASON, GAME_ID)
else:
# игра сегодня
if cal_status is None:
# нет статуса в календаре — считаем, что ещё не началась
STATUS = "today_not_started"
for t in threads_offline:
t.start()
start_offline_threads(SEASON, GAME_ID)
elif cal_status == "Scheduled":
# ещё не началась
# проверим, не меньше ли часа до начала
if game_dt:
delta = game_dt - datetime.now()
if delta <= timedelta(hours=1):
# уже скоро → можно запускать онлайн
STATUS = "live_soon"
GAME_SOON = True
for t in threads_live:
t.start()
start_live_threads(SEASON, GAME_ID)
else:
# ещё далеко → офлайн, но говорим что сегодня
STATUS = "today_not_started"
for t in threads_offline:
t.start()
# и можно повесить будильник, как раньше
start_offline_threads(SEASON, GAME_ID)
else:
# нет времени — просто офлайн, но сегодня
STATUS = "today_not_started"
for t in threads_offline:
t.start()
start_offline_threads(SEASON, GAME_ID)
elif cal_status == "Online":
# матч идёт → сразу онлайн
STATUS = "live"
GAME_SOON = False
for t in threads_live:
t.start()
start_live_threads(SEASON, GAME_ID)
elif cal_status in ["Result", "ResultConfirmed"]:
# матч уже сыгран, но дата всё ещё сегодня
STATUS = "finished_today"
for t in threads_offline:
t.start()
start_offline_threads(SEASON, GAME_ID)
else:
# неизвестный статус — безопасный вариант
STATUS = "today_not_started"
for t in threads_offline:
t.start()
start_offline_threads(SEASON, GAME_ID)
yield
# -------- shutdown --------
stop_event.set()
# офлайн/онлайн ты можешь не делить тут, но оставлю
stop_event.set()
for t in threads_live + threads_offline:
t.join(timeout=1)
stop_event.set() # завершить consumer
stop_live_threads()
stop_offline_threads()
thread_result_consumer.join(timeout=1)
@@ -554,7 +778,7 @@ def format_time(seconds: float | int) -> str:
return "0:00"
@app.get("/team1.json")
@app.get("/team1")
async def team1():
game = get_latest_game_safe()
if not game:
@@ -562,7 +786,7 @@ async def team1():
return await team("team1")
@app.get("/team2.json")
@app.get("/team2")
async def team2():
game = get_latest_game_safe()
if not game:
@@ -570,36 +794,36 @@ async def team2():
return await team("team2")
@app.get("/top_team1.json")
@app.get("/top_team1")
async def top_team1():
data = await team("team1")
return await top_sorted_team(data)
@app.get("/top_team2.json")
@app.get("/top_team2")
async def top_team2():
data = await team("team2")
return await top_sorted_team(data)
@app.get("/started_team1.json")
@app.get("/started_team1")
async def started_team1():
data = await team("team1")
return await started_team(data)
@app.get("/started_team2.json")
@app.get("/started_team2")
async def started_team2():
data = await team("team2")
return await started_team(data)
@app.get("/game.json")
@app.get("/game")
async def game():
return latest_data["game"]
@app.get("/status.json")
@app.get("/status")
async def status(request: Request):
def color_for_status(status_value: str) -> str:
"""Подбор цвета статуса в HEX"""
@@ -608,13 +832,25 @@ async def status(request: Request):
return "#00FF00" # зелёный
elif status_value in ["scheduled", "today_not_started", "upcoming"]:
return "#FFFF00" # жёлтый
elif status_value in ["result", "resultconfirmed", "finished", "finished_today"]:
elif status_value in [
"result",
"resultconfirmed",
"finished",
"finished_today",
]:
return "#FF0000" # красный
elif status_value in ["no_game_today", "unknown", "none"]:
return "#FFFFFF" # белый
else:
return "#808080" # серый (неизвестный статус)
# ✳️ сортируем latest_data в нужном порядке
sort_order = ["game", "live-status", "box-score", "play-by-play"]
sorted_keys = (
[k for k in sort_order if k in latest_data] +
sorted([k for k in latest_data if k not in sort_order])
)
data = {
"league": LEAGUE,
"team": TEAM,
@@ -624,16 +860,20 @@ async def status(request: Request):
{
"name": TEAM,
"status": STATUS,
"ts": GAME_START_DT.strftime("%Y-%m-%d %H:%M") if GAME_START_DT else "N/A",
"ts": (
GAME_START_DT.strftime("%Y-%m-%d %H:%M") if GAME_START_DT else "N/A"
),
"link": LEAGUE,
"color": color_for_status(STATUS) # ← добавлено
"color": color_for_status(STATUS),
}
] + [
]
+ [
{
"name": item,
"status": (
latest_data[item]["data"]["status"]
if isinstance(latest_data[item]["data"], dict) and "status" in latest_data[item]["data"]
if isinstance(latest_data[item]["data"], dict)
and "status" in latest_data[item]["data"]
else latest_data[item]["data"]
),
"ts": latest_data[item]["ts"],
@@ -646,14 +886,15 @@ async def status(request: Request):
),
"color": color_for_status(
latest_data[item]["data"]["status"]
if isinstance(latest_data[item]["data"], dict) and "status" in latest_data[item]["data"]
if isinstance(latest_data[item]["data"], dict)
and "status" in latest_data[item]["data"]
else latest_data[item]["data"]
) # ← добавлено
),
}
for item in latest_data
for item in sorted_keys # ← используем отсортированный порядок
],
}
accept = request.headers.get("accept", "")
if "text/html" in accept:
status_raw = str(STATUS).lower()
@@ -675,7 +916,7 @@ async def status(request: Request):
else:
gs_class = "unknown"
gs_text = "⚪ UNKNOWN"
html = f"""
<html>
<head>
@@ -732,11 +973,19 @@ async def status(request: Request):
for s in data["statuses"]:
status_text = str(s["status"]).strip().lower()
if any(x in status_text for x in ["ok", "success", "live", "live_soon", "online"]):
if any(
x in status_text
for x in ["ok", "success", "live", "live_soon", "online"]
):
color_class = "ok"
elif any(x in status_text for x in ["scheduled", "today_not_started", "upcoming"]):
elif any(
x in status_text for x in ["scheduled", "today_not_started", "upcoming"]
):
color_class = "warn"
elif any(x in status_text for x in ["result", "resultconfirmed", "finished", "finished_today"]):
elif any(
x in status_text
for x in ["result", "resultconfirmed", "finished", "finished_today"]
):
color_class = "fail"
else:
color_class = "unknown"
@@ -762,7 +1011,8 @@ async def status(request: Request):
response.headers["Refresh"] = "1"
return response
@app.get("/scores.json")
@app.get("/scores")
async def scores():
game = get_latest_game_safe()
if not game:
@@ -803,7 +1053,6 @@ async def scores():
return score_by_quarter
async def top_sorted_team(data):
top_sorted_team = sorted(
(p for p in data if p.get("startRole") in ["Player", ""]),
@@ -869,7 +1118,9 @@ async def team(who: str):
# нормализуем доступ к данным
game_data = game["data"] if "data" in game else game
result = game_data["result"] # здесь уже безопасно, мы проверили в get_latest_game_safe
result = game_data[
"result"
] # здесь уже безопасно, мы проверили в get_latest_game_safe
# в result ожидаем "teams"
teams = result.get("teams")
@@ -1069,6 +1320,7 @@ async def team(who: str):
return sorted_team
async def started_team(data):
started_team = sorted(
(
@@ -1285,7 +1537,7 @@ stat_name_list = [
]
@app.get("/team_stats.json")
@app.get("/team_stats")
async def team_stats():
teams = latest_data["game"]["data"]["result"]["teams"]
plays = latest_data["game"]["data"]["result"]["plays"]
@@ -1339,7 +1591,7 @@ async def team_stats():
return result_json
@app.get("/referee.json")
@app.get("/referee")
async def referee():
desired_order = [
"Crew chief",
@@ -1391,7 +1643,7 @@ async def referee():
return referees
@app.get("/team_comparison.json")
@app.get("/team_comparison")
async def team_comparison():
if STATUS not in ["no_game_today", "finished_today"]:
data = latest_data["pregame"]["data"]["result"]
@@ -1467,9 +1719,7 @@ async def team_comparison():
return [{"Данных о сравнении команд нет!"}]
@app.get("/standings.json")
@app.get("/standings")
async def regular_standings():
data = latest_data["actual-standings"]["data"]["items"]
for item in data:
@@ -1540,7 +1790,7 @@ async def regular_standings():
return standings_payload
@app.get("/live_status.json")
@app.get("/live_status")
async def live_status():
# если матч реально идёт/вот-вот — пытаемся отдать то, что есть
if STATUS in ["live", "live_soon"]:
@@ -1563,9 +1813,7 @@ async def live_status():
# 2) если это просто строка статуса ("ok" / "no-status" / "error")
if isinstance(raw, str):
return [{
"status": raw
}]
return [{"status": raw}]
# fallback
return [{"foulsA": 0, "foulsB": 0}]
@@ -1575,4 +1823,4 @@ async def live_status():
if __name__ == "__main__":
uvicorn.run("get_data:app", host="0.0.0.0", port=8000, reload=True)
uvicorn.run("get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="critical")