mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 15:40:12 +01:00
314 lines
11 KiB
Python
314 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from collections import Counter
|
|
from typing import Any, Dict, Iterable
|
|
|
|
__all__ = [
|
|
"record_land_summary",
|
|
"get_mdfc_metrics",
|
|
"record_theme_summary",
|
|
"get_theme_metrics",
|
|
"record_partner_summary",
|
|
"get_partner_metrics",
|
|
]
|
|
|
|
|
|
_lock = threading.Lock()
|
|
_metrics: Dict[str, Any] = {
|
|
"total_builds": 0,
|
|
"builds_with_mdfc": 0,
|
|
"total_mdfc_lands": 0,
|
|
"last_updated": None,
|
|
"last_updated_iso": None,
|
|
"last_summary": None,
|
|
}
|
|
_top_cards: Counter[str] = Counter()
|
|
|
|
_theme_metrics: Dict[str, Any] = {
|
|
"total_builds": 0,
|
|
"with_user_themes": 0,
|
|
"last_updated": None,
|
|
"last_updated_iso": None,
|
|
"last_summary": None,
|
|
}
|
|
_user_theme_counter: Counter[str] = Counter()
|
|
_user_theme_labels: Dict[str, str] = {}
|
|
|
|
_partner_metrics: Dict[str, Any] = {
|
|
"total_pairs": 0,
|
|
"mode_counts": {},
|
|
"last_summary": None,
|
|
"last_updated": None,
|
|
"last_updated_iso": None,
|
|
}
|
|
|
|
|
|
def _to_int(value: Any) -> int:
|
|
try:
|
|
if value is None:
|
|
return 0
|
|
if isinstance(value, bool):
|
|
return int(value)
|
|
return int(float(value))
|
|
except (TypeError, ValueError):
|
|
return 0
|
|
|
|
|
|
def _sanitize_cards(cards: Iterable[Dict[str, Any]] | None) -> list[Dict[str, Any]]:
|
|
if not cards:
|
|
return []
|
|
sanitized: list[Dict[str, Any]] = []
|
|
for entry in cards:
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
name = str(entry.get("name") or "").strip()
|
|
if not name:
|
|
continue
|
|
count = _to_int(entry.get("count", 1)) or 1
|
|
colors = entry.get("colors")
|
|
if isinstance(colors, (list, tuple)):
|
|
color_list = [str(c) for c in colors if str(c)]
|
|
else:
|
|
color_list = []
|
|
sanitized.append(
|
|
{
|
|
"name": name,
|
|
"count": count,
|
|
"colors": color_list,
|
|
"counts_as_land": bool(entry.get("counts_as_land")),
|
|
"adds_extra_land": bool(entry.get("adds_extra_land")),
|
|
}
|
|
)
|
|
return sanitized
|
|
|
|
|
|
def record_land_summary(land_summary: Dict[str, Any] | None) -> None:
|
|
if not isinstance(land_summary, dict):
|
|
return
|
|
|
|
dfc_lands = _to_int(land_summary.get("dfc_lands"))
|
|
with_dfc = _to_int(land_summary.get("with_dfc"))
|
|
timestamp = time.time()
|
|
cards = _sanitize_cards(land_summary.get("dfc_cards"))
|
|
|
|
with _lock:
|
|
_metrics["total_builds"] = int(_metrics.get("total_builds", 0)) + 1
|
|
if dfc_lands > 0:
|
|
_metrics["builds_with_mdfc"] = int(_metrics.get("builds_with_mdfc", 0)) + 1
|
|
_metrics["total_mdfc_lands"] = int(_metrics.get("total_mdfc_lands", 0)) + dfc_lands
|
|
for entry in cards:
|
|
_top_cards[entry["name"]] += entry["count"]
|
|
_metrics["last_summary"] = {
|
|
"dfc_lands": dfc_lands,
|
|
"with_dfc": with_dfc,
|
|
"cards": cards,
|
|
}
|
|
_metrics["last_updated"] = timestamp
|
|
_metrics["last_updated_iso"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp))
|
|
|
|
|
|
def get_mdfc_metrics() -> Dict[str, Any]:
|
|
with _lock:
|
|
builds = int(_metrics.get("total_builds", 0) or 0)
|
|
builds_with = int(_metrics.get("builds_with_mdfc", 0) or 0)
|
|
total_lands = int(_metrics.get("total_mdfc_lands", 0) or 0)
|
|
ratio = (builds_with / builds) if builds else 0.0
|
|
avg_lands = (total_lands / builds_with) if builds_with else 0.0
|
|
top_cards = dict(_top_cards.most_common(10))
|
|
return {
|
|
"total_builds": builds,
|
|
"builds_with_mdfc": builds_with,
|
|
"build_share": ratio,
|
|
"total_mdfc_lands": total_lands,
|
|
"avg_mdfc_lands": avg_lands,
|
|
"top_cards": top_cards,
|
|
"last_summary": _metrics.get("last_summary"),
|
|
"last_updated": _metrics.get("last_updated_iso"),
|
|
}
|
|
|
|
|
|
def _reset_metrics_for_test() -> None:
|
|
with _lock:
|
|
_metrics.update(
|
|
{
|
|
"total_builds": 0,
|
|
"builds_with_mdfc": 0,
|
|
"total_mdfc_lands": 0,
|
|
"last_updated": None,
|
|
"last_updated_iso": None,
|
|
"last_summary": None,
|
|
}
|
|
)
|
|
_top_cards.clear()
|
|
_theme_metrics.update(
|
|
{
|
|
"total_builds": 0,
|
|
"with_user_themes": 0,
|
|
"last_updated": None,
|
|
"last_updated_iso": None,
|
|
"last_summary": None,
|
|
}
|
|
)
|
|
_user_theme_counter.clear()
|
|
_user_theme_labels.clear()
|
|
_partner_metrics.update(
|
|
{
|
|
"total_pairs": 0,
|
|
"mode_counts": {},
|
|
"last_summary": None,
|
|
"last_updated": None,
|
|
"last_updated_iso": None,
|
|
}
|
|
)
|
|
|
|
|
|
def _sanitize_theme_list(values: Iterable[Any]) -> list[str]:
|
|
sanitized: list[str] = []
|
|
seen: set[str] = set()
|
|
for raw in values or []:
|
|
text = str(raw or "").strip()
|
|
if not text:
|
|
continue
|
|
key = text.casefold()
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
sanitized.append(text)
|
|
return sanitized
|
|
|
|
|
|
def record_theme_summary(theme_summary: Dict[str, Any] | None) -> None:
|
|
if not isinstance(theme_summary, dict):
|
|
return
|
|
|
|
commander_themes = _sanitize_theme_list(theme_summary.get("commanderThemes") or [])
|
|
user_themes = _sanitize_theme_list(theme_summary.get("userThemes") or [])
|
|
requested = _sanitize_theme_list(theme_summary.get("requested") or [])
|
|
resolved = _sanitize_theme_list(theme_summary.get("resolved") or [])
|
|
unresolved_raw = theme_summary.get("unresolved") or []
|
|
if isinstance(unresolved_raw, (list, tuple)):
|
|
unresolved = [str(item).strip() for item in unresolved_raw if str(item).strip()]
|
|
else:
|
|
unresolved = []
|
|
mode = str(theme_summary.get("mode") or "AND")
|
|
try:
|
|
weight = float(theme_summary.get("weight", 1.0) or 1.0)
|
|
except Exception:
|
|
weight = 1.0
|
|
catalog_version = theme_summary.get("themeCatalogVersion")
|
|
matches = theme_summary.get("matches") if isinstance(theme_summary.get("matches"), list) else []
|
|
fuzzy = theme_summary.get("fuzzyCorrections") if isinstance(theme_summary.get("fuzzyCorrections"), dict) else {}
|
|
|
|
merged: list[str] = []
|
|
seen_merge: set[str] = set()
|
|
for collection in (commander_themes, user_themes):
|
|
for item in collection:
|
|
key = item.casefold()
|
|
if key in seen_merge:
|
|
continue
|
|
seen_merge.add(key)
|
|
merged.append(item)
|
|
|
|
timestamp = time.time()
|
|
iso = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp))
|
|
|
|
with _lock:
|
|
_theme_metrics["total_builds"] = int(_theme_metrics.get("total_builds", 0) or 0) + 1
|
|
if user_themes:
|
|
_theme_metrics["with_user_themes"] = int(_theme_metrics.get("with_user_themes", 0) or 0) + 1
|
|
for label in user_themes:
|
|
key = label.casefold()
|
|
_user_theme_counter[key] += 1
|
|
if key not in _user_theme_labels:
|
|
_user_theme_labels[key] = label
|
|
_theme_metrics["last_summary"] = {
|
|
"commanderThemes": commander_themes,
|
|
"userThemes": user_themes,
|
|
"mergedThemes": merged,
|
|
"requested": requested,
|
|
"resolved": resolved,
|
|
"unresolved": unresolved,
|
|
"unresolvedCount": len(unresolved),
|
|
"mode": mode,
|
|
"weight": weight,
|
|
"matches": matches,
|
|
"fuzzyCorrections": fuzzy,
|
|
"themeCatalogVersion": catalog_version,
|
|
}
|
|
_theme_metrics["last_updated"] = timestamp
|
|
_theme_metrics["last_updated_iso"] = iso
|
|
|
|
|
|
def get_theme_metrics() -> Dict[str, Any]:
|
|
with _lock:
|
|
total = int(_theme_metrics.get("total_builds", 0) or 0)
|
|
with_user = int(_theme_metrics.get("with_user_themes", 0) or 0)
|
|
share = (with_user / total) if total else 0.0
|
|
top_user: list[Dict[str, Any]] = []
|
|
for key, count in _user_theme_counter.most_common(10):
|
|
label = _user_theme_labels.get(key, key)
|
|
top_user.append({"theme": label, "count": int(count)})
|
|
return {
|
|
"total_builds": total,
|
|
"with_user_themes": with_user,
|
|
"user_theme_share": share,
|
|
"last_summary": _theme_metrics.get("last_summary"),
|
|
"last_updated": _theme_metrics.get("last_updated_iso"),
|
|
"top_user_themes": top_user,
|
|
}
|
|
|
|
|
|
def record_partner_summary(commander_summary: Dict[str, Any] | None) -> None:
|
|
if not isinstance(commander_summary, dict):
|
|
return
|
|
|
|
combined = commander_summary.get("combined")
|
|
if not isinstance(combined, dict):
|
|
return
|
|
|
|
mode = str(commander_summary.get("partner_mode") or combined.get("partner_mode") or "none")
|
|
primary = commander_summary.get("primary")
|
|
secondary = commander_summary.get("secondary")
|
|
names = commander_summary.get("names")
|
|
color_identity_raw = combined.get("color_identity")
|
|
if isinstance(color_identity_raw, (list, tuple)):
|
|
colors = [str(c).strip().upper() for c in color_identity_raw if str(c).strip()]
|
|
else:
|
|
colors = []
|
|
entry = {
|
|
"primary": primary,
|
|
"secondary": secondary,
|
|
"names": list(names or []) if isinstance(names, (list, tuple)) else names,
|
|
"partner_mode": mode,
|
|
"color_identity": colors,
|
|
"color_code": combined.get("color_code"),
|
|
"color_label": combined.get("color_label"),
|
|
"color_sources": combined.get("color_sources"),
|
|
"color_delta": combined.get("color_delta"),
|
|
"secondary_role": combined.get("secondary_role"),
|
|
"secondary_role_label": combined.get("secondary_role_label"),
|
|
}
|
|
|
|
timestamp = time.time()
|
|
iso = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(timestamp))
|
|
mode_key = mode.lower() if isinstance(mode, str) else "none"
|
|
|
|
with _lock:
|
|
_partner_metrics["total_pairs"] = int(_partner_metrics.get("total_pairs", 0) or 0) + 1
|
|
mode_counts = _partner_metrics.setdefault("mode_counts", {})
|
|
mode_counts[mode_key] = int(mode_counts.get(mode_key, 0) or 0) + 1
|
|
_partner_metrics["last_summary"] = entry
|
|
_partner_metrics["last_updated"] = timestamp
|
|
_partner_metrics["last_updated_iso"] = iso
|
|
|
|
|
|
def get_partner_metrics() -> Dict[str, Any]:
|
|
with _lock:
|
|
return {
|
|
"total_pairs": int(_partner_metrics.get("total_pairs", 0) or 0),
|
|
"mode_counts": dict(_partner_metrics.get("mode_counts", {})),
|
|
"last_summary": _partner_metrics.get("last_summary"),
|
|
"last_updated": _partner_metrics.get("last_updated_iso"),
|
|
}
|