что то поменял, но нужно проверить

This commit is contained in:
2025-11-13 15:53:43 +03:00
parent 71f1e62630
commit 28b0f09ec2

View File

@@ -1,5 +1,5 @@
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import Response, JSONResponse, HTMLResponse
from fastapi.responses import Response, JSONResponse, HTMLResponse, StreamingResponse
from fastapi.encoders import jsonable_encoder
import pandas as pd
import requests, io, os
@@ -12,26 +12,30 @@ import time
from contextlib import asynccontextmanager
import numpy as np
import nasio
import logging
import logging.config
import platform
from pprint import pprint
# --- Глобальные переменные ---
selected_game_id: int | None = None
current_tournament_id: int | None = None # будем обновлять при загрузке расписания
current_season: str | None = None # будем обновлять при загрузке расписания
latest_game_data: dict | None = None # сюда кладём последние данные по матчу
current_tournament_id: int | None = None # будем обновлять при загрузке расписания
current_season: str | None = None # будем обновлять при загрузке расписания
latest_game_data: dict | None = None # сюда кладём последние данные по матчу
latest_game_error: str | None = None
_latest_lock = Lock()
_stop_event = Event()
_worker_thread: Thread | None = None
# Загружаем переменные из .env
if load_dotenv(dotenv_path="/mnt/khl/.env",verbose=True):
if load_dotenv(dotenv_path="/mnt/khl/.env", verbose=True):
print("Добавить в лог что был найден файл окружения!!")
pass
else:
load_dotenv()
print("Добавить в лог что не был найден файл окружения!!")
api_user = os.getenv("API_USER")
api_pass = os.getenv("API_PASS")
league = os.getenv("LEAGUE")
@@ -42,7 +46,6 @@ PASSWORD = os.getenv("SYNO_PASSWORD")
PATH = "/team-folders/GFX/Hockey/KHL/Soft/MATCH.xlsm"
def load_today_schedule():
"""Возвращает DataFrame матчей на сегодня с нужными колонками (или пустой DF)."""
url_tournaments = "http://stat2tv.khl.ru/tournaments.xml"
@@ -74,7 +77,17 @@ def load_today_schedule():
schedule_df = pd.read_xml(io.StringIO(r.text))
# Нужные колонки (скорректируй под реальные имена из XML)
needed_columns = ["id", "date", "time", "homeName_en", "visitorName_en", "arena_en", "arena_city_en", "homeCity_en", "visitorCity_en"]
needed_columns = [
"id",
"date",
"time",
"homeName_en",
"visitorName_en",
"arena_en",
"arena_city_en",
"homeCity_en",
"visitorCity_en",
]
exist = [c for c in needed_columns if c in schedule_df.columns]
schedule_df = schedule_df[exist].copy()
@@ -124,10 +137,7 @@ def _fetch_game_once(tournament_id: int, game_id: int) -> dict:
"""Один запрос к API матча -> чистый JSON из API."""
url = _build_game_url(tournament_id, game_id)
r = requests.get(
url,
auth=HTTPBasicAuth(api_user, api_pass),
verify=False,
timeout=10
url, auth=HTTPBasicAuth(api_user, api_pass), verify=False, timeout=10
)
r.raise_for_status()
@@ -183,17 +193,18 @@ async def lifespan(app: FastAPI):
_worker_thread.join(timeout=2)
print("🛑 Background thread stopped")
app = FastAPI(
lifespan=lifespan,
docs_url=None, # ❌ отключает /docs
redoc_url=None, # ❌ отключает /redoc
openapi_url=None # ❌ отключает /openapi.json
docs_url=None, # ❌ отключает /docs
redoc_url=None, # ❌ отключает /redoc
openapi_url=None, # ❌ отключает /openapi.json
)
@app.get("/games")
async def games():
df = load_today_schedule()
if df.empty:
return JSONResponse({"message": "Сегодня матчей нет"})
@@ -202,8 +213,6 @@ async def games():
return Response(content=json_schedule, media_type="application/json")
@app.get("/select")
async def select():
df = load_today_schedule()
@@ -336,13 +345,15 @@ async def get_selected_game():
async def game_url():
if not (selected_game_id and current_tournament_id):
return JSONResponse({"message": "game_id или tournament_id не задан"})
return JSONResponse({
"url": _build_game_url(current_tournament_id, selected_game_id),
"game_id": selected_game_id,
"tournament_id": current_tournament_id
})
return JSONResponse(
{
"url": _build_game_url(current_tournament_id, selected_game_id),
"game_id": selected_game_id,
"tournament_id": current_tournament_id,
}
)
# @app.get("/info")
# async def info():
# if selected_game_id:
@@ -356,7 +367,9 @@ async def game_data():
return JSONResponse(latest_game_data)
if latest_game_error:
return JSONResponse({"error": latest_game_error}, status_code=502)
return JSONResponse({"message": "Ещё нет данных. Выберите матч и подождите первое обновление."})
return JSONResponse(
{"message": "Ещё нет данных. Выберите матч и подождите первое обновление."}
)
@app.get("/referee")
@@ -383,15 +396,14 @@ async def referee():
return data_referees
async def team(who:str):
""""who: A - домашняя команда, B - гостевая"""
async def team(who: str):
""" "who: A - домашняя команда, B - гостевая"""
with _latest_lock:
lgd = latest_game_data
# print(lgd)
if not lgd or "data" not in lgd:
return [{"details":"Нет данных по матчу!"}]
return [{"details": "Нет данных по матчу!"}]
players1_temp = lgd["data"]["players"][who]
players1 = []
players1_f = []
@@ -426,7 +438,7 @@ async def team(who:str):
position = "forward"
elif players1_temp[player]["ps"] == "d":
position = "defenseman"
data_with_number = {
"number": player,
"NameSurnameGFX": names_new + " " + lastname1,
@@ -462,7 +474,7 @@ async def team(who:str):
}
players1_g.append(data_with_number2)
goaltenders1.append(data_with_number2)
def make_empty(example_list):
if not example_list:
return {}
@@ -479,20 +491,22 @@ async def team(who:str):
players1_f.append(empty_f.copy())
while len(players1_g) < 3:
players1_g.append(empty_g.copy())
players1 = players1_d + players1_f + players1_g
# print(len(players1))
return players1
@app.get("/team1")
async def team1():
return await team("A")
@app.get("/team2")
async def team2():
return await team("B")
# 👉 метка для первой строки (period row)
def _period_label(period: str | int) -> str:
s = str(period).strip().upper()
@@ -516,6 +530,7 @@ def _period_label(period: str | int) -> str:
return "FINAL"
return ""
# 👉 сортировка периодов: 1,2,3, затем OT/OT2/...
def _period_sort_key(k: str) -> tuple[int, int]:
s = str(k).strip().upper()
@@ -578,19 +593,25 @@ def format_team_stat(team1: dict, team2: dict, period: str | None = None) -> lis
teams = [{k: str(v) for k, v in t.items()} for t in [team1, team2]]
keys = list(teams[0].keys())
formatted = []
if period is not None:
formatted.append({
"name0": "period",
"name1": str(period),
"name2": "",
"StatParameterGFX": _period_label(period) or "Period"
})
formatted.append(
{
"name0": "period",
"name1": str(period),
"name2": "",
"StatParameterGFX": _period_label(period) or "Period",
}
)
for key in keys:
row = {"name0": key, "name1": teams[0].get(key, ""), "name2": teams[1].get(key, "")}
row = {
"name0": key,
"name1": teams[0].get(key, ""),
"name2": teams[1].get(key, ""),
}
# подписи
for code, eng, _ru in stat_list:
if key == code:
@@ -608,6 +629,7 @@ def format_team_stat(team1: dict, team2: dict, period: str | None = None) -> lis
def flip(s: str) -> str:
parts = s.split()
return f"{parts[1]} {parts[0]}" if len(parts) >= 2 else s
r["name1"] = flip(r["name1"])
r["name2"] = flip(r["name2"])
@@ -622,11 +644,13 @@ def _iter_period_pairs(teams_periods: dict):
"""
a = teams_periods.get("A", {})
b = teams_periods.get("B", {})
def _key(k):
try:
return int(k)
except (TypeError, ValueError):
return k
for k in sorted(a.keys(), key=_key):
if k in b:
yield k, a[k], b[k]
@@ -638,22 +662,20 @@ def _build_all_stats(payload: dict) -> dict:
"""
total_a = payload["teams"]["A"]
total_b = payload["teams"]["B"]
result = {
"total": format_team_stat(total_a, total_b),
"periods": []
}
result = {"total": format_team_stat(total_a, total_b), "periods": []}
for period_key, a_stat, b_stat in _iter_period_pairs(payload["teams_periods"]):
result["periods"].append({
"period": period_key,
"stats": format_team_stat(a_stat, b_stat)
})
result["periods"].append(
{"period": period_key, "stats": format_team_stat(a_stat, b_stat)}
)
return result
@app.get("/teams/stats")
async def teams_stats(
scope: str = Query("all", pattern="^(all|total|period)$"),
n: str | None = Query(None, description="Номер периода (строка или число) при scope=period"),
n: str | None = Query(
None, description="Номер периода (строка или число) при scope=period"
),
):
"""
Все-в-одном: GET /teams/stats?scope=all
@@ -671,7 +693,9 @@ async def teams_stats(
payload = lgd["data"]
if scope == "total":
data = format_team_stat(payload["teams"]["A"], payload["teams"]["B"], period="Total")
data = format_team_stat(
payload["teams"]["A"], payload["teams"]["B"], period="Total"
)
return JSONResponse({"scope": "total", "data": data})
if scope == "period":
@@ -688,21 +712,23 @@ async def teams_stats(
a = payload["teams_periods"]["A"].get(period_key)
b = payload["teams_periods"]["B"].get(period_key)
if a is None or b is None:
raise HTTPException(status_code=404, detail=f"Период {period_key} не найден.")
return JSONResponse({
"scope": "period",
"period": period_key,
"is_current": period_key == _current_period_key(payload),
"data": format_team_stat(a, b, period=period_key)
})
raise HTTPException(
status_code=404, detail=f"Период {period_key} не найден."
)
return JSONResponse(
{
"scope": "period",
"period": period_key,
"is_current": period_key == _current_period_key(payload),
"data": format_team_stat(a, b, period=period_key),
}
)
# scope == "all"
cur = _current_period_key(payload)
return JSONResponse({
"scope": "all",
"current_period": cur,
"data": _build_all_stats(payload)
})
return JSONResponse(
{"scope": "all", "current_period": cur, "data": _build_all_stats(payload)}
)
def _norm_name(s: str | None) -> str:
@@ -711,8 +737,9 @@ def _norm_name(s: str | None) -> str:
return ""
return str(s).strip().casefold()
@app.get("/info")
async def info():
def info():
# 1) Проверяем, выбран ли матч
global current_season
if not selected_game_id:
@@ -721,73 +748,100 @@ async def info():
# 2) Берём расписание и ищем строку по выбранному ID
df = load_today_schedule()
if df.empty:
return JSONResponse({"message": "Сегодня матчей нет", "selected_id": selected_game_id})
return JSONResponse(
{"message": "Сегодня матчей нет", "selected_id": selected_game_id}
)
# безопасно приводим id к int и ищем
try:
row = df.loc[df["id"].astype(int) == int(selected_game_id)].iloc[0]
except Exception:
return JSONResponse({"message": "Выбранный матч не найден в расписании на сегодня",
"selected_id": selected_game_id}, status_code=404)
return JSONResponse(
{
"message": "Выбранный матч не найден в расписании на сегодня",
"selected_id": selected_game_id,
},
status_code=404,
)
home_name = str(row.get("homeName_en", "")).strip()
away_name = str(row.get("visitorName_en", "")).strip()
# 3) Подтягиваем справочник команд из Excel (лист TEAMS)
teams_df = nasio.load_formatted(
user=USER,
password=PASSWORD,
nas_ip=SERVER_NAME,
nas_port="443",
path=PATH,
sheet="TEAMS"
)
# Оставляем только полезные поля (подгони под свой файл)
keep = [ "Team", "Logo", "Short", "HexPodl", "HexBase", "HexText" ]
keep = [c for c in keep if c in teams_df.columns]
teams_df = teams_df.loc[:, keep].copy()
# 4) Нормализованные ключи для джоина по имени
teams_df["__key"] = teams_df["Team"].apply(_norm_name)
def _pick_team_info(name: str) -> dict:
key = _norm_name(name)
hit = teams_df.loc[teams_df["__key"] == key]
if hit.empty:
# не нашли точное совпадение — вернём только название
return {"Team": name}
rec = hit.iloc[0].to_dict()
rec.pop("__key", None)
# заменим NaN/inf на None, чтобы JSON не падал
for k, v in list(rec.items()):
if pd.isna(v) or v in (np.inf, -np.inf):
rec[k] = None
return rec
home_info = _pick_team_info(home_name)
away_info = _pick_team_info(away_name)
date_obj = datetime.strptime(row.get("datetime_str", ""), "%d.%m.%Y %H:%M")
try:
full_format = date_obj.strftime("%B %-d, %Y")
except ValueError:
full_format = date_obj.strftime("%B %#d, %Y")
binary_bytes: bytes = nasio.load_bio(
user=USER,
password=PASSWORD,
nas_ip=SERVER_NAME,
nas_port="443",
path=PATH,
# sheet="TEAMS"
)
buf = io.BytesIO(binary_bytes)
headers = {
"Content-Length": str(len(binary_bytes)),
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
# можно подсунуть имя файла (если парсеру это важно)
"Content-Disposition": 'inline; filename="MATCH.xlsm"',
}
return StreamingResponse(
buf,
media_type="application/vnd.ms-excel.sheet.macroEnabled.12",
headers=headers,
)
# Оставляем только полезные поля (подгони под свой файл)
keep = ["Team", "Logo", "Short", "HexPodl", "HexBase", "HexText"]
keep = [c for c in keep if c in teams_df.columns]
teams_df = teams_df.loc[:, keep].copy()
payload = [{
"selected_id": int(selected_game_id),
"tournament_id": int(current_tournament_id) if current_tournament_id else None,
"datetime": str(full_format),
"arena": str(row.get("arena_en", "")),
"arena_city": str(row.get("arena_city_en", "")),
"home": home_info,
"home_city": str(row.get("homeCity_en", "")),
"away": away_info,
"away_city": str(row.get("visitorCity_en", "")),
"season": current_season,
}]
# 4) Нормализованные ключи для джоина по имени
teams_df["__key"] = teams_df["Team"].apply(_norm_name)
def _pick_team_info(name: str) -> dict:
key = _norm_name(name)
hit = teams_df.loc[teams_df["__key"] == key]
if hit.empty:
# не нашли точное совпадение — вернём только название
return {"Team": name}
rec = hit.iloc[0].to_dict()
rec.pop("__key", None)
# заменим NaN/inf на None, чтобы JSON не падал
for k, v in list(rec.items()):
if pd.isna(v) or v in (np.inf, -np.inf):
rec[k] = None
return rec
home_info = _pick_team_info(home_name)
away_info = _pick_team_info(away_name)
date_obj = datetime.strptime(row.get("datetime_str", ""), "%d.%m.%Y %H:%M")
try:
full_format = date_obj.strftime("%B %-d, %Y")
except ValueError:
full_format = date_obj.strftime("%B %#d, %Y")
payload = [
{
"selected_id": int(selected_game_id),
"tournament_id": (
int(current_tournament_id) if current_tournament_id else None
),
"datetime": str(full_format),
"arena": str(row.get("arena_en", "")),
"arena_city": str(row.get("arena_city_en", "")),
"home": home_info,
"home_city": str(row.get("homeCity_en", "")),
"away": away_info,
"away_city": str(row.get("visitorCity_en", "")),
"season": current_season,
}
]
return JSONResponse(content=payload)
except Exception as ex:
pprint(ex)
return JSONResponse(content=payload)
if __name__ == "__main__":
uvicorn.run(