Add global ranking page

This commit is contained in:
devRaGonSa
2026-06-08 19:14:29 +02:00
parent 05d9939a23
commit dbfa28f8a4
13 changed files with 1441 additions and 232 deletions

View File

@@ -50,6 +50,7 @@ from .historical_storage import (
)
from .rcon_historical_read_model import get_rcon_historical_match_detail
from .rcon_annual_rankings import get_annual_ranking_snapshot
from .rcon_historical_leaderboards import list_rcon_materialized_leaderboard
from .rcon_historical_player_stats import search_rcon_materialized_players
from .rcon_historical_player_stats import get_rcon_materialized_player_stats
from .normalizers import normalize_map_name
@@ -480,6 +481,41 @@ def _to_iso_or_none(value: object) -> str | None:
return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def _normalize_public_server_id(server_id: str | None) -> str:
normalized = str(server_id or "").strip().lower()
if not normalized or normalized == "all":
return ALL_SERVERS_SLUG
return str(server_id).strip()
def _serialize_public_server_id(server_id: object) -> str:
normalized = str(server_id or "").strip()
if not normalized or normalized == ALL_SERVERS_SLUG:
return "all"
return normalized
def _normalize_global_ranking_items(items: object) -> list[dict[str, object]]:
normalized_items: list[dict[str, object]] = []
for item in items if isinstance(items, list) else []:
if not isinstance(item, dict):
continue
normalized_items.append(
{
"ranking_position": int(item.get("ranking_position") or 0),
"player_id": item.get("player_id"),
"player_name": item.get("player_name"),
"metric_value": int(item.get("metric_value") or 0),
"matches_considered": int(item.get("matches_considered") or 0),
"kills": int(item.get("kills") or 0),
"deaths": int(item.get("deaths") or 0),
"teamkills": int(item.get("teamkills") or 0),
"kd_ratio": float(item.get("kd_ratio") or 0.0),
}
)
return normalized_items
def _source_when_present(*values: object, source: str) -> str | None:
return source if any(value is not None for value in values) else None
@@ -713,6 +749,95 @@ def build_annual_ranking_snapshot_payload(
}
def build_global_ranking_payload(
*,
timeframe: str = "weekly",
server_id: str | None = None,
metric: str = "kills",
limit: int = 20,
year: int | None = None,
) -> dict[str, object]:
"""Return the dedicated Ranking page payload without changing Stats contracts."""
normalized_timeframe = str(timeframe or "weekly").strip().lower()
normalized_server_id = _normalize_public_server_id(server_id)
if normalized_timeframe == "annual":
if year is None:
raise ValueError("year is required when timeframe=annual")
result = get_annual_ranking_snapshot(
year=year,
server_key=normalized_server_id,
metric=metric,
limit=limit,
)
return {
"status": "ok",
"data": {
"page_kind": "global-ranking",
"title": "Ranking global anual",
"context": "global-ranking-annual",
"timeframe": "annual",
"server_id": _serialize_public_server_id(result.get("server_id")),
"metric": result.get("metric"),
"limit": int(result.get("limit") or 0),
"requested_limit": int(result.get("requested_limit") or 0),
"effective_limit": int(result.get("effective_limit") or 0),
"year": int(result.get("year") or year),
"window_start": result.get("window_start"),
"window_end": result.get("window_end"),
"window_kind": "annual-snapshot",
"window_label": "Anual",
"snapshot_status": result.get("snapshot_status"),
"snapshot_limit": result.get("snapshot_limit"),
"item_count": int(result.get("item_count") or 0),
"source_matches_count": int(result.get("source_matches_count") or 0),
"source": {
"primary_source": "rcon",
"read_model": "rcon-annual-ranking-snapshot",
"generated_at": result.get("generated_at"),
"freshness": (
"snapshot" if result.get("snapshot_status") == "ready" else "missing"
),
},
"items": _normalize_global_ranking_items(result.get("items")),
},
}
result = list_rcon_materialized_leaderboard(
server_key=normalized_server_id,
timeframe=normalized_timeframe,
metric=metric,
limit=limit,
)
return {
"status": "ok",
"data": {
"page_kind": "global-ranking",
"title": "Ranking global",
"context": f"global-ranking-{normalized_timeframe}",
"timeframe": normalized_timeframe,
"server_id": _serialize_public_server_id(result.get("server_key")),
"metric": result.get("metric"),
"limit": int(result.get("limit") or limit),
"requested_limit": int(limit),
"effective_limit": int(result.get("limit") or limit),
"window_start": result.get("window_start"),
"window_end": result.get("window_end"),
"window_kind": result.get("window_kind"),
"window_label": result.get("window_label"),
"selection_reason": result.get("selection_reason"),
"snapshot_status": "ready",
"source": {
"primary_source": "rcon",
"read_model": "rcon-materialized-admin-log-leaderboard",
"generated_at": _utc_timestamp_now(),
"freshness": "runtime",
},
"items": _normalize_global_ranking_items(result.get("items")),
},
}
def build_weekly_leaderboard_payload(
*,
limit: int = 10,

View File

@@ -8,6 +8,7 @@ from urllib.parse import parse_qs, urlparse
from .config import get_historical_data_source_kind
from .payloads import (
build_global_ranking_payload,
build_stats_player_profile_payload,
build_community_payload,
build_current_match_kill_feed_payload,
@@ -111,6 +112,38 @@ def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object]
except ValueError as error:
return HTTPStatus.BAD_REQUEST, build_error_payload(str(error))
if parsed.path == "/api/ranking":
params = parse_qs(parsed.query)
timeframe = params.get("timeframe", ["weekly"])[0]
if timeframe not in {"weekly", "monthly", "annual"}:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid timeframe parameter")
metric = params.get("metric", ["kills"])[0]
if metric != "kills":
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid metric parameter")
limit = _parse_limit(parsed.query)
if limit is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter")
server_id = params.get("server_id", [None])[0]
if server_id is None:
server_id = params.get("server", [None])[0]
if not _is_supported_ranking_server_id(server_id):
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid server_id parameter")
year = None
if timeframe == "annual":
year = _parse_required_year(parsed.query)
if year is None:
return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid year parameter")
try:
return HTTPStatus.OK, build_global_ranking_payload(
timeframe=timeframe,
server_id=server_id,
metric=metric,
limit=limit,
year=year,
)
except ValueError as error:
return HTTPStatus.BAD_REQUEST, build_error_payload(str(error))
if parsed.path == "/api/current-match":
server_slug = parse_qs(parsed.query).get("server", [None])[0]
if not server_slug:
@@ -471,8 +504,28 @@ def _parse_year(query: str) -> int | None:
return year
def _parse_required_year(query: str) -> int | None:
params = parse_qs(query)
if "year" not in params:
return None
return _parse_year(query)
def _parse_limit_with_default(query: str, default: int = 20) -> int | None:
params = parse_qs(query)
if "limit" not in params:
return default
return _parse_limit(query)
def _is_supported_ranking_server_id(server_id: str | None) -> bool:
if server_id is None:
return True
normalized = str(server_id).strip().lower()
return normalized in {
"",
"all",
"all-servers",
"comunidad-hispana-01",
"comunidad-hispana-02",
}