1157 lines
46 KiB
Python
1157 lines
46 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from typing import Any, Callable
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from .auth_bridge import HockeyUser
|
|
from .service import HockeyDataService
|
|
from .vmix_portable import HockeyVmixPortable
|
|
|
|
|
|
class PublicSettingsPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
base_url: str | None = None
|
|
auth_mode: str | None = None
|
|
ui_language: str | None = None
|
|
fallback_language: str | None = None
|
|
store_ru: bool | None = None
|
|
store_en: bool | None = None
|
|
sync_interval_seconds: int | None = None
|
|
request_timeout_seconds: int | None = None
|
|
verify_ssl: bool | None = None
|
|
timezone: str | None = None
|
|
auto_sync_on_startup: bool | None = None
|
|
games_endpoint_template: str | None = None
|
|
games_auto_discovery: bool | None = None
|
|
games_cache_seconds: int | None = None
|
|
players_endpoint_template: str | None = None
|
|
player_countries_endpoint_template: str | None = None
|
|
referees_endpoint_template: str | None = None
|
|
match_json_endpoint_template: str | None = None
|
|
match_json_en_endpoint_template: str | None = None
|
|
shots_endpoint_template: str | None = None
|
|
match_directories_cache_seconds: int | None = None
|
|
standings_endpoint_template: str | None = None
|
|
standings_cache_seconds: int | None = None
|
|
powerplay_endpoint_template: str | None = None
|
|
rank_endpoint_template: str | None = None
|
|
tournament_statistics_cache_seconds: int | None = None
|
|
optional_resource_negative_cache_seconds: int | None = None
|
|
|
|
timer_period_minutes: int | None = None
|
|
timer_regular_overtime_minutes: int | None = None
|
|
timer_playoff_overtime_minutes: int | None = None
|
|
timer_reset_on_period_change: bool | None = None
|
|
strength_regulation_skaters: int | None = None
|
|
strength_regular_overtime_skaters: int | None = None
|
|
strength_playoff_overtime_skaters: int | None = None
|
|
strength_min_skaters: int | None = None
|
|
strength_regulation_display: str | None = None
|
|
strength_regular_overtime_display: str | None = None
|
|
strength_playoff_overtime_display: str | None = None
|
|
strength_powerplay_label: str | None = None
|
|
strength_shorthanded_label: str | None = None
|
|
strength_show_shorthanded: bool | None = None
|
|
strength_state_labels: dict[str, Any] | None = None
|
|
period_status_labels: dict[str, Any] | None = None
|
|
scoreboard_team_states: dict[str, Any] | None = None
|
|
|
|
|
|
class CredentialsPayload(BaseModel):
|
|
username: str | None = None
|
|
password: str | None = Field(default=None, max_length=1024)
|
|
clear_password: bool = False
|
|
|
|
|
|
class PreferencePayload(BaseModel):
|
|
display_language: str = "ru"
|
|
vmix_language: str = "ru"
|
|
tournament_external_id: str = ""
|
|
|
|
|
|
class TriggerPreferencePayload(BaseModel):
|
|
enabled: bool = True
|
|
|
|
|
|
class SessionPayload(BaseModel):
|
|
tournament_external_id: str = Field(min_length=1, max_length=64)
|
|
game_external_id: str = Field(default="", max_length=64)
|
|
display_language: str = "ru"
|
|
vmix_language: str = "ru"
|
|
vmix_device_id: str = Field(default="", max_length=128)
|
|
replace_existing: bool = True
|
|
|
|
|
|
class GamePeriodPayload(BaseModel):
|
|
current_period: str = Field(min_length=1, max_length=16)
|
|
language: str = Field(default="ru", pattern="^(ru|en)$")
|
|
|
|
|
|
class GameTimersPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
main_timer: dict[str, Any] = Field(default_factory=dict)
|
|
penalty_board: dict[str, Any] = Field(default_factory=dict)
|
|
language: str = Field(default="ru", pattern="^(ru|en)$")
|
|
|
|
|
|
class GameFlagsPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
flags: dict[str, bool] = Field(default_factory=dict)
|
|
language: str = Field(default="ru", pattern="^(ru|en)$")
|
|
|
|
|
|
class GameValuesPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
values: dict[str, Any] = Field(default_factory=dict)
|
|
language: str = Field(default="ru", pattern="^(ru|en)$")
|
|
|
|
|
|
class ShootoutSetupPayload(BaseModel):
|
|
initial_attempts: int = Field(default=3)
|
|
reset: bool = False
|
|
language: str = Field(default="ru", pattern="^(ru|en)$")
|
|
|
|
|
|
class ShootoutAttemptPayload(BaseModel):
|
|
side: str = Field(pattern="^(home|away)$")
|
|
player_external_id: str = Field(min_length=1, max_length=64)
|
|
scored: bool
|
|
language: str = Field(default="ru", pattern="^(ru|en)$")
|
|
|
|
|
|
class TeamDirectoryPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
external_id: str = Field(default="", max_length=64)
|
|
league_key: str = Field(default="", max_length=32)
|
|
tournament_external_id: str = Field(default="", max_length=64)
|
|
name_ru: str = Field(default="", max_length=500)
|
|
name_en: str = Field(default="", max_length=500)
|
|
short_name_ru: str = Field(default="", max_length=255)
|
|
short_name_en: str = Field(default="", max_length=255)
|
|
city_ru: str = Field(default="", max_length=255)
|
|
city_en: str = Field(default="", max_length=255)
|
|
logo_url: str = Field(default="", max_length=2000)
|
|
active: bool = True
|
|
|
|
|
|
class CountryDirectoryPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
external_id: str = Field(default="", max_length=64)
|
|
iso2: str = Field(default="", max_length=2)
|
|
iso3: str = Field(default="", max_length=3)
|
|
name_ru: str = Field(default="", max_length=255)
|
|
name_en: str = Field(default="", max_length=255)
|
|
active: bool = True
|
|
|
|
|
|
class PlayerDirectoryPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
external_id: str = Field(default="", max_length=64)
|
|
tournament_external_id: str = Field(default="", max_length=64)
|
|
first_name_ru: str = Field(default="", max_length=255)
|
|
first_name_en: str = Field(default="", max_length=255)
|
|
last_name_ru: str = Field(default="", max_length=255)
|
|
last_name_en: str = Field(default="", max_length=255)
|
|
full_name_ru: str = Field(default="", max_length=500)
|
|
full_name_en: str = Field(default="", max_length=500)
|
|
position_ru: str = Field(default="", max_length=128)
|
|
position_en: str = Field(default="", max_length=128)
|
|
birth_date: str = Field(default="", max_length=32)
|
|
height_cm: int = Field(default=0, ge=0, le=300)
|
|
weight_kg: int = Field(default=0, ge=0, le=300)
|
|
stick: str = Field(default="", max_length=32)
|
|
country_external_id: str = Field(default="", max_length=64)
|
|
country_code: str = Field(default="", max_length=8)
|
|
photo_url: str = Field(default="", max_length=2000)
|
|
club_external_id: str = Field(default="", max_length=64)
|
|
team_entry_external_id: str = Field(default="", max_length=64)
|
|
team_name_ru: str = Field(default="", max_length=255)
|
|
team_name_en: str = Field(default="", max_length=255)
|
|
jersey_number: str = Field(default="", max_length=16)
|
|
active: bool = True
|
|
|
|
|
|
class RefereeDirectoryPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
external_id: str = Field(default="", max_length=64)
|
|
first_name_ru: str = Field(default="", max_length=255)
|
|
first_name_en: str = Field(default="", max_length=255)
|
|
last_name_ru: str = Field(default="", max_length=255)
|
|
last_name_en: str = Field(default="", max_length=255)
|
|
full_name_ru: str = Field(default="", max_length=500)
|
|
full_name_en: str = Field(default="", max_length=500)
|
|
position_code: str = Field(default="", max_length=32)
|
|
number: str = Field(default="", max_length=16)
|
|
birth_date: str = Field(default="", max_length=32)
|
|
country_ru: str = Field(default="", max_length=255)
|
|
country_en: str = Field(default="", max_length=255)
|
|
country_code: str = Field(default="", max_length=8)
|
|
town_ru: str = Field(default="", max_length=255)
|
|
town_en: str = Field(default="", max_length=255)
|
|
active: bool = True
|
|
|
|
|
|
class CoachDirectoryPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
external_id: str = Field(default="", max_length=64)
|
|
tournament_external_id: str = Field(default="", max_length=64)
|
|
first_name_ru: str = Field(default="", max_length=255)
|
|
first_name_en: str = Field(default="", max_length=255)
|
|
last_name_ru: str = Field(default="", max_length=255)
|
|
last_name_en: str = Field(default="", max_length=255)
|
|
full_name_ru: str = Field(default="", max_length=500)
|
|
full_name_en: str = Field(default="", max_length=500)
|
|
birth_date: str = Field(default="", max_length=32)
|
|
country_external_id: str = Field(default="", max_length=64)
|
|
country_code: str = Field(default="", max_length=8)
|
|
photo_url: str = Field(default="", max_length=2000)
|
|
team_external_id: str = Field(default="", max_length=64)
|
|
team_name_ru: str = Field(default="", max_length=255)
|
|
team_name_en: str = Field(default="", max_length=255)
|
|
role_ru: str = Field(default="Главный тренер", max_length=128)
|
|
role_en: str = Field(default="Head coach", max_length=128)
|
|
active: bool = True
|
|
|
|
|
|
class PenaltyDirectoryPayload(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
code: str = Field(min_length=1, max_length=64)
|
|
name_ru: str = Field(min_length=1, max_length=500)
|
|
name_en: str = Field(min_length=1, max_length=500)
|
|
default_preset: str = Field(default="2", max_length=32)
|
|
team_penalty: bool = False
|
|
sort_order: int | None = Field(default=None, ge=0, le=100000)
|
|
active: bool = True
|
|
|
|
|
|
def create_hockey_router(
|
|
service: HockeyDataService,
|
|
*,
|
|
auth_dependency: Callable[..., Any] | None = None,
|
|
admin_dependency: Callable[..., Any] | None = None,
|
|
agent_hub: Any | None = None,
|
|
) -> APIRouter:
|
|
authenticated = [Depends(auth_dependency)] if auth_dependency else []
|
|
router = APIRouter(
|
|
prefix="/api/hockey",
|
|
tags=["Hockey / Stat2TV"],
|
|
dependencies=authenticated,
|
|
)
|
|
admin = [Depends(admin_dependency)] if admin_dependency else []
|
|
|
|
@router.get("/me")
|
|
async def current_user(
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"id": user.id,
|
|
"login": user.login,
|
|
"display_name": user.display_name,
|
|
"role": user.role,
|
|
"is_admin": user.is_admin,
|
|
}
|
|
|
|
@router.get("/vmix-channel")
|
|
async def current_vmix_channel(
|
|
request: Request,
|
|
language: str = Query("ru", pattern="^(ru|en|bilingual)$"),
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
channel = service.get_user_vmix_channel(user.id)
|
|
return {
|
|
"channel": channel,
|
|
"language": language,
|
|
"links": (
|
|
service.vmix_links(
|
|
token=str(channel.get("channel_key") or channel.get("token")),
|
|
base_url=str(request.base_url).rstrip("/"),
|
|
language=language,
|
|
)
|
|
if channel is not None
|
|
else {}
|
|
),
|
|
}
|
|
|
|
@router.get("/status")
|
|
async def status() -> dict[str, Any]:
|
|
return {
|
|
"settings": service.settings.status(),
|
|
"last_sync": service.last_sync(),
|
|
"database": service.database.schema_status(),
|
|
}
|
|
|
|
@router.get("/settings")
|
|
async def get_settings() -> dict[str, Any]:
|
|
return service.settings.status()
|
|
|
|
@router.get("/database/status")
|
|
async def database_status() -> dict[str, Any]:
|
|
return service.database.schema_status()
|
|
|
|
@router.get("/database/statistics-status")
|
|
async def database_statistics_status(
|
|
tournament_id: str = Query("", max_length=64),
|
|
) -> dict[str, Any]:
|
|
return service.normalized_storage_status(
|
|
tournament_external_id=tournament_id
|
|
)
|
|
|
|
@router.post("/database/statistics-backfill", dependencies=admin)
|
|
async def database_statistics_backfill() -> dict[str, Any]:
|
|
try:
|
|
return service.backfill_normalized_statistics()
|
|
except Exception as error:
|
|
raise HTTPException(status_code=500, detail=str(error)) from error
|
|
|
|
@router.post("/database/repair", dependencies=admin)
|
|
async def repair_database() -> dict[str, Any]:
|
|
try:
|
|
return service.database.ensure_game_schema()
|
|
except Exception as error:
|
|
raise HTTPException(status_code=500, detail=str(error)) from error
|
|
|
|
@router.put("/settings", dependencies=admin)
|
|
async def update_settings(payload: PublicSettingsPayload) -> dict[str, Any]:
|
|
values = {
|
|
key: value
|
|
for key, value in payload.model_dump().items()
|
|
if value is not None
|
|
}
|
|
return service.settings.update_public(values)
|
|
|
|
@router.put("/credentials", dependencies=admin)
|
|
async def update_credentials(payload: CredentialsPayload) -> dict[str, Any]:
|
|
return service.settings.update_credentials(
|
|
username=payload.username,
|
|
password=payload.password,
|
|
clear_password=payload.clear_password,
|
|
)
|
|
|
|
@router.post("/connection/test", dependencies=admin)
|
|
async def test_connection() -> dict[str, Any]:
|
|
try:
|
|
return await service.test_connection()
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/sync/tournaments", dependencies=admin)
|
|
async def sync_tournaments() -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_tournaments()
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/sync/tournaments/seed", dependencies=admin)
|
|
async def import_seed() -> dict[str, Any]:
|
|
if not service.seed_file.exists():
|
|
raise HTTPException(status_code=404, detail="Seed XML не найден")
|
|
return service.import_tournaments_file(service.seed_file, source="seed")
|
|
|
|
@router.get("/directories/teams")
|
|
async def directory_teams(
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
tournament_external_id: str = Query("", max_length=64),
|
|
league_key: str = Query("", max_length=32),
|
|
include_inactive: bool = False,
|
|
) -> dict[str, Any]:
|
|
return service.team_directory(
|
|
language=language,
|
|
tournament_external_id=tournament_external_id,
|
|
league_key=league_key,
|
|
include_inactive=include_inactive,
|
|
)
|
|
|
|
@router.post("/directories/teams/sync", dependencies=admin)
|
|
async def sync_directory_teams(
|
|
tournament_external_id: str = Query("", max_length=64),
|
|
) -> dict[str, Any]:
|
|
return service.sync_team_directory(
|
|
tournament_external_id=tournament_external_id,
|
|
)
|
|
|
|
@router.post("/directories/teams", dependencies=admin)
|
|
async def create_directory_team(
|
|
payload: TeamDirectoryPayload,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.save_directory_team(
|
|
payload.model_dump(),
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/directories/teams/{membership_id}", dependencies=admin)
|
|
async def update_directory_team(
|
|
membership_id: int,
|
|
payload: TeamDirectoryPayload,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.save_directory_team(
|
|
payload.model_dump(),
|
|
membership_id=membership_id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.get("/directories/countries")
|
|
async def directory_countries(
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
include_inactive: bool = False,
|
|
search: str = Query("", max_length=200),
|
|
) -> dict[str, Any]:
|
|
return service.country_directory(language=language, include_inactive=include_inactive, search=search)
|
|
|
|
@router.post("/directories/countries/sync", dependencies=admin)
|
|
async def sync_directory_countries() -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_country_directory()
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/directories/countries", dependencies=admin)
|
|
async def create_directory_country(payload: CountryDirectoryPayload, language: str = Query("ru", pattern="^(ru|en)$")) -> dict[str, Any]:
|
|
try:
|
|
return service.save_country(payload.model_dump(), language=language)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/directories/countries/{country_id}", dependencies=admin)
|
|
async def update_directory_country(country_id: int, payload: CountryDirectoryPayload, language: str = Query("ru", pattern="^(ru|en)$")) -> dict[str, Any]:
|
|
try:
|
|
return service.save_country(payload.model_dump(), country_id=country_id, language=language)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.get("/directories/players")
|
|
async def directory_players(
|
|
tournament_external_id: str = Query("", max_length=64),
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
include_inactive: bool = False,
|
|
search: str = Query("", max_length=200),
|
|
) -> dict[str, Any]:
|
|
return service.player_directory(tournament_external_id=tournament_external_id, language=language, include_inactive=include_inactive, search=search)
|
|
|
|
@router.post("/directories/players/sync", dependencies=admin)
|
|
async def sync_directory_players(tournament_external_id: str = Query(..., min_length=1, max_length=64)) -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_player_directory(tournament_external_id=tournament_external_id)
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/directories/players", dependencies=admin)
|
|
async def create_directory_player(payload: PlayerDirectoryPayload) -> dict[str, Any]:
|
|
try:
|
|
return service.save_player(payload.model_dump())
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/directories/players/{player_id}", dependencies=admin)
|
|
async def update_directory_player(player_id: int, payload: PlayerDirectoryPayload) -> dict[str, Any]:
|
|
try:
|
|
return service.save_player(payload.model_dump(), player_id=player_id)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.get("/directories/referees")
|
|
async def directory_referees(
|
|
tournament_external_id: str = Query("", max_length=64),
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
include_inactive: bool = False,
|
|
search: str = Query("", max_length=200),
|
|
) -> dict[str, Any]:
|
|
return service.referee_directory(tournament_external_id=tournament_external_id, language=language, include_inactive=include_inactive, search=search)
|
|
|
|
@router.post("/directories/referees/sync", dependencies=admin)
|
|
async def sync_directory_referees(tournament_external_id: str = Query(..., min_length=1, max_length=64)) -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_referee_directory(tournament_external_id=tournament_external_id)
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/directories/referees", dependencies=admin)
|
|
async def create_directory_referee(payload: RefereeDirectoryPayload) -> dict[str, Any]:
|
|
try:
|
|
return service.save_referee(payload.model_dump())
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/directories/referees/{referee_id}", dependencies=admin)
|
|
async def update_directory_referee(referee_id: int, payload: RefereeDirectoryPayload) -> dict[str, Any]:
|
|
try:
|
|
return service.save_referee(payload.model_dump(), referee_id=referee_id)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.get("/directories/coaches")
|
|
async def directory_coaches(
|
|
tournament_external_id: str = Query("", max_length=64),
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
include_inactive: bool = False,
|
|
search: str = Query("", max_length=200),
|
|
) -> dict[str, Any]:
|
|
return service.coach_directory(tournament_external_id=tournament_external_id, language=language, include_inactive=include_inactive, search=search)
|
|
|
|
@router.post("/directories/coaches/sync", dependencies=admin)
|
|
async def sync_directory_coaches(tournament_external_id: str = Query(..., min_length=1, max_length=64)) -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_coach_directory(tournament_external_id=tournament_external_id)
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/directories/coaches", dependencies=admin)
|
|
async def create_directory_coach(payload: CoachDirectoryPayload) -> dict[str, Any]:
|
|
try:
|
|
return service.save_coach(payload.model_dump())
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/directories/coaches/{coach_id}", dependencies=admin)
|
|
async def update_directory_coach(coach_id: int, payload: CoachDirectoryPayload) -> dict[str, Any]:
|
|
try:
|
|
return service.save_coach(payload.model_dump(), coach_id=coach_id)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.get("/directories/penalties")
|
|
async def directory_penalties(
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
include_inactive: bool = False,
|
|
) -> dict[str, Any]:
|
|
return service.penalty_directory(
|
|
language=language,
|
|
include_inactive=include_inactive,
|
|
)
|
|
|
|
@router.post("/directories/penalties", dependencies=admin)
|
|
async def create_directory_penalty(
|
|
payload: PenaltyDirectoryPayload,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.save_penalty_type(
|
|
payload.model_dump(),
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/directories/penalties/{penalty_id}", dependencies=admin)
|
|
async def update_directory_penalty(
|
|
penalty_id: int,
|
|
payload: PenaltyDirectoryPayload,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.save_penalty_type(
|
|
payload.model_dump(),
|
|
penalty_id=penalty_id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.get("/navigation")
|
|
async def navigation(
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
on_date: date = Query(default_factory=date.today),
|
|
search: str = Query("", max_length=200),
|
|
league: str = Query("", max_length=32),
|
|
season: str = Query("", max_length=32),
|
|
stage: str = Query("", max_length=32),
|
|
limit: int = Query(600, ge=1, le=2000),
|
|
) -> dict[str, Any]:
|
|
return service.navigation(
|
|
language=language,
|
|
on_date=on_date,
|
|
search=search,
|
|
league=league,
|
|
season=season,
|
|
stage=stage,
|
|
limit=limit,
|
|
)
|
|
|
|
|
|
@router.get("/games")
|
|
async def games_by_date(
|
|
on_date: date = Query(default_factory=date.today),
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
return service.games(
|
|
on_date=on_date,
|
|
language=language,
|
|
)
|
|
|
|
@router.get("/tournaments/{external_id}/games")
|
|
async def tournament_games(
|
|
external_id: str,
|
|
on_date: date = Query(default_factory=date.today),
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
all_dates: bool = False,
|
|
) -> dict[str, Any]:
|
|
if service.tournament(external_id, language=language) is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
return service.games(
|
|
tournament_external_id=external_id,
|
|
on_date=on_date,
|
|
language=language,
|
|
all_dates=all_dates,
|
|
)
|
|
|
|
@router.post("/tournaments/{external_id}/games/sync")
|
|
async def sync_tournament_games(
|
|
external_id: str,
|
|
on_date: date = Query(default_factory=date.today),
|
|
force_discovery: bool = False,
|
|
lightweight: bool = False,
|
|
) -> dict[str, Any]:
|
|
if service.tournament(external_id, language="ru") is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
try:
|
|
return await service.sync_games(
|
|
tournament_external_id=external_id,
|
|
on_date=on_date,
|
|
force_discovery=force_discovery,
|
|
lightweight=lightweight,
|
|
)
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.get("/tournaments/{external_id}/standings")
|
|
async def tournament_standings(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
game_id: str = Query("", max_length=64),
|
|
) -> dict[str, Any]:
|
|
value = service.standings(
|
|
external_id,
|
|
language=language,
|
|
game_external_id=game_id,
|
|
)
|
|
if value is None:
|
|
raise HTTPException(status_code=404, detail="Турнирная таблица ещё не загружена")
|
|
return value
|
|
|
|
@router.post("/tournaments/{external_id}/standings/sync")
|
|
async def sync_tournament_standings(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
game_id: str = Query("", max_length=64),
|
|
) -> dict[str, Any]:
|
|
if service.tournament(external_id, language=language) is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
try:
|
|
return await service.sync_standings(
|
|
external_id,
|
|
language=language,
|
|
game_external_id=game_id,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
|
|
@router.get("/tournaments/{external_id}/statistics")
|
|
async def tournament_statistics(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
if service.tournament(external_id, language=language) is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
return service.tournament_statistics(external_id, language=language)
|
|
|
|
@router.get("/tournaments/{external_id}/navigation-labels")
|
|
async def tournament_navigation_labels(
|
|
external_id: str,
|
|
force: bool = False,
|
|
) -> dict[str, Any]:
|
|
if service.tournament(external_id, language="ru") is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
return await service.statistic_navigation_labels(external_id, force=force)
|
|
|
|
@router.post("/tournaments/{external_id}/statistics/sync")
|
|
async def sync_tournament_statistics(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
force: bool = False,
|
|
) -> dict[str, Any]:
|
|
if service.tournament(external_id, language=language) is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
try:
|
|
return await service.sync_tournament_statistics(
|
|
external_id, language=language, force=force
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.get(
|
|
"/tournaments/{external_id}/games/diagnostics",
|
|
dependencies=admin,
|
|
)
|
|
async def games_diagnostics(external_id: str) -> dict[str, Any]:
|
|
return service.endpoint_diagnostics(
|
|
tournament_external_id=external_id
|
|
)
|
|
|
|
@router.post("/games/{external_id}/live")
|
|
async def refresh_selected_game(
|
|
external_id: str,
|
|
tournament_id: str = Query(..., min_length=1, max_length=64),
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_live_game(
|
|
tournament_external_id=tournament_id,
|
|
game_external_id=external_id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=404, detail=str(error)) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/games/manual/{game_external_id}")
|
|
async def sync_manual_game_global(
|
|
game_external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_game_by_id_global(
|
|
game_external_id=game_external_id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=404, detail=str(error)) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/tournaments/{tournament_external_id}/games/manual/{game_external_id}")
|
|
async def sync_manual_game(
|
|
tournament_external_id: str,
|
|
game_external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
if service.tournament(tournament_external_id, language=language) is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
try:
|
|
return await service.sync_game_by_id(
|
|
tournament_external_id=tournament_external_id,
|
|
game_external_id=game_external_id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=404, detail=str(error)) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.post("/games/{external_id}/details/sync")
|
|
async def sync_game_details(
|
|
external_id: str,
|
|
tournament_id: str = Query(..., min_length=1, max_length=64),
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return await service.sync_selected_game_details(
|
|
tournament_external_id=tournament_id,
|
|
game_external_id=external_id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=404, detail=str(error)) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=502, detail=str(error)) from error
|
|
|
|
@router.get("/games/{external_id}/details")
|
|
async def game_details(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
value = service.game_details(external_id, language=language)
|
|
if value is None:
|
|
raise HTTPException(status_code=404, detail="Матч не найден")
|
|
return value
|
|
|
|
@router.get("/games/{external_id}/control")
|
|
async def game_control(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
value = service.game_control(external_id, language=language)
|
|
if value is None:
|
|
raise HTTPException(status_code=404, detail="Матч не найден")
|
|
return value
|
|
|
|
@router.put("/games/{external_id}/control/period")
|
|
async def update_game_period(
|
|
external_id: str,
|
|
payload: GamePeriodPayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.set_game_period(
|
|
external_id,
|
|
current_period=payload.current_period,
|
|
updated_by=user.id,
|
|
language=payload.language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/games/{external_id}/control/timers")
|
|
async def update_game_timers(
|
|
external_id: str,
|
|
payload: GameTimersPayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.save_game_timers(
|
|
external_id,
|
|
main_timer=payload.main_timer,
|
|
penalty_board=payload.penalty_board,
|
|
updated_by=user.id,
|
|
language=payload.language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/games/{external_id}/control/flags")
|
|
async def update_game_flags(
|
|
external_id: str,
|
|
payload: GameFlagsPayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.set_game_flags(
|
|
external_id,
|
|
flags=payload.flags,
|
|
updated_by=user.id,
|
|
language=payload.language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/games/{external_id}/control/values")
|
|
async def update_game_values(
|
|
external_id: str,
|
|
payload: GameValuesPayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.set_game_values(
|
|
external_id,
|
|
values=payload.values,
|
|
updated_by=user.id,
|
|
language=payload.language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.put("/games/{external_id}/shootout/setup")
|
|
async def setup_shootout(
|
|
external_id: str,
|
|
payload: ShootoutSetupPayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
if payload.initial_attempts not in {3, 5}:
|
|
raise HTTPException(status_code=400, detail="Допустимо 3 или 5 буллитов")
|
|
try:
|
|
return service.configure_shootout(
|
|
external_id,
|
|
initial_attempts=payload.initial_attempts,
|
|
reset=payload.reset,
|
|
updated_by=user.id,
|
|
language=payload.language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.post("/games/{external_id}/shootout/round")
|
|
async def add_shootout_round(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.add_shootout_round(
|
|
external_id,
|
|
updated_by=user.id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.post("/games/{external_id}/shootout/attempts")
|
|
async def add_shootout_attempt(
|
|
external_id: str,
|
|
payload: ShootoutAttemptPayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.add_shootout_attempt(
|
|
external_id,
|
|
side=payload.side,
|
|
player_external_id=payload.player_external_id,
|
|
scored=payload.scored,
|
|
created_by=user.id,
|
|
language=payload.language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
@router.delete("/games/{external_id}/shootout/attempts/{attempt_id}")
|
|
async def delete_shootout_attempt(
|
|
external_id: str,
|
|
attempt_id: int,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
return service.delete_shootout_attempt(
|
|
external_id,
|
|
attempt_id,
|
|
updated_by=user.id,
|
|
language=language,
|
|
)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=404, detail=str(error)) from error
|
|
|
|
@router.get("/games/{external_id}")
|
|
async def game(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
value = service.game(external_id, language=language)
|
|
if value is None:
|
|
raise HTTPException(status_code=404, detail="Матч не найден")
|
|
return value
|
|
|
|
@router.get("/tournaments/{external_id}")
|
|
async def tournament(
|
|
external_id: str,
|
|
language: str = Query("ru", pattern="^(ru|en)$"),
|
|
) -> dict[str, Any]:
|
|
value = service.tournament(external_id, language=language)
|
|
if value is None:
|
|
raise HTTPException(status_code=404, detail="Турнир не найден")
|
|
return value
|
|
|
|
@router.get("/preferences")
|
|
async def user_preference(
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
return service.user_preference(wfl_user_id=user.id)
|
|
|
|
@router.put("/preferences")
|
|
async def save_preference(
|
|
payload: PreferencePayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
return service.save_preference(
|
|
wfl_user_id=user.id,
|
|
**payload.model_dump(),
|
|
)
|
|
|
|
@router.put("/preferences/toasts")
|
|
async def save_toasts_preference(
|
|
payload: TriggerPreferencePayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
return service.save_toasts_preference(
|
|
wfl_user_id=user.id,
|
|
enabled=payload.enabled,
|
|
)
|
|
|
|
@router.put("/preferences/events")
|
|
async def save_events_preference(
|
|
payload: TriggerPreferencePayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
return service.save_toasts_preference(
|
|
wfl_user_id=user.id,
|
|
enabled=payload.enabled,
|
|
)
|
|
|
|
@router.post("/sessions")
|
|
async def open_session(
|
|
payload: SessionPayload,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
try:
|
|
session_data = payload.model_dump()
|
|
vmix_device_id = str(session_data.pop("vmix_device_id", "") or "").strip()
|
|
result = service.open_session(
|
|
wfl_user_id=user.id,
|
|
login_snapshot=user.login,
|
|
display_name_snapshot=user.display_name,
|
|
vmix_device_uuid=vmix_device_id,
|
|
**session_data,
|
|
)
|
|
if agent_hub is not None and result.get("game_external_id"):
|
|
try:
|
|
result["agent_assignment"] = await agent_hub.assign_match(
|
|
wfl_user_id=user.id,
|
|
game_external_id=str(result.get("game_external_id") or ""),
|
|
tournament_external_id=str(result.get("tournament_external_id") or ""),
|
|
device_id=vmix_device_id,
|
|
operator_session_token=str(result.get("token") or ""),
|
|
)
|
|
except Exception as agent_error:
|
|
# The match/session must remain usable even when a browser
|
|
# carries a stale Agent id from another WFL account.
|
|
result["agent_assignment"] = None
|
|
result["agent_warning"] = str(agent_error)[:300]
|
|
return result
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=409, detail=str(error)) from error
|
|
|
|
@router.get("/sessions/{token}")
|
|
async def get_session(
|
|
token: str,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
value = service.get_session(token)
|
|
if value is None or value["wfl_user_id"] != user.id:
|
|
raise HTTPException(status_code=404, detail="Сессия не найдена")
|
|
return value
|
|
|
|
@router.delete("/sessions/{token}")
|
|
async def close_session(
|
|
token: str,
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
if not service.close_session(token, wfl_user_id=user.id):
|
|
raise HTTPException(status_code=404, detail="Сессия не найдена")
|
|
return {"ok": True}
|
|
|
|
@router.get("/sessions/{token}/vmix-links")
|
|
async def vmix_links(
|
|
token: str,
|
|
request: Request,
|
|
language: str = Query("ru", pattern="^(ru|en|bilingual)$"),
|
|
user: HockeyUser = Depends(auth_dependency),
|
|
) -> dict[str, Any]:
|
|
operator_session = service.get_session(token)
|
|
if operator_session is None or operator_session["wfl_user_id"] != user.id:
|
|
raise HTTPException(status_code=404, detail="Сессия не найдена")
|
|
return {
|
|
"token": token, # legacy field
|
|
"channel_key": token,
|
|
"channel_owner": operator_session.get("channel_owner", ""),
|
|
"language": language,
|
|
"links": service.vmix_links(
|
|
token=token,
|
|
base_url=str(request.base_url).rstrip("/"),
|
|
language=language,
|
|
),
|
|
}
|
|
|
|
return router
|
|
|
|
|
|
def create_hockey_vmix_router(
|
|
service: HockeyDataService,
|
|
portable: HockeyVmixPortable | None = None,
|
|
) -> APIRouter:
|
|
portable = portable or HockeyVmixPortable(service)
|
|
router = APIRouter(tags=["Hockey vMix JSON"])
|
|
|
|
def translate_error(error: Exception) -> HTTPException:
|
|
if isinstance(error, KeyError):
|
|
return HTTPException(status_code=404, detail=str(error).strip("'"))
|
|
return HTTPException(status_code=400, detail=str(error))
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/info.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/info.json")
|
|
async def info(token: str, language: str) -> dict[str, Any]:
|
|
try:
|
|
return portable.info_payload(token, language)
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/scoreboard.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/scoreboard.json")
|
|
async def scoreboard(token: str, language: str) -> dict[str, Any]:
|
|
try:
|
|
return portable.scoreboard_payload(token, language)
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/home-lineup.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/home-lineup.json")
|
|
async def home_lineup(token: str, language: str) -> dict[str, Any]:
|
|
try:
|
|
return portable.line_up_payload(token, language, "home")
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/away-lineup.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/away-lineup.json")
|
|
async def away_lineup(token: str, language: str) -> dict[str, Any]:
|
|
try:
|
|
return portable.line_up_payload(token, language, "away")
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/officials.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/officials.json")
|
|
async def officials(token: str, language: str) -> dict[str, Any]:
|
|
try:
|
|
return portable.officials_payload(token, language)
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/penalties.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/penalties.json")
|
|
async def penalties(token: str, language: str) -> dict[str, Any]:
|
|
try:
|
|
return portable.penalties_payload(token, language)
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/events.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/events.json")
|
|
async def events(token: str, language: str) -> dict[str, Any]:
|
|
try:
|
|
return portable.events_payload(token, language)
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
|
|
@router.get("/vmix/hockey/session/{token}/{language}/custom/{json_key}.json", include_in_schema=False)
|
|
@router.get("/vmix/hockey/channel/{token}/{language}/custom/{json_key}.json")
|
|
async def custom_json(
|
|
token: str,
|
|
language: str,
|
|
json_key: str,
|
|
limit: int | None = Query(None, ge=0, le=1000),
|
|
) -> list[dict[str, Any]]:
|
|
try:
|
|
return portable.configured_output(token, language, json_key, limit=limit)
|
|
except (KeyError, ValueError) as error:
|
|
raise translate_error(error) from error
|
|
except Exception as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Ошибка сборки vMix JSON {json_key}: {error}",
|
|
) from error
|
|
|
|
return router
|