Fix
This commit is contained in:
842
backend/app/historical_snapshots.py
Normal file
842
backend/app/historical_snapshots.py
Normal file
@@ -0,0 +1,842 @@
|
||||
"""Definitions for persisted precomputed historical snapshots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from .config import get_database_url, get_historical_data_source_kind
|
||||
from .data_sources import SOURCE_KIND_RCON, get_rcon_historical_read_model
|
||||
from .historical_storage import (
|
||||
ALL_SERVERS_SLUG,
|
||||
list_historical_server_summaries,
|
||||
list_historical_servers,
|
||||
list_monthly_leaderboard,
|
||||
list_monthly_mvp_ranking,
|
||||
list_monthly_mvp_v2_ranking,
|
||||
list_recent_historical_matches,
|
||||
list_weekly_leaderboard,
|
||||
)
|
||||
from .player_event_aggregates import (
|
||||
list_death_by,
|
||||
list_most_killed,
|
||||
list_net_duel_summaries,
|
||||
list_teamkill_summaries,
|
||||
list_weapon_kills,
|
||||
)
|
||||
from .player_event_storage import initialize_player_event_storage
|
||||
|
||||
SNAPSHOT_TYPE_SERVER_SUMMARY = "server-summary"
|
||||
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD = "weekly-leaderboard"
|
||||
SNAPSHOT_TYPE_MONTHLY_LEADERBOARD = "monthly-leaderboard"
|
||||
SNAPSHOT_TYPE_MONTHLY_MVP = "monthly-mvp"
|
||||
SNAPSHOT_TYPE_MONTHLY_MVP_V2 = "monthly-mvp-v2"
|
||||
SNAPSHOT_TYPE_RECENT_MATCHES = "recent-matches"
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED = "player-event-most-killed"
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY = "player-event-death-by"
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS = "player-event-duels"
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS = "player-event-weapon-kills"
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS = "player-event-teamkills"
|
||||
|
||||
SUPPORTED_SNAPSHOT_TYPES = frozenset(
|
||||
{
|
||||
SNAPSHOT_TYPE_SERVER_SUMMARY,
|
||||
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||
SNAPSHOT_TYPE_MONTHLY_LEADERBOARD,
|
||||
SNAPSHOT_TYPE_MONTHLY_MVP,
|
||||
SNAPSHOT_TYPE_MONTHLY_MVP_V2,
|
||||
SNAPSHOT_TYPE_RECENT_MATCHES,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS,
|
||||
}
|
||||
)
|
||||
|
||||
SUPPORTED_LEADERBOARD_METRICS = frozenset(
|
||||
{
|
||||
"kills",
|
||||
"deaths",
|
||||
"support",
|
||||
"matches_over_100_kills",
|
||||
}
|
||||
)
|
||||
PREWARM_SNAPSHOT_SERVER_KEYS = (
|
||||
"comunidad-hispana-01",
|
||||
"comunidad-hispana-02",
|
||||
ALL_SERVERS_SLUG,
|
||||
)
|
||||
PREWARM_LEADERBOARD_METRICS = ("kills",)
|
||||
SNAPSHOT_LEADERBOARD_METRICS = (
|
||||
"kills",
|
||||
"deaths",
|
||||
"matches_over_100_kills",
|
||||
"support",
|
||||
)
|
||||
PLAYER_EVENT_SNAPSHOT_TYPES = (
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS,
|
||||
)
|
||||
|
||||
DEFAULT_SNAPSHOT_WINDOW = "all-time"
|
||||
DEFAULT_WEEKLY_SNAPSHOT_WINDOW = "7d"
|
||||
DEFAULT_MONTHLY_SNAPSHOT_WINDOW = "month"
|
||||
DEFAULT_WEEKLY_LEADERBOARD_LIMIT = 10
|
||||
DEFAULT_RECENT_MATCHES_LIMIT = 20
|
||||
|
||||
|
||||
def validate_snapshot_identity(
|
||||
*,
|
||||
snapshot_type: str,
|
||||
metric: str | None = None,
|
||||
) -> None:
|
||||
"""Validate the persisted snapshot selectors accepted by the storage layer."""
|
||||
if snapshot_type not in SUPPORTED_SNAPSHOT_TYPES:
|
||||
raise ValueError(f"Unsupported historical snapshot type: {snapshot_type}")
|
||||
|
||||
if snapshot_type in {
|
||||
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||
SNAPSHOT_TYPE_MONTHLY_LEADERBOARD,
|
||||
}:
|
||||
if metric not in SUPPORTED_LEADERBOARD_METRICS:
|
||||
raise ValueError(f"Unsupported historical snapshot metric: {metric}")
|
||||
return
|
||||
|
||||
if metric is not None:
|
||||
raise ValueError(
|
||||
"Metric is only supported for weekly-leaderboard and monthly-leaderboard."
|
||||
)
|
||||
|
||||
|
||||
def list_snapshot_server_keys(*, db_path: Path | None = None) -> list[str]:
|
||||
"""Return the historical server slugs that should receive persisted snapshots."""
|
||||
server_keys = [
|
||||
str(item["slug"])
|
||||
for item in list_historical_servers(db_path=db_path)
|
||||
if item.get("slug")
|
||||
]
|
||||
server_keys.append(ALL_SERVERS_SLUG)
|
||||
return server_keys
|
||||
|
||||
|
||||
def build_historical_server_snapshots(
|
||||
*,
|
||||
server_key: str,
|
||||
generated_at: datetime | None = None,
|
||||
leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT,
|
||||
recent_matches_limit: int = DEFAULT_RECENT_MATCHES_LIMIT,
|
||||
db_path: Path | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
"""Build all precomputed historical snapshots required for one server."""
|
||||
generated_at_value = _as_utc(generated_at or datetime.now(timezone.utc))
|
||||
leaderboard_limit = _normalize_snapshot_limit("leaderboard_limit", leaderboard_limit)
|
||||
recent_matches_limit = _normalize_snapshot_limit(
|
||||
"recent_matches_limit",
|
||||
recent_matches_limit,
|
||||
)
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_SERVER_SUMMARY)
|
||||
snapshots = [_build_server_summary_snapshot(server_key, generated_at_value, db_path=db_path)]
|
||||
|
||||
for metric in SNAPSHOT_LEADERBOARD_METRICS:
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_WEEKLY_LEADERBOARD, metric=metric)
|
||||
snapshots.append(
|
||||
_build_weekly_leaderboard_snapshot(
|
||||
server_key,
|
||||
metric,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_MONTHLY_LEADERBOARD, metric=metric)
|
||||
snapshots.append(
|
||||
_build_monthly_leaderboard_snapshot(
|
||||
server_key,
|
||||
metric,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_MONTHLY_MVP)
|
||||
snapshots.append(
|
||||
_build_monthly_mvp_snapshot(
|
||||
server_key,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_MONTHLY_MVP_V2)
|
||||
snapshots.append(
|
||||
_build_monthly_mvp_v2_snapshot(
|
||||
server_key,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
for snapshot_type in PLAYER_EVENT_SNAPSHOT_TYPES:
|
||||
_log_snapshot_build_started(server_key, snapshot_type)
|
||||
snapshots.append(
|
||||
_build_player_event_snapshot(
|
||||
server_key,
|
||||
snapshot_type,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_RECENT_MATCHES)
|
||||
snapshots.append(
|
||||
_build_recent_matches_snapshot(
|
||||
server_key,
|
||||
generated_at_value,
|
||||
limit=recent_matches_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
return snapshots
|
||||
|
||||
|
||||
def build_priority_historical_snapshots(
|
||||
*,
|
||||
server_keys: tuple[str, ...] = PREWARM_SNAPSHOT_SERVER_KEYS,
|
||||
generated_at: datetime | None = None,
|
||||
leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT,
|
||||
recent_matches_limit: int = DEFAULT_RECENT_MATCHES_LIMIT,
|
||||
db_path: Path | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
"""Build the minimum warm snapshot set required by the historical UI."""
|
||||
generated_at_value = _as_utc(generated_at or datetime.now(timezone.utc))
|
||||
leaderboard_limit = _normalize_snapshot_limit("leaderboard_limit", leaderboard_limit)
|
||||
recent_matches_limit = _normalize_snapshot_limit(
|
||||
"recent_matches_limit",
|
||||
recent_matches_limit,
|
||||
)
|
||||
snapshots: list[dict[str, object]] = []
|
||||
for server_key in server_keys:
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_SERVER_SUMMARY)
|
||||
snapshots.append(
|
||||
_build_server_summary_snapshot(server_key, generated_at_value, db_path=db_path)
|
||||
)
|
||||
for metric in PREWARM_LEADERBOARD_METRICS:
|
||||
_log_snapshot_build_started(
|
||||
server_key,
|
||||
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||
metric=metric,
|
||||
)
|
||||
snapshots.append(
|
||||
_build_weekly_leaderboard_snapshot(
|
||||
server_key,
|
||||
metric,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
_log_snapshot_build_started(
|
||||
server_key,
|
||||
SNAPSHOT_TYPE_MONTHLY_LEADERBOARD,
|
||||
metric=metric,
|
||||
)
|
||||
snapshots.append(
|
||||
_build_monthly_leaderboard_snapshot(
|
||||
server_key,
|
||||
metric,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_MONTHLY_MVP)
|
||||
snapshots.append(
|
||||
_build_monthly_mvp_snapshot(
|
||||
server_key,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_MONTHLY_MVP_V2)
|
||||
snapshots.append(
|
||||
_build_monthly_mvp_v2_snapshot(
|
||||
server_key,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
for snapshot_type in PLAYER_EVENT_SNAPSHOT_TYPES:
|
||||
_log_snapshot_build_started(server_key, snapshot_type)
|
||||
snapshots.append(
|
||||
_build_player_event_snapshot(
|
||||
server_key,
|
||||
snapshot_type,
|
||||
generated_at_value,
|
||||
limit=leaderboard_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
_log_snapshot_build_started(server_key, SNAPSHOT_TYPE_RECENT_MATCHES)
|
||||
snapshots.append(
|
||||
_build_recent_matches_snapshot(
|
||||
server_key,
|
||||
generated_at_value,
|
||||
limit=recent_matches_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
return snapshots
|
||||
|
||||
|
||||
def build_all_historical_snapshots(
|
||||
*,
|
||||
server_key: str | None = None,
|
||||
generated_at: datetime | None = None,
|
||||
leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT,
|
||||
recent_matches_limit: int = DEFAULT_RECENT_MATCHES_LIMIT,
|
||||
db_path: Path | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
"""Build the full snapshot set for one server or for all configured servers."""
|
||||
target_server_keys = _resolve_snapshot_target_keys(server_key=server_key, db_path=db_path)
|
||||
snapshots: list[dict[str, object]] = []
|
||||
for target_server_key in target_server_keys:
|
||||
snapshots.extend(
|
||||
build_historical_server_snapshots(
|
||||
server_key=target_server_key,
|
||||
generated_at=generated_at,
|
||||
leaderboard_limit=leaderboard_limit,
|
||||
recent_matches_limit=recent_matches_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
)
|
||||
return snapshots
|
||||
|
||||
|
||||
def generate_and_persist_historical_snapshots(
|
||||
*,
|
||||
server_key: str | None = None,
|
||||
generated_at: datetime | None = None,
|
||||
leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT,
|
||||
recent_matches_limit: int = DEFAULT_RECENT_MATCHES_LIMIT,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Build and persist precomputed snapshots for one server or all servers."""
|
||||
from .historical_snapshot_storage import persist_historical_snapshot_batch
|
||||
|
||||
generated_at_value = _as_utc(generated_at or datetime.now(timezone.utc))
|
||||
snapshots = build_all_historical_snapshots(
|
||||
server_key=server_key,
|
||||
generated_at=generated_at_value,
|
||||
leaderboard_limit=leaderboard_limit,
|
||||
recent_matches_limit=recent_matches_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
persisted_records = persist_historical_snapshot_batch(snapshots, db_path=db_path)
|
||||
snapshots_by_server: dict[str, int] = {}
|
||||
for record in persisted_records:
|
||||
snapshots_by_server.setdefault(record.server_key, 0)
|
||||
snapshots_by_server[record.server_key] += 1
|
||||
|
||||
return {
|
||||
"generated_at": _to_iso(generated_at_value),
|
||||
"server_slug": server_key,
|
||||
"snapshot_policy": "full-matrix",
|
||||
"snapshot_count": len(persisted_records),
|
||||
"servers_processed": len(snapshots_by_server),
|
||||
"snapshots_by_server": snapshots_by_server,
|
||||
}
|
||||
|
||||
|
||||
def generate_and_persist_priority_historical_snapshots(
|
||||
*,
|
||||
generated_at: datetime | None = None,
|
||||
leaderboard_limit: int = DEFAULT_WEEKLY_LEADERBOARD_LIMIT,
|
||||
recent_matches_limit: int = DEFAULT_RECENT_MATCHES_LIMIT,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Build and persist the priority snapshot set used for prewarm."""
|
||||
from .historical_snapshot_storage import persist_historical_snapshot_batch
|
||||
|
||||
generated_at_value = _as_utc(generated_at or datetime.now(timezone.utc))
|
||||
snapshots = build_priority_historical_snapshots(
|
||||
generated_at=generated_at_value,
|
||||
leaderboard_limit=leaderboard_limit,
|
||||
recent_matches_limit=recent_matches_limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
persisted_records = persist_historical_snapshot_batch(snapshots, db_path=db_path)
|
||||
snapshots_by_server: dict[str, int] = {}
|
||||
for record in persisted_records:
|
||||
snapshots_by_server.setdefault(record.server_key, 0)
|
||||
snapshots_by_server[record.server_key] += 1
|
||||
|
||||
return {
|
||||
"generated_at": _to_iso(generated_at_value),
|
||||
"server_slug": None,
|
||||
"snapshot_policy": "priority-prewarm",
|
||||
"prewarm_server_keys": list(PREWARM_SNAPSHOT_SERVER_KEYS),
|
||||
"prewarm_metrics": list(PREWARM_LEADERBOARD_METRICS),
|
||||
"snapshot_count": len(persisted_records),
|
||||
"servers_processed": len(snapshots_by_server),
|
||||
"snapshots_by_server": snapshots_by_server,
|
||||
}
|
||||
|
||||
|
||||
def _build_server_summary_snapshot(
|
||||
server_key: str,
|
||||
generated_at: datetime,
|
||||
*,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
if get_historical_data_source_kind() == SOURCE_KIND_RCON:
|
||||
data_source = get_rcon_historical_read_model()
|
||||
summary_items = (
|
||||
data_source.list_server_summaries(server_key=server_key)
|
||||
if data_source is not None
|
||||
else []
|
||||
)
|
||||
else:
|
||||
summary_items = list_historical_server_summaries(server_slug=server_key, db_path=db_path)
|
||||
summary_item = summary_items[0] if summary_items else {}
|
||||
time_range = summary_item.get("time_range") if isinstance(summary_item, dict) else {}
|
||||
return {
|
||||
"server_key": server_key,
|
||||
"snapshot_type": SNAPSHOT_TYPE_SERVER_SUMMARY,
|
||||
"metric": None,
|
||||
"window": DEFAULT_SNAPSHOT_WINDOW,
|
||||
"generated_at": generated_at,
|
||||
"source_range_start": _parse_optional_timestamp(time_range.get("start")),
|
||||
"source_range_end": _parse_optional_timestamp(time_range.get("end")),
|
||||
"is_stale": False,
|
||||
"payload": {
|
||||
"server_key": server_key,
|
||||
"generated_at": _to_iso(generated_at),
|
||||
"item": summary_item,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_weekly_leaderboard_snapshot(
|
||||
server_key: str,
|
||||
metric: str,
|
||||
generated_at: datetime,
|
||||
*,
|
||||
limit: int,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
if get_historical_data_source_kind() == SOURCE_KIND_RCON:
|
||||
from .rcon_historical_leaderboards import list_rcon_materialized_leaderboard
|
||||
|
||||
leaderboard_result = list_rcon_materialized_leaderboard(
|
||||
limit=limit,
|
||||
server_key=server_key,
|
||||
metric=metric,
|
||||
timeframe="weekly",
|
||||
db_path=db_path,
|
||||
now=generated_at,
|
||||
)
|
||||
else:
|
||||
leaderboard_result = list_weekly_leaderboard(
|
||||
limit=limit,
|
||||
server_id=server_key,
|
||||
metric=metric,
|
||||
db_path=db_path,
|
||||
)
|
||||
return {
|
||||
"server_key": server_key,
|
||||
"snapshot_type": SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||
"metric": metric,
|
||||
"window": DEFAULT_WEEKLY_SNAPSHOT_WINDOW,
|
||||
"generated_at": generated_at,
|
||||
"source_range_start": _parse_optional_timestamp(leaderboard_result.get("window_start")),
|
||||
"source_range_end": _parse_optional_timestamp(leaderboard_result.get("window_end")),
|
||||
"is_stale": False,
|
||||
"payload": {
|
||||
"server_key": server_key,
|
||||
"metric": metric,
|
||||
"limit": limit,
|
||||
"generated_at": _to_iso(generated_at),
|
||||
**leaderboard_result,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_monthly_leaderboard_snapshot(
|
||||
server_key: str,
|
||||
metric: str,
|
||||
generated_at: datetime,
|
||||
*,
|
||||
limit: int,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
if get_historical_data_source_kind() == SOURCE_KIND_RCON:
|
||||
from .rcon_historical_leaderboards import list_rcon_materialized_leaderboard
|
||||
|
||||
leaderboard_result = list_rcon_materialized_leaderboard(
|
||||
limit=limit,
|
||||
server_key=server_key,
|
||||
metric=metric,
|
||||
timeframe="monthly",
|
||||
db_path=db_path,
|
||||
now=generated_at,
|
||||
)
|
||||
else:
|
||||
leaderboard_result = list_monthly_leaderboard(
|
||||
limit=limit,
|
||||
server_id=server_key,
|
||||
metric=metric,
|
||||
db_path=db_path,
|
||||
)
|
||||
return {
|
||||
"server_key": server_key,
|
||||
"snapshot_type": SNAPSHOT_TYPE_MONTHLY_LEADERBOARD,
|
||||
"metric": metric,
|
||||
"window": DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
|
||||
"generated_at": generated_at,
|
||||
"source_range_start": _parse_optional_timestamp(leaderboard_result.get("window_start")),
|
||||
"source_range_end": _parse_optional_timestamp(leaderboard_result.get("window_end")),
|
||||
"is_stale": False,
|
||||
"payload": {
|
||||
"server_key": server_key,
|
||||
"metric": metric,
|
||||
"limit": limit,
|
||||
"generated_at": _to_iso(generated_at),
|
||||
**leaderboard_result,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_recent_matches_snapshot(
|
||||
server_key: str,
|
||||
generated_at: datetime,
|
||||
*,
|
||||
limit: int,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
if get_historical_data_source_kind() == SOURCE_KIND_RCON:
|
||||
data_source = get_rcon_historical_read_model()
|
||||
items = (
|
||||
data_source.list_recent_activity(server_key=server_key, limit=limit)
|
||||
if data_source is not None
|
||||
else []
|
||||
)
|
||||
else:
|
||||
items = list_recent_historical_matches(
|
||||
limit=limit,
|
||||
server_slug=server_key,
|
||||
db_path=db_path,
|
||||
)
|
||||
closed_points = [
|
||||
_parse_optional_timestamp(item.get("closed_at"))
|
||||
for item in items
|
||||
if isinstance(item, dict) and item.get("closed_at")
|
||||
]
|
||||
return {
|
||||
"server_key": server_key,
|
||||
"snapshot_type": SNAPSHOT_TYPE_RECENT_MATCHES,
|
||||
"metric": None,
|
||||
"window": DEFAULT_SNAPSHOT_WINDOW,
|
||||
"generated_at": generated_at,
|
||||
"source_range_start": min(closed_points) if closed_points else None,
|
||||
"source_range_end": max(closed_points) if closed_points else None,
|
||||
"is_stale": False,
|
||||
"payload": {
|
||||
"server_key": server_key,
|
||||
"limit": limit,
|
||||
"generated_at": _to_iso(generated_at),
|
||||
"items": items,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_player_event_snapshot(
|
||||
server_key: str,
|
||||
snapshot_type: str,
|
||||
generated_at: datetime,
|
||||
*,
|
||||
limit: int,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
month_key = _get_latest_player_event_month_key(server_key=server_key, db_path=db_path)
|
||||
source_range_start = None
|
||||
source_range_end = None
|
||||
items: list[dict[str, object]] = []
|
||||
found = False
|
||||
|
||||
if month_key:
|
||||
source_range_start, source_range_end = _get_player_event_source_range(
|
||||
server_key=server_key,
|
||||
month_key=month_key,
|
||||
db_path=db_path,
|
||||
)
|
||||
items = _list_player_event_snapshot_items(
|
||||
snapshot_type=snapshot_type,
|
||||
server_key=server_key,
|
||||
month_key=month_key,
|
||||
limit=limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
found = bool(items or source_range_start or source_range_end)
|
||||
|
||||
return {
|
||||
"server_key": server_key,
|
||||
"snapshot_type": snapshot_type,
|
||||
"metric": None,
|
||||
"window": DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
|
||||
"generated_at": generated_at,
|
||||
"source_range_start": source_range_start,
|
||||
"source_range_end": source_range_end,
|
||||
"is_stale": False,
|
||||
"payload": {
|
||||
"server_key": server_key,
|
||||
"period": "monthly",
|
||||
"month_key": month_key,
|
||||
"limit": limit,
|
||||
"found": found,
|
||||
"generated_at": _to_iso(generated_at),
|
||||
"source_range_start": _to_iso(source_range_start) if source_range_start else None,
|
||||
"source_range_end": _to_iso(source_range_end) if source_range_end else None,
|
||||
"items": items,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_monthly_mvp_snapshot(
|
||||
server_key: str,
|
||||
generated_at: datetime,
|
||||
*,
|
||||
limit: int,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
ranking_result = list_monthly_mvp_ranking(
|
||||
limit=limit,
|
||||
server_id=server_key,
|
||||
db_path=db_path,
|
||||
)
|
||||
month_key = str(ranking_result.get("window_start") or "")[:7] or None
|
||||
return {
|
||||
"server_key": server_key,
|
||||
"snapshot_type": SNAPSHOT_TYPE_MONTHLY_MVP,
|
||||
"metric": None,
|
||||
"window": DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
|
||||
"generated_at": generated_at,
|
||||
"source_range_start": _parse_optional_timestamp(ranking_result.get("window_start")),
|
||||
"source_range_end": _parse_optional_timestamp(ranking_result.get("window_end")),
|
||||
"is_stale": False,
|
||||
"payload": {
|
||||
"server_key": server_key,
|
||||
"limit": limit,
|
||||
"month_key": month_key,
|
||||
"generated_at": _to_iso(generated_at),
|
||||
**ranking_result,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_monthly_mvp_v2_snapshot(
|
||||
server_key: str,
|
||||
generated_at: datetime,
|
||||
*,
|
||||
limit: int,
|
||||
db_path: Path | None = None,
|
||||
) -> dict[str, object]:
|
||||
ranking_result = list_monthly_mvp_v2_ranking(
|
||||
limit=limit,
|
||||
server_id=server_key,
|
||||
db_path=db_path,
|
||||
)
|
||||
month_key = str(ranking_result.get("window_start") or "")[:7] or None
|
||||
event_coverage = ranking_result.get("event_coverage")
|
||||
source_range_start = None
|
||||
source_range_end = None
|
||||
if isinstance(event_coverage, dict):
|
||||
source_range_start = _parse_optional_timestamp(event_coverage.get("source_range_start"))
|
||||
source_range_end = _parse_optional_timestamp(event_coverage.get("source_range_end"))
|
||||
return {
|
||||
"server_key": server_key,
|
||||
"snapshot_type": SNAPSHOT_TYPE_MONTHLY_MVP_V2,
|
||||
"metric": None,
|
||||
"window": DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
|
||||
"generated_at": generated_at,
|
||||
"source_range_start": source_range_start,
|
||||
"source_range_end": source_range_end,
|
||||
"is_stale": False,
|
||||
"payload": {
|
||||
"server_key": server_key,
|
||||
"limit": limit,
|
||||
"month_key": month_key,
|
||||
"found": bool(event_coverage.get("ready")) if isinstance(event_coverage, dict) else False,
|
||||
"generated_at": _to_iso(generated_at),
|
||||
**ranking_result,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _resolve_snapshot_target_keys(
|
||||
*,
|
||||
server_key: str | None,
|
||||
db_path: Path | None = None,
|
||||
) -> list[str]:
|
||||
"""Expand targeted rebuilds so the logical global aggregate stays in sync."""
|
||||
if not server_key:
|
||||
return list_snapshot_server_keys(db_path=db_path)
|
||||
|
||||
normalized_server_key = server_key.strip()
|
||||
if not normalized_server_key:
|
||||
return list_snapshot_server_keys(db_path=db_path)
|
||||
if normalized_server_key == ALL_SERVERS_SLUG:
|
||||
return [ALL_SERVERS_SLUG]
|
||||
|
||||
return [normalized_server_key, ALL_SERVERS_SLUG]
|
||||
|
||||
|
||||
def _list_player_event_snapshot_items(
|
||||
*,
|
||||
snapshot_type: str,
|
||||
server_key: str,
|
||||
month_key: str,
|
||||
limit: int,
|
||||
db_path: Path | None,
|
||||
) -> list[dict[str, object]]:
|
||||
aggregator_by_snapshot_type = {
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_MOST_KILLED: list_most_killed,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DEATH_BY: list_death_by,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_DUELS: list_net_duel_summaries,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_WEAPON_KILLS: list_weapon_kills,
|
||||
SNAPSHOT_TYPE_PLAYER_EVENT_TEAMKILLS: list_teamkill_summaries,
|
||||
}
|
||||
aggregator = aggregator_by_snapshot_type[snapshot_type]
|
||||
return aggregator(
|
||||
server_slug=server_key,
|
||||
month=month_key,
|
||||
limit=limit,
|
||||
db_path=db_path,
|
||||
)
|
||||
|
||||
|
||||
def _get_latest_player_event_month_key(
|
||||
*,
|
||||
server_key: str,
|
||||
db_path: Path | None = None,
|
||||
) -> str | None:
|
||||
resolved_path = initialize_player_event_storage(db_path=db_path)
|
||||
where_sql, params = _build_player_event_scope_where(server_key=server_key)
|
||||
with _connect(resolved_path) as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT MAX(substr(CAST(occurred_at AS TEXT), 1, 7)) AS latest_month
|
||||
FROM player_event_raw_ledger
|
||||
WHERE occurred_at IS NOT NULL
|
||||
AND {where_sql}
|
||||
""",
|
||||
params,
|
||||
).fetchone()
|
||||
if not row or not row["latest_month"]:
|
||||
return None
|
||||
return str(row["latest_month"])
|
||||
|
||||
|
||||
def _get_player_event_source_range(
|
||||
*,
|
||||
server_key: str,
|
||||
month_key: str,
|
||||
db_path: Path | None = None,
|
||||
) -> tuple[datetime | None, datetime | None]:
|
||||
resolved_path = initialize_player_event_storage(db_path=db_path)
|
||||
where_sql, params = _build_player_event_scope_where(server_key=server_key)
|
||||
with _connect(resolved_path) as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
MIN(occurred_at) AS source_range_start,
|
||||
MAX(occurred_at) AS source_range_end
|
||||
FROM player_event_raw_ledger
|
||||
WHERE occurred_at IS NOT NULL
|
||||
AND substr(CAST(occurred_at AS TEXT), 1, 7) = ?
|
||||
AND {where_sql}
|
||||
""",
|
||||
[month_key, *params],
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None, None
|
||||
return (
|
||||
_parse_optional_timestamp(row["source_range_start"]),
|
||||
_parse_optional_timestamp(row["source_range_end"]),
|
||||
)
|
||||
|
||||
|
||||
def _build_player_event_scope_where(*, server_key: str) -> tuple[str, list[object]]:
|
||||
if server_key == ALL_SERVERS_SLUG:
|
||||
return "1 = 1", []
|
||||
return "server_slug = ?", [server_key]
|
||||
|
||||
|
||||
def _connect(db_path: Path) -> sqlite3.Connection:
|
||||
if get_database_url():
|
||||
from .postgres_display_storage import connect_postgres_compat
|
||||
|
||||
return connect_postgres_compat()
|
||||
connection = sqlite3.connect(db_path)
|
||||
connection.row_factory = sqlite3.Row
|
||||
return connection
|
||||
|
||||
|
||||
def _parse_optional_timestamp(value: object) -> datetime | None:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return None
|
||||
normalized = value.strip().replace("Z", "+00:00")
|
||||
parsed = datetime.fromisoformat(normalized)
|
||||
if parsed.tzinfo is None:
|
||||
return parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def _as_utc(value: datetime) -> datetime:
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=timezone.utc)
|
||||
return value.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def _to_iso(value: datetime) -> str:
|
||||
return _as_utc(value).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _normalize_snapshot_limit(name: str, value: object) -> int:
|
||||
try:
|
||||
limit = int(value)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError(f"{name} must be a positive integer.") from error
|
||||
if limit <= 0:
|
||||
raise ValueError(f"{name} must be a positive integer.")
|
||||
return limit
|
||||
|
||||
|
||||
def _log_snapshot_build_started(
|
||||
server_key: str,
|
||||
snapshot_type: str,
|
||||
*,
|
||||
metric: str | None = None,
|
||||
) -> None:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"event": "historical-snapshot-build-started",
|
||||
"server_key": server_key,
|
||||
"snapshot_type": snapshot_type,
|
||||
"metric": metric,
|
||||
},
|
||||
ensure_ascii=True,
|
||||
default=str,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
Reference in New Issue
Block a user