Implement monthly MVP V2 workflow and snapshots

This commit is contained in:
devRaGonSa
2026-03-24 16:40:19 +01:00
parent c84ebad5af
commit af140d6f3e
13 changed files with 1620 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
@@ -11,15 +12,30 @@ from .historical_storage import (
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(
{
@@ -27,7 +43,13 @@ SUPPORTED_SNAPSHOT_TYPES = frozenset(
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,
}
)
@@ -52,6 +74,13 @@ SNAPSHOT_LEADERBOARD_METRICS = (
"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"
@@ -134,6 +163,24 @@ def build_historical_server_snapshots(
db_path=db_path,
)
)
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:
snapshots.append(
_build_player_event_snapshot(
server_key,
snapshot_type,
generated_at_value,
limit=leaderboard_limit,
db_path=db_path,
)
)
snapshots.append(
_build_recent_matches_snapshot(
@@ -188,6 +235,24 @@ def build_priority_historical_snapshots(
db_path=db_path,
)
)
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:
snapshots.append(
_build_player_event_snapshot(
server_key,
snapshot_type,
generated_at_value,
limit=leaderboard_limit,
db_path=db_path,
)
)
snapshots.append(
_build_recent_matches_snapshot(
server_key,
@@ -420,6 +485,58 @@ def _build_recent_matches_snapshot(
}
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,
@@ -452,6 +569,45 @@ def _build_monthly_mvp_snapshot(
}
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,
@@ -470,6 +626,93 @@ def _resolve_snapshot_target_keys(
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(occurred_at, 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(occurred_at, 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:
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