Implement Moxfield deck source adapter (closes #35)

This commit is contained in:
KoalaTrapLord authored and GitHub committed 2026-03-17 15:59:10 -04:00
1 parent 554b166787
commit 179a53c0af
1 file changed
+121 -9
+121 -9
View File
@@ -1,21 +1,133 @@
"""
Moxfield Adapter (stub)
════════════════════════
Moxfield Adapter
================
Fetches template decklists from Moxfield for a given commander.
Currently returns empty (experimental — not yet implemented).
Uses the undocumented but usable api2.moxfield.com API.
Rate-limited to <=10 req/sec. Errors are caught and logged.
"""
from .models import TemplateDeck, TemplateDeckCard
import logging
import time
import httpx
from .models import TemplateDeck, TemplateDeckCard
log = logging.getLogger("commander_ai_lab.deckgen.moxfield")
MOXFIELD_API = "https://api2.moxfield.com"
HEADERS = {
"User-Agent": "CommanderAILab/1.0",
"Accept": "application/json",
}
# Minimum seconds between requests (<=10 req/sec)
_RATE_LIMIT_DELAY = 0.12
def fetch_template_decks(commander_name: str, color_identity: list, config: dict = None) -> list:
def _slugify_commander(name: str) -> str:
"""Return the commander name as-is for the Moxfield search query."""
return name.strip()
def _build_cards_from_deck(deck_data: dict) -> list:
"""Extract commander + mainboard cards from a full Moxfield deck response."""
cards = []
# Commanders first
for card_name, card_obj in deck_data.get("commanders", {}).items():
qty = card_obj.get("quantity", 1)
cards.append(TemplateDeckCard(name=card_name, quantity=qty))
# Mainboard
for card_name, card_obj in deck_data.get("mainboard", {}).items():
qty = card_obj.get("quantity", 1)
cards.append(TemplateDeckCard(name=card_name, quantity=qty))
return cards
def fetch_template_decks(
commander_name: str,
color_identity: list,
config: dict = None,
) -> list:
"""
Fetch template decks from Moxfield for the given commander.
Experimental — not yet implemented.
Fetch top Commander decks from Moxfield for the given commander.
Returns a list of TemplateDeck objects. Returns empty list on any
API failure — errors are logged, never raised.
Args:
commander_name: The commander's card name.
color_identity: List of color symbols (unused, kept for interface compat).
config: Optional dict. Supported key: ``max_decks`` (default 3).
Returns:
List of TemplateDeck objects populated with commander + mainboard cards.
"""
log.info(f" Stub: Not yet implemented for '{commander_name}'")
max_decks = (config or {}).get("max_decks", 3)
results = []
try:
# Step 1 — Search for top decks by commander name
search_url = f"{MOXFIELD_API}/v2/decks/search"
params = {
"q": "",
"fmt": "commander",
"commander": _slugify_commander(commander_name),
"sortType": "views",
"sortDirection": "Descending",
"pageSize": max_decks,
}
log.info(f" Moxfield: Searching for '{commander_name}' (max={max_decks})")
resp = httpx.get(search_url, params=params, headers=HEADERS, timeout=15)
resp.raise_for_status()
search_data = resp.json()
deck_summaries = search_data.get("data", [])[:max_decks]
if not deck_summaries:
log.info(f" Moxfield: No decks found for '{commander_name}'")
return []
log.info(f" Moxfield: Found {len(deck_summaries)} deck(s) for '{commander_name}', fetching details...")
# Step 2 — Fetch full detail for each deck
for summary in deck_summaries:
deck_id = summary.get("publicId")
deck_name = summary.get("name", f"Moxfield {commander_name}")
if not deck_id:
log.warning(" Moxfield: Skipping deck with no publicId")
continue
time.sleep(_RATE_LIMIT_DELAY) # Rate limit: <=10 req/sec
try:
deck_url = f"{MOXFIELD_API}/v3/decks/all/{deck_id}"
deck_resp = httpx.get(deck_url, headers=HEADERS, timeout=15)
deck_resp.raise_for_status()
deck_data = deck_resp.json()
except Exception as deck_err:
log.warning(f" Moxfield: Failed to fetch deck '{deck_id}': {deck_err}")
continue
cards = _build_cards_from_deck(deck_data)
if not cards:
log.warning(f" Moxfield: Deck '{deck_name}' ({deck_id}) had no cards, skipping")
continue
results.append(TemplateDeck(
name=f"{deck_name} (Moxfield)",
source=f"https://moxfield.com/decks/{deck_id}",
cards=cards,
))
log.info(f" Moxfield: Added deck '{deck_name}' ({len(cards)} cards)")
log.info(f" Moxfield: Returning {len(results)} deck(s) for '{commander_name}'")
except httpx.HTTPStatusError as e:
log.warning(
f" Moxfield fetch failed for '{commander_name}': "
f"HTTP {e.response.status_code} — {e}"
)
except httpx.RequestError as e:
log.warning(f" Moxfield request error for '{commander_name}': {e}")
except Exception as e:
log.warning(f" Moxfield unexpected error for '{commander_name}': {e}")
return results