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"), }