diff --git a/get_data.py b/get_data.py index cba2592..e0a0b0b 100644 --- a/get_data.py +++ b/get_data.py @@ -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(