Compare commits

..

14 Commits

Author SHA1 Message Date
Alexey Barabanov
5994e18efa Не ябучий 3й питон!!! 2025-11-13 17:42:29 +03:00
Alexey Barabanov
5205dd5c5c lgkllhdfg 2025-11-13 17:40:52 +03:00
Alexey Barabanov
ead3683e4c коминт 2025-11-13 17:26:09 +03:00
Alexey Barabanov
8f300e0154 ОПЕЧАТКА 2025-11-13 16:48:10 +03:00
6e92022e67 коммит 2025-11-13 15:57:12 +03:00
74801ad710 Merge branch 'main' of https://git.tvstart.ru/ychernenko/KHL 2025-11-13 15:54:42 +03:00
28b0f09ec2 что то поменял, но нужно проверить 2025-11-13 15:53:43 +03:00
Alexey Barabanov
c60caaa8aa Jgtxfnrf 2025-11-13 15:30:27 +03:00
Alexey Barabanov
9cbf32f831 Испровление опечатки 2025-11-13 15:30:09 +03:00
Alexey Barabanov
d4d9584bb3 Поменяли взоиможействие с файлом окружения.
Поменяли что береться из файла окружения
2025-11-13 15:28:14 +03:00
Alexey Barabanov
216138ceed Изменения в содании сервиса.
Добавлены:
Лимиты
Логирование
Поведение при перезапуске
Работа с виртуальным окрыжением и файлом окружения
2025-11-13 15:09:50 +03:00
Юрий Черненко
0d06097181 версия с чтением и передачей бинарного файла 2025-11-11 19:46:41 +03:00
71f1e62630 поправил gitignore 2025-11-11 15:18:41 +03:00
d120630548 Remove __pycache__ from history 2025-11-11 15:17:56 +03:00
5 changed files with 246 additions and 141 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,8 @@
.env .env
/.venv /.venv
logs/
/logs/* /logs/*
/__pycache__/* __pycache__/
*.pyc *.pyc
*.pyd
*.pyo

View File

@@ -0,0 +1,4 @@
## Проверка логов
```shell
sudo journalctl -t KHL
```

Binary file not shown.

View File

@@ -11,6 +11,7 @@ NC='\033[0m' # No Color
REPO_URL="https://git.tvstart.ru/ychernenko/KHL.git" REPO_URL="https://git.tvstart.ru/ychernenko/KHL.git"
TARGET_DIR="/root/KHL" TARGET_DIR="/root/KHL"
SERVICE_NAME="khl-data.service" SERVICE_NAME="khl-data.service"
TARGET_ENV="/mnt/khl/.env"
show_help() { show_help() {
echo "Использование: $0 -r <релиз> [-h]" echo "Использование: $0 -r <релиз> [-h]"
@@ -41,7 +42,7 @@ log_debug() {
# Функция проверки зависимостей системы # Функция проверки зависимостей системы
check_dependencies() { check_dependencies() {
local deps=("git" "python3" "pip3" "netstat" "systemctl") local deps=("git" "python3" "pip" "netstat" "systemctl")
local missing=() local missing=()
for dep in "${deps[@]}"; do for dep in "${deps[@]}"; do
@@ -77,13 +78,13 @@ install_packages() {
exit 1 exit 1
fi fi
if ! command -v pip3 &> /dev/null; then if ! command -v pip &> /dev/null; then
log_error "pip3 не установлен!" log_error "pip не установлен!"
exit 1 exit 1
fi fi
log_info "Версия Python: $(python3 --version)" log_info "Версия Python: $(python3 --version)"
log_info "Версия pip: $(pip3 --version)" log_info "Версия pip: $(pip --version)"
} }
# Функция загрузки кода # Функция загрузки кода
@@ -206,25 +207,38 @@ create_systemd_service() {
fi fi
# Формируем команду для data сервиса # Формируем команду для data сервиса
local data_command="$TARGET_DIR/.venv/bin/python3 $TARGET_DIR/get_data.py"
local data_service_file="/etc/systemd/system/$SERVICE_NAME" local data_service_file="/etc/systemd/system/$SERVICE_NAME"
log_info "Создание файла сервиса: $data_service_file" log_info "Создание файла сервиса: $data_service_file"
cat > "$data_service_file" << EOF cat > "$data_service_file" << EOF
[Unit] [Unit]
Description=KHL Data Service Description=KHL Data Service
Documentation=https://git.tvstart.ru/ychernenko/KHL
After=network.target After=network.target
Wants=network.target
[Service] [Service]
Type=simple Type=simple
User=root User=root
WorkingDirectory=$TARGET_DIR WorkingDirectory=$TARGET_DIR
Environment=PATH=$TARGET_DIR/.venv/bin Environment=PATH=$TARGET_DIR/.venv/bin
ExecStart=$data_command EnvironmentFile=$TARGET_ENV
Restart=always ExecStart=python3 $TARGET_DIR/get_data.py
RestartSec=30
# Лимиты ресурсов
MemoryMax=1G
CPUQuota=80%
# Логирование
StandardOutput=journal StandardOutput=journal
StandardError=journal StandardError=journal
SyslogIdentifier=KHL
# Поведение при перезапуске
Restart=always
RestartSec=10
StartLimitInterval=300
StartLimitBurst=5
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
@@ -489,6 +503,7 @@ main() {
log_info "" log_info ""
log_info "Для просмотра логов:" log_info "Для просмотра логов:"
log_info " journalctl -u $SERVICE_NAME -f" log_info " journalctl -u $SERVICE_NAME -f"
log_info " journalctl -t KHL -f"
log_info "" log_info ""
log_info "Управление сервисом:" log_info "Управление сервисом:"
log_info " Перезапуск: systemctl restart $SERVICE_NAME" log_info " Перезапуск: systemctl restart $SERVICE_NAME"

View File

@@ -1,5 +1,5 @@
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import Response, JSONResponse, HTMLResponse from fastapi.responses import Response, JSONResponse, HTMLResponse, StreamingResponse
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
import pandas as pd import pandas as pd
import requests, io, os import requests, io, os
@@ -12,6 +12,11 @@ import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import numpy as np import numpy as np
import nasio import nasio
import logging
import logging.config
import platform
import json
from pprint import pprint
# --- Глобальные переменные --- # --- Глобальные переменные ---
@@ -24,28 +29,20 @@ _latest_lock = Lock()
_stop_event = Event() _stop_event = Event()
_worker_thread: Thread | None = None _worker_thread: Thread | None = None
# Загружаем переменные из .env pprint(f"Локальный файл окружения ={load_dotenv(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_user = os.getenv("API_USER")
api_pass = os.getenv("API_PASS") api_pass = os.getenv("API_PASS")
league = os.getenv("LEAGUE") league = os.getenv("LEAGUE")
api_base_url = os.getenv("API_BASE_URL")
POLL_SEC = int(os.getenv("GAME_POLL_SECONDS")) POLL_SEC = int(os.getenv("GAME_POLL_SECONDS"))
SERVER_NAME = os.getenv("SYNO_URL") SERVER_NAME = os.getenv("SYNO_URL")
USER = os.getenv("SYNO_USERNAME") USER = os.getenv("SYNO_USERNAME")
PASSWORD = os.getenv("SYNO_PASSWORD") PASSWORD = os.getenv("SYNO_PASSWORD")
PATH = "/team-folders/GFX/Hockey/KHL/Soft/MATCH.xlsm" PATH = f'{os.getenv("SYNO_PATH")}MATCH.xlsm'
def load_today_schedule(): def load_today_schedule():
"""Возвращает DataFrame матчей на сегодня с нужными колонками (или пустой DF).""" """Возвращает DataFrame матчей на сегодня с нужными колонками (или пустой DF)."""
url_tournaments = "http://stat2tv.khl.ru/tournaments.xml" url_tournaments = f"{api_base_url}tournaments.xml"
r = requests.get( r = requests.get(
url_tournaments, auth=HTTPBasicAuth(api_user, api_pass), verify=False url_tournaments, auth=HTTPBasicAuth(api_user, api_pass), verify=False
) )
@@ -69,12 +66,22 @@ def load_today_schedule():
global current_tournament_id, current_season global current_tournament_id, current_season
current_tournament_id = tournament_id current_tournament_id = tournament_id
current_season = season current_season = season
url_schedule = f"http://stat2tv.khl.ru/{tournament_id}/schedule-{tournament_id}.xml" url_schedule = f"{api_base_url}{tournament_id}/schedule-{tournament_id}.xml"
r = requests.get(url_schedule, auth=HTTPBasicAuth(api_user, api_pass), verify=False) r = requests.get(url_schedule, auth=HTTPBasicAuth(api_user, api_pass), verify=False)
schedule_df = pd.read_xml(io.StringIO(r.text)) schedule_df = pd.read_xml(io.StringIO(r.text))
# Нужные колонки (скорректируй под реальные имена из XML) # Нужные колонки (скорректируй под реальные имена из 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] exist = [c for c in needed_columns if c in schedule_df.columns]
schedule_df = schedule_df[exist].copy() schedule_df = schedule_df[exist].copy()
@@ -117,17 +124,14 @@ def load_today_schedule():
def _build_game_url(tournament_id: int, game_id: int) -> str: def _build_game_url(tournament_id: int, game_id: int) -> str:
# URL по аналогии с расписанием: .../{tournament_id}/json_en/{game_id}.json # URL по аналогии с расписанием: .../{tournament_id}/json_en/{game_id}.json
# Если у тебя другой шаблон — просто поменяй строку ниже. # Если у тебя другой шаблон — просто поменяй строку ниже.
return f"http://stat2tv.khl.ru/{tournament_id}/json_en/{game_id}.json" return f"{api_base_url}{tournament_id}/json_en/{game_id}.json"
def _fetch_game_once(tournament_id: int, game_id: int) -> dict: def _fetch_game_once(tournament_id: int, game_id: int) -> dict:
"""Один запрос к API матча -> чистый JSON из API.""" """Один запрос к API матча -> чистый JSON из API."""
url = _build_game_url(tournament_id, game_id) url = _build_game_url(tournament_id, game_id)
r = requests.get( r = requests.get(
url, url, auth=HTTPBasicAuth(api_user, api_pass), verify=False, timeout=10
auth=HTTPBasicAuth(api_user, api_pass),
verify=False,
timeout=10
) )
r.raise_for_status() r.raise_for_status()
@@ -183,11 +187,12 @@ async def lifespan(app: FastAPI):
_worker_thread.join(timeout=2) _worker_thread.join(timeout=2)
print("🛑 Background thread stopped") print("🛑 Background thread stopped")
app = FastAPI( app = FastAPI(
lifespan=lifespan, lifespan=lifespan,
docs_url=None, # ❌ отключает /docs docs_url=None, # ❌ отключает /docs
redoc_url=None, # ❌ отключает /redoc redoc_url=None, # ❌ отключает /redoc
openapi_url=None # ❌ отключает /openapi.json openapi_url=None, # ❌ отключает /openapi.json
) )
@@ -202,8 +207,6 @@ async def games():
return Response(content=json_schedule, media_type="application/json") return Response(content=json_schedule, media_type="application/json")
@app.get("/select") @app.get("/select")
async def select(): async def select():
df = load_today_schedule() df = load_today_schedule()
@@ -336,11 +339,13 @@ async def get_selected_game():
async def game_url(): async def game_url():
if not (selected_game_id and current_tournament_id): if not (selected_game_id and current_tournament_id):
return JSONResponse({"message": "game_id или tournament_id не задан"}) return JSONResponse({"message": "game_id или tournament_id не задан"})
return JSONResponse({ return JSONResponse(
{
"url": _build_game_url(current_tournament_id, selected_game_id), "url": _build_game_url(current_tournament_id, selected_game_id),
"game_id": selected_game_id, "game_id": selected_game_id,
"tournament_id": current_tournament_id "tournament_id": current_tournament_id,
}) }
)
# @app.get("/info") # @app.get("/info")
@@ -356,7 +361,9 @@ async def game_data():
return JSONResponse(latest_game_data) return JSONResponse(latest_game_data)
if latest_game_error: if latest_game_error:
return JSONResponse({"error": latest_game_error}, status_code=502) return JSONResponse({"error": latest_game_error}, status_code=502)
return JSONResponse({"message": "Ещё нет данных. Выберите матч и подождите первое обновление."}) return JSONResponse(
{"message": "Ещё нет данных. Выберите матч и подождите первое обновление."}
)
@app.get("/referee") @app.get("/referee")
@@ -383,7 +390,6 @@ async def referee():
return data_referees return data_referees
async def team(who: str): async def team(who: str):
""" "who: A - домашняя команда, B - гостевая""" """ "who: A - домашняя команда, B - гостевая"""
with _latest_lock: with _latest_lock:
@@ -489,10 +495,12 @@ async def team(who:str):
async def team1(): async def team1():
return await team("A") return await team("A")
@app.get("/team2") @app.get("/team2")
async def team2(): async def team2():
return await team("B") return await team("B")
# 👉 метка для первой строки (period row) # 👉 метка для первой строки (period row)
def _period_label(period: str | int) -> str: def _period_label(period: str | int) -> str:
s = str(period).strip().upper() s = str(period).strip().upper()
@@ -516,6 +524,7 @@ def _period_label(period: str | int) -> str:
return "FINAL" return "FINAL"
return "" return ""
# 👉 сортировка периодов: 1,2,3, затем OT/OT2/... # 👉 сортировка периодов: 1,2,3, затем OT/OT2/...
def _period_sort_key(k: str) -> tuple[int, int]: def _period_sort_key(k: str) -> tuple[int, int]:
s = str(k).strip().upper() s = str(k).strip().upper()
@@ -582,15 +591,21 @@ def format_team_stat(team1: dict, team2: dict, period: str | None = None) -> lis
formatted = [] formatted = []
if period is not None: if period is not None:
formatted.append({ formatted.append(
{
"name0": "period", "name0": "period",
"name1": str(period), "name1": str(period),
"name2": "", "name2": "",
"StatParameterGFX": _period_label(period) or "Period" "StatParameterGFX": _period_label(period) or "Period",
}) }
)
for key in keys: 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: for code, eng, _ru in stat_list:
if key == code: if key == code:
@@ -608,6 +623,7 @@ def format_team_stat(team1: dict, team2: dict, period: str | None = None) -> lis
def flip(s: str) -> str: def flip(s: str) -> str:
parts = s.split() parts = s.split()
return f"{parts[1]} {parts[0]}" if len(parts) >= 2 else s return f"{parts[1]} {parts[0]}" if len(parts) >= 2 else s
r["name1"] = flip(r["name1"]) r["name1"] = flip(r["name1"])
r["name2"] = flip(r["name2"]) r["name2"] = flip(r["name2"])
@@ -622,11 +638,13 @@ def _iter_period_pairs(teams_periods: dict):
""" """
a = teams_periods.get("A", {}) a = teams_periods.get("A", {})
b = teams_periods.get("B", {}) b = teams_periods.get("B", {})
def _key(k): def _key(k):
try: try:
return int(k) return int(k)
except (TypeError, ValueError): except (TypeError, ValueError):
return k return k
for k in sorted(a.keys(), key=_key): for k in sorted(a.keys(), key=_key):
if k in b: if k in b:
yield k, a[k], b[k] yield k, a[k], b[k]
@@ -638,22 +656,20 @@ def _build_all_stats(payload: dict) -> dict:
""" """
total_a = payload["teams"]["A"] total_a = payload["teams"]["A"]
total_b = payload["teams"]["B"] total_b = payload["teams"]["B"]
result = { result = {"total": format_team_stat(total_a, total_b), "periods": []}
"total": format_team_stat(total_a, total_b),
"periods": []
}
for period_key, a_stat, b_stat in _iter_period_pairs(payload["teams_periods"]): for period_key, a_stat, b_stat in _iter_period_pairs(payload["teams_periods"]):
result["periods"].append({ result["periods"].append(
"period": period_key, {"period": period_key, "stats": format_team_stat(a_stat, b_stat)}
"stats": format_team_stat(a_stat, b_stat) )
})
return result return result
@app.get("/teams/stats") @app.get("/teams/stats")
async def teams_stats( async def teams_stats(
scope: str = Query("all", pattern="^(all|total|period)$"), 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 Все-в-одном: GET /teams/stats?scope=all
@@ -671,7 +687,9 @@ async def teams_stats(
payload = lgd["data"] payload = lgd["data"]
if scope == "total": 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}) return JSONResponse({"scope": "total", "data": data})
if scope == "period": if scope == "period":
@@ -688,21 +706,23 @@ async def teams_stats(
a = payload["teams_periods"]["A"].get(period_key) a = payload["teams_periods"]["A"].get(period_key)
b = payload["teams_periods"]["B"].get(period_key) b = payload["teams_periods"]["B"].get(period_key)
if a is None or b is None: if a is None or b is None:
raise HTTPException(status_code=404, detail=f"Период {period_key} не найден.") raise HTTPException(
return JSONResponse({ status_code=404, detail=f"Период {period_key} не найден."
)
return JSONResponse(
{
"scope": "period", "scope": "period",
"period": period_key, "period": period_key,
"is_current": period_key == _current_period_key(payload), "is_current": period_key == _current_period_key(payload),
"data": format_team_stat(a, b, period=period_key) "data": format_team_stat(a, b, period=period_key),
}) }
)
# scope == "all" # scope == "all"
cur = _current_period_key(payload) cur = _current_period_key(payload)
return JSONResponse({ return JSONResponse(
"scope": "all", {"scope": "all", "current_period": cur, "data": _build_all_stats(payload)}
"current_period": cur, )
"data": _build_all_stats(payload)
})
def _norm_name(s: str | None) -> str: def _norm_name(s: str | None) -> str:
@@ -711,8 +731,16 @@ def _norm_name(s: str | None) -> str:
return "" return ""
return str(s).strip().casefold() return str(s).strip().casefold()
def _load_buf():
buf = nasio.load_bio(user=USER, password=PASSWORD,
nas_ip=SERVER_NAME, nas_port="443", path=PATH)
if isinstance(buf, (bytes, bytearray, memoryview)):
buf = io.BytesIO(buf)
buf.seek(0)
return buf
@app.get("/info") @app.get("/info")
async def info(): async def info(format: str = "xlsx", sheet: str = "TEAMS"):
# 1) Проверяем, выбран ли матч # 1) Проверяем, выбран ли матч
global current_season global current_season
if not selected_game_id: if not selected_game_id:
@@ -721,73 +749,128 @@ async def info():
# 2) Берём расписание и ищем строку по выбранному ID # 2) Берём расписание и ищем строку по выбранному ID
df = load_today_schedule() df = load_today_schedule()
if df.empty: if df.empty:
return JSONResponse({"message": "Сегодня матчей нет", "selected_id": selected_game_id}) return JSONResponse(
{"message": "Сегодня матчей нет", "selected_id": selected_game_id}
)
# безопасно приводим id к int и ищем # безопасно приводим id к int и ищем
try: try:
row = df.loc[df["id"].astype(int) == int(selected_game_id)].iloc[0] row = df.loc[df["id"].astype(int) == int(selected_game_id)].iloc[0]
except Exception: except Exception:
return JSONResponse({"message": "Выбранный матч не найден в расписании на сегодня", return JSONResponse(
"selected_id": selected_game_id}, status_code=404) {
"message": "Выбранный матч не найден в расписании на сегодня",
"selected_id": selected_game_id,
},
status_code=404,
)
home_name = str(row.get("homeName_en", "")).strip() home_name = str(row.get("homeName_en", "")).strip()
away_name = str(row.get("visitorName_en", "")).strip() away_name = str(row.get("visitorName_en", "")).strip()
# 3) Подтягиваем справочник команд из Excel (лист TEAMS) # 3) Подтягиваем справочник команд из Excel (лист TEAMS)
teams_df = nasio.load_formatted( src = _load_buf()
user=USER,
password=PASSWORD, if format == "xlsx":
nas_ip=SERVER_NAME, # читаем нужный лист из исходного XLSM
nas_port="443", df = pd.read_excel(src, sheet_name=sheet, engine="openpyxl")
path=PATH,
sheet="TEAMS" # пишем НОВЫЙ XLSX (без макросов) — это то, что понимает vMix
out = io.BytesIO()
with pd.ExcelWriter(out, engine="openpyxl") as writer:
df.to_excel(writer, sheet_name=sheet, index=False)
out.seek(0)
return StreamingResponse(
out,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
# стабильное имя файла, чтобы vMix не путался
"Content-Disposition": "inline; filename=vmix.xlsx",
# отключаем кэш браузера/прокси, vMix сам опрашивает по интервалу
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
) )
# Оставляем только полезные поля (подгони под свой файл) elif format == "csv":
keep = [ "Team", "Logo", "Short", "HexPodl", "HexBase", "HexText" ] df = pd.read_excel(src, sheet_name=sheet, engine="openpyxl")
keep = [c for c in keep if c in teams_df.columns] csv_bytes = df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8")
teams_df = teams_df.loc[:, keep].copy() return StreamingResponse(
io.BytesIO(csv_bytes),
media_type="text/csv; charset=utf-8",
headers={
"Content-Disposition": "inline; filename=vmix.csv",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
)
# 4) Нормализованные ключи для джоина по имени elif format == "json":
teams_df["__key"] = teams_df["Team"].apply(_norm_name) df = pd.read_excel(src, sheet_name=sheet, engine="openpyxl")
payload = json.dumps(df.to_dict(orient="records"), ensure_ascii=False)
return Response(
content=payload,
media_type="application/json; charset=utf-8",
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
)
def _pick_team_info(name: str) -> dict: return Response("Unsupported format", status_code=400)
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) # keep = ["Team", "Logo", "Short", "HexPodl", "HexBase", "HexText"]
date_obj = datetime.strptime(row.get("datetime_str", ""), "%d.%m.%Y %H:%M") # keep = [c for c in keep if c in teams_df.columns]
try: # teams_df = teams_df.loc[:, keep].copy()
full_format = date_obj.strftime("%B %-d, %Y")
except ValueError:
full_format = date_obj.strftime("%B %#d, %Y")
# # 4) Нормализованные ключи для джоина по имени
# teams_df["__key"] = teams_df["Team"].apply(_norm_name)
payload = [{ # def _pick_team_info(name: str) -> dict:
"selected_id": int(selected_game_id), # key = _norm_name(name)
"tournament_id": int(current_tournament_id) if current_tournament_id else None, # hit = teams_df.loc[teams_df["__key"] == key]
"datetime": str(full_format), # if hit.empty:
"arena": str(row.get("arena_en", "")), # # не нашли точное совпадение — вернём только название
"arena_city": str(row.get("arena_city_en", "")), # return {"Team": name}
"home": home_info, # rec = hit.iloc[0].to_dict()
"home_city": str(row.get("homeCity_en", "")), # rec.pop("__key", None)
"away": away_info, # # заменим NaN/inf на None, чтобы JSON не падал
"away_city": str(row.get("visitorCity_en", "")), # for k, v in list(rec.items()):
"season": current_season, # 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__": if __name__ == "__main__":
uvicorn.run( uvicorn.run(