fix(lint): improved type checking and code quality (77% error reduction)

This commit is contained in:
matt 2025-10-31 08:18:09 -07:00
parent 3c45a31aa3
commit 83fe527979
37 changed files with 423 additions and 303 deletions

View file

@ -4,6 +4,6 @@ __all__ = ['DeckBuilder']
def __getattr__(name):
# Lazy-load DeckBuilder to avoid side effects during import of submodules
if name == 'DeckBuilder':
from .builder import DeckBuilder # type: ignore
from .builder import DeckBuilder
return DeckBuilder
raise AttributeError(name)

View file

@ -95,7 +95,7 @@ class DeckBuilder(
# If a seed was assigned pre-init, use it
if self.seed is not None:
# Import here to avoid any heavy import cycles at module import time
from random_util import set_seed as _set_seed # type: ignore
from random_util import set_seed as _set_seed
self._rng = _set_seed(int(self.seed))
else:
self._rng = random.Random()
@ -107,7 +107,7 @@ class DeckBuilder(
def set_seed(self, seed: int | str) -> None:
"""Set deterministic seed for this builder and reset its RNG instance."""
try:
from random_util import derive_seed_from_string as _derive, set_seed as _set_seed # type: ignore
from random_util import derive_seed_from_string as _derive, set_seed as _set_seed
s = _derive(seed)
self.seed = int(s)
self._rng = _set_seed(s)
@ -215,7 +215,7 @@ class DeckBuilder(
try:
# Compute a quick compliance snapshot here to hint at upcoming enforcement
if hasattr(self, 'compute_and_print_compliance') and not getattr(self, 'headless', False):
from deck_builder.brackets_compliance import evaluate_deck as _eval # type: ignore
from deck_builder.brackets_compliance import evaluate_deck as _eval
bracket_key = str(getattr(self, 'bracket_name', '') or getattr(self, 'bracket_level', 'core')).lower()
commander = getattr(self, 'commander_name', None)
snap = _eval(self.card_library, commander_name=commander, bracket=bracket_key)
@ -240,15 +240,15 @@ class DeckBuilder(
csv_path = self.export_decklist_csv()
# Persist CSV path immediately (before any later potential exceptions)
try:
self.last_csv_path = csv_path # type: ignore[attr-defined]
self.last_csv_path = csv_path
except Exception:
pass
try:
import os as _os
base, _ext = _os.path.splitext(_os.path.basename(csv_path))
txt_path = self.export_decklist_text(filename=base + '.txt') # type: ignore[attr-defined]
txt_path = self.export_decklist_text(filename=base + '.txt')
try:
self.last_txt_path = txt_path # type: ignore[attr-defined]
self.last_txt_path = txt_path
except Exception:
pass
# Display the text file contents for easy copy/paste to online deck builders
@ -256,18 +256,18 @@ class DeckBuilder(
# Compute bracket compliance and save a JSON report alongside exports
try:
if hasattr(self, 'compute_and_print_compliance'):
report0 = self.compute_and_print_compliance(base_stem=base) # type: ignore[attr-defined]
report0 = self.compute_and_print_compliance(base_stem=base)
# If non-compliant and interactive, offer enforcement now
try:
if isinstance(report0, dict) and report0.get('overall') == 'FAIL' and not getattr(self, 'headless', False):
from deck_builder.phases.phase6_reporting import ReportingMixin as _RM # type: ignore
from deck_builder.phases.phase6_reporting import ReportingMixin as _RM
if isinstance(self, _RM) and hasattr(self, 'enforce_and_reexport'):
self.output_func("One or more bracket limits exceeded. Enter to auto-resolve, or Ctrl+C to skip.")
try:
_ = self.input_func("")
except Exception:
pass
self.enforce_and_reexport(base_stem=base, mode='prompt') # type: ignore[attr-defined]
self.enforce_and_reexport(base_stem=base, mode='prompt')
except Exception:
pass
except Exception:
@ -295,12 +295,12 @@ class DeckBuilder(
cfg_dir = 'config'
if cfg_dir:
_os.makedirs(cfg_dir, exist_ok=True)
self.export_run_config_json(directory=cfg_dir, filename=base + '.json') # type: ignore[attr-defined]
self.export_run_config_json(directory=cfg_dir, filename=base + '.json')
if cfg_path_env:
cfg_dir2 = _os.path.dirname(cfg_path_env) or '.'
cfg_name2 = _os.path.basename(cfg_path_env)
_os.makedirs(cfg_dir2, exist_ok=True)
self.export_run_config_json(directory=cfg_dir2, filename=cfg_name2) # type: ignore[attr-defined]
self.export_run_config_json(directory=cfg_dir2, filename=cfg_name2)
except Exception:
pass
except Exception:
@ -308,8 +308,8 @@ class DeckBuilder(
else:
# Mark suppression so random flow knows nothing was exported yet
try:
self.last_csv_path = None # type: ignore[attr-defined]
self.last_txt_path = None # type: ignore[attr-defined]
self.last_csv_path = None
self.last_txt_path = None
except Exception:
pass
# If owned-only and deck not complete, print a note
@ -624,8 +624,8 @@ class DeckBuilder(
try:
rec.card_library = rec_subset
# Export CSV and TXT with suffix
rec.export_decklist_csv(directory='deck_files', filename=base_stem + '_recommendations.csv', suppress_output=True) # type: ignore[attr-defined]
rec.export_decklist_text(directory='deck_files', filename=base_stem + '_recommendations.txt', suppress_output=True) # type: ignore[attr-defined]
rec.export_decklist_csv(directory='deck_files', filename=base_stem + '_recommendations.csv', suppress_output=True)
rec.export_decklist_text(directory='deck_files', filename=base_stem + '_recommendations.txt', suppress_output=True)
finally:
rec.card_library = original_lib
# Notify user succinctly
@ -1843,7 +1843,7 @@ class DeckBuilder(
from deck_builder import builder_constants as bc
from settings import MULTIPLE_COPY_CARDS
except Exception:
MULTIPLE_COPY_CARDS = [] # type: ignore
MULTIPLE_COPY_CARDS = []
is_land = 'land' in str(card_type or entry.get('Card Type','')).lower()
is_basic = False
try:
@ -2353,7 +2353,7 @@ class DeckBuilder(
rng = getattr(self, 'rng', None)
try:
if rng:
rng.shuffle(bucket_keys) # type: ignore
rng.shuffle(bucket_keys)
else:
random.shuffle(bucket_keys)
except Exception:

View file

@ -1,4 +1,4 @@
from typing import Dict, List, Final, Tuple, Union, Callable, Any as _Any
from typing import Dict, List, Final, Tuple, Union, Callable, Any
from settings import CARD_DATA_COLUMNS as CSV_REQUIRED_COLUMNS # unified
from path_util import csv_dir
import pandas as pd
@ -21,7 +21,7 @@ DUPLICATE_CARD_FORMAT: Final[str] = '{card_name} x {count}'
COMMANDER_CSV_PATH: Final[str] = f"{csv_dir()}/commander_cards.csv"
DECK_DIRECTORY = '../deck_files'
# M4: Deprecated - Parquet handles types natively (no converters needed)
COMMANDER_CONVERTERS: Final[Dict[str, str]] = {
COMMANDER_CONVERTERS: Final[Dict[str, Any]] = {
'themeTags': ast.literal_eval,
'creatureTypes': ast.literal_eval,
'roleTags': ast.literal_eval,
@ -140,18 +140,18 @@ OTHER_COLOR_MAP: Final[Dict[str, Tuple[str, List[str], List[str]]]] = {
}
# Card category validation rules
CREATURE_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = {
CREATURE_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = {
'power': {'type': ('str', 'int', 'float'), 'required': True},
'toughness': {'type': ('str', 'int', 'float'), 'required': True},
'creatureTypes': {'type': 'list', 'required': True}
}
SPELL_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = {
SPELL_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = {
'manaCost': {'type': 'str', 'required': True},
'text': {'type': 'str', 'required': True}
}
LAND_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = {
LAND_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = {
'type': {'type': ('str', 'object'), 'required': True},
'text': {'type': ('str', 'object'), 'required': False}
}
@ -526,7 +526,7 @@ CSV_READ_TIMEOUT: Final[int] = 30 # Timeout in seconds for CSV read operations
CSV_PROCESSING_BATCH_SIZE: Final[int] = 1000 # Number of rows to process in each batch
# CSV validation configuration
CSV_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float]]]] = {
CSV_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = {
'name': {'type': ('str', 'object'), 'required': True, 'unique': True},
'edhrecRank': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 100000},
'manaValue': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 20},
@ -602,12 +602,12 @@ GAME_CHANGERS: Final[List[str]] = [
# - color_identity: list[str] of required color letters (subset must be in commander CI)
# - printed_cap: int | None (None means no printed cap)
# - exclusive_group: str | None (at most one from the same group)
# - triggers: { tags_any: list[str], tags_all: list[str] }
# - triggers: { tagsAny: list[str], tags_all: list[str] }
# - default_count: int (default 25)
# - rec_window: tuple[int,int] (recommendation window)
# - thrumming_stone_synergy: bool
# - type_hint: 'creature' | 'noncreature'
MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, Any]]] = {
'cid_timeless_artificer': {
'id': 'cid_timeless_artificer',
'name': 'Cid, Timeless Artificer',
@ -615,7 +615,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['artificer kindred', 'hero kindred', 'artifacts matter'],
'tagsAny': ['artificer kindred', 'hero kindred', 'artifacts matter'],
'tags_all': []
},
'default_count': 25,
@ -630,7 +630,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['burn','spellslinger','prowess','storm','copy','cascade','impulse draw','treasure','ramp','graveyard','mill','discard','recursion'],
'tagsAny': ['burn','spellslinger','prowess','storm','copy','cascade','impulse draw','treasure','ramp','graveyard','mill','discard','recursion'],
'tags_all': []
},
'default_count': 25,
@ -645,7 +645,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['rabbit kindred','tokens matter','aggro'],
'tagsAny': ['rabbit kindred','tokens matter','aggro'],
'tags_all': []
},
'default_count': 25,
@ -660,7 +660,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['tokens','tokens matter','go-wide','exile matters','ooze kindred','spells matter','spellslinger','graveyard','mill','discard','recursion','domain','self-mill','delirium','descend'],
'tagsAny': ['tokens','tokens matter','go-wide','exile matters','ooze kindred','spells matter','spellslinger','graveyard','mill','discard','recursion','domain','self-mill','delirium','descend'],
'tags_all': []
},
'default_count': 25,
@ -675,7 +675,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': 'rats',
'triggers': {
'tags_any': ['rats','swarm','aristocrats','sacrifice','devotion-b','lifedrain','graveyard','recursion'],
'tagsAny': ['rats','swarm','aristocrats','sacrifice','devotion-b','lifedrain','graveyard','recursion'],
'tags_all': []
},
'default_count': 25,
@ -690,7 +690,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': 'rats',
'triggers': {
'tags_any': ['rats','swarm','aristocrats','sacrifice','devotion-b','lifedrain','graveyard','recursion'],
'tagsAny': ['rats','swarm','aristocrats','sacrifice','devotion-b','lifedrain','graveyard','recursion'],
'tags_all': []
},
'default_count': 25,
@ -705,7 +705,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': 7,
'exclusive_group': None,
'triggers': {
'tags_any': ['dwarf kindred','treasure','equipment','tokens','go-wide','tribal'],
'tagsAny': ['dwarf kindred','treasure','equipment','tokens','go-wide','tribal'],
'tags_all': []
},
'default_count': 7,
@ -720,7 +720,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['mill','advisor kindred','control','defenders','walls','draw-go'],
'tagsAny': ['mill','advisor kindred','control','defenders','walls','draw-go'],
'tags_all': []
},
'default_count': 25,
@ -735,7 +735,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['demon kindred','aristocrats','sacrifice','recursion','lifedrain'],
'tagsAny': ['demon kindred','aristocrats','sacrifice','recursion','lifedrain'],
'tags_all': []
},
'default_count': 25,
@ -750,7 +750,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': 9,
'exclusive_group': None,
'triggers': {
'tags_any': ['wraith kindred','ring','amass','orc','menace','aristocrats','sacrifice','devotion-b'],
'tagsAny': ['wraith kindred','ring','amass','orc','menace','aristocrats','sacrifice','devotion-b'],
'tags_all': []
},
'default_count': 9,
@ -765,7 +765,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['bird kindred','aggro'],
'tagsAny': ['bird kindred','aggro'],
'tags_all': []
},
'default_count': 25,
@ -780,7 +780,7 @@ MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, _Any]]] = {
'printed_cap': None,
'exclusive_group': None,
'triggers': {
'tags_any': ['aggro','human kindred','knight kindred','historic matters','artifacts matter'],
'tagsAny': ['aggro','human kindred','knight kindred','historic matters','artifacts matter'],
'tags_all': []
},
'default_count': 25,
@ -956,3 +956,4 @@ def get_backgrounds(df: pd.DataFrame) -> pd.DataFrame:
if 'isBackground' not in df.columns:
return pd.DataFrame()
return df[df['isBackground'] == True].copy() # noqa: E712

View file

@ -425,7 +425,7 @@ def compute_color_source_matrix(card_library: Dict[str, dict], full_df) -> Dict[
matrix: Dict[str, Dict[str, int]] = {}
lookup = {}
if full_df is not None and not getattr(full_df, 'empty', True) and 'name' in full_df.columns:
for _, r in full_df.iterrows(): # type: ignore[attr-defined]
for _, r in full_df.iterrows():
nm = str(r.get('name', ''))
if nm and nm not in lookup:
lookup[nm] = r
@ -850,7 +850,7 @@ def select_top_land_candidates(df, already: set[str], basics: set[str], top_n: i
out: list[tuple[int,str,str,str]] = []
if df is None or getattr(df, 'empty', True):
return out
for _, row in df.iterrows(): # type: ignore[attr-defined]
for _, row in df.iterrows():
try:
name = str(row.get('name',''))
if not name or name in already or name in basics:
@ -1114,7 +1114,7 @@ def prefer_owned_first(df, owned_names_lower: set[str], name_col: str = 'name'):
# ---------------------------------------------------------------------------
# Tag-driven land suggestion helpers
# ---------------------------------------------------------------------------
def build_tag_driven_suggestions(builder) -> list[dict]: # type: ignore[override]
def build_tag_driven_suggestions(builder) -> list[dict]:
"""Return a list of suggestion dicts based on selected commander tags.
Each dict fields:
@ -1202,7 +1202,7 @@ def color_balance_addition_candidates(builder, target_color: str, combined_df) -
return []
existing = set(builder.card_library.keys())
out: list[tuple[str, int]] = []
for _, row in combined_df.iterrows(): # type: ignore[attr-defined]
for _, row in combined_df.iterrows():
name = str(row.get('name', ''))
if not name or name in existing or any(name == o[0] for o in out):
continue

View file

@ -25,11 +25,11 @@ No behavior change intended.
# Attempt to use a fast fuzzy library; fall back gracefully
try:
from rapidfuzz import process as rf_process, fuzz as rf_fuzz # type: ignore
from rapidfuzz import process as rf_process, fuzz as rf_fuzz
_FUZZ_BACKEND = "rapidfuzz"
except ImportError: # pragma: no cover - environment dependent
try:
from fuzzywuzzy import process as fw_process, fuzz as fw_fuzz # type: ignore
from fuzzywuzzy import process as fw_process, fuzz as fw_fuzz
_FUZZ_BACKEND = "fuzzywuzzy"
except ImportError: # pragma: no cover
_FUZZ_BACKEND = "difflib"

View file

@ -68,7 +68,7 @@ class CommanderSelectionMixin:
out_words[0] = out_words[0][:1].upper() + out_words[0][1:]
return ' '.join(out_words)
def choose_commander(self) -> str: # type: ignore[override]
def choose_commander(self) -> str:
df = self.load_commander_data()
names = df["name"].tolist()
while True:
@ -113,7 +113,7 @@ class CommanderSelectionMixin:
continue
query = self._normalize_commander_query(choice) # treat as new (normalized) query
def _present_commander_and_confirm(self, df: pd.DataFrame, name: str) -> bool: # type: ignore[override]
def _present_commander_and_confirm(self, df: pd.DataFrame, name: str) -> bool:
row = df[df["name"] == name].iloc[0]
pretty = self._format_commander_pretty(row)
self.output_func("\n" + pretty)
@ -126,7 +126,7 @@ class CommanderSelectionMixin:
return False
self.output_func("Please enter y or n.")
def _apply_commander_selection(self, row: pd.Series): # type: ignore[override]
def _apply_commander_selection(self, row: pd.Series):
self.commander_name = row["name"]
self.commander_row = row
tags_value = row.get("themeTags", [])
@ -136,7 +136,7 @@ class CommanderSelectionMixin:
# ---------------------------
# Tag Prioritization
# ---------------------------
def select_commander_tags(self) -> List[str]: # type: ignore[override]
def select_commander_tags(self) -> List[str]:
if not self.commander_name:
self.output_func("No commander chosen yet. Selecting commander first...")
self.choose_commander()
@ -173,7 +173,7 @@ class CommanderSelectionMixin:
self._update_commander_dict_with_selected_tags()
return self.selected_tags
def _prompt_tag_choice(self, available: List[str], prompt_text: str, allow_stop: bool) -> Optional[str]: # type: ignore[override]
def _prompt_tag_choice(self, available: List[str], prompt_text: str, allow_stop: bool) -> Optional[str]:
while True:
self.output_func("\nCurrent options:")
for i, t in enumerate(available, 1):
@ -192,7 +192,7 @@ class CommanderSelectionMixin:
return matches[0]
self.output_func("Invalid selection. Try again.")
def _update_commander_dict_with_selected_tags(self): # type: ignore[override]
def _update_commander_dict_with_selected_tags(self):
if not self.commander_dict and self.commander_row is not None:
self._initialize_commander_dict(self.commander_row)
if not self.commander_dict:
@ -205,7 +205,7 @@ class CommanderSelectionMixin:
# ---------------------------
# Power Bracket Selection
# ---------------------------
def select_power_bracket(self) -> BracketDefinition: # type: ignore[override]
def select_power_bracket(self) -> BracketDefinition:
if self.bracket_definition:
return self.bracket_definition
self.output_func("\nChoose Deck Power Bracket:")
@ -229,14 +229,14 @@ class CommanderSelectionMixin:
return match
self.output_func("Invalid input. Type 1-5 or 'info'.")
def _print_bracket_details(self): # type: ignore[override]
def _print_bracket_details(self):
self.output_func("\nBracket Details:")
for bd in BRACKET_DEFINITIONS:
self.output_func(f"\n[{bd.level}] {bd.name}")
self.output_func(bd.long_desc)
self.output_func(self._format_limits(bd.limits))
def _print_selected_bracket_summary(self): # type: ignore[override]
def _print_selected_bracket_summary(self):
self.output_func("\nBracket Constraints:")
if self.bracket_limits:
self.output_func(self._format_limits(self.bracket_limits))

View file

@ -22,7 +22,7 @@ Expected attributes / methods on the host DeckBuilder:
class LandBasicsMixin:
def add_basic_lands(self): # type: ignore[override]
def add_basic_lands(self):
"""Add basic (or snow basic) lands based on color identity.
Logic:
@ -71,8 +71,8 @@ class LandBasicsMixin:
basic_min: Optional[int] = None
land_total: Optional[int] = None
if hasattr(self, 'ideal_counts') and getattr(self, 'ideal_counts'):
basic_min = self.ideal_counts.get('basic_lands') # type: ignore[attr-defined]
land_total = self.ideal_counts.get('lands') # type: ignore[attr-defined]
basic_min = self.ideal_counts.get('basic_lands')
land_total = self.ideal_counts.get('lands')
if basic_min is None:
basic_min = getattr(bc, 'DEFAULT_BASIC_LAND_COUNT', 20)
if land_total is None:
@ -136,7 +136,7 @@ class LandBasicsMixin:
self.output_func(f" {name.ljust(width)} : {cnt}")
self.output_func(f" Total Basics : {sum(allocation.values())} (Target {target_basics}, Min {basic_min})")
def run_land_step1(self): # type: ignore[override]
def run_land_step1(self):
"""Public wrapper to execute land building step 1 (basics)."""
self.add_basic_lands()
try:

View file

@ -21,7 +21,7 @@ Host DeckBuilder must provide:
"""
class LandDualsMixin:
def add_dual_lands(self, requested_count: int | None = None): # type: ignore[override]
def add_dual_lands(self, requested_count: int | None = None):
"""Add two-color 'typed' dual lands based on color identity."""
if not getattr(self, 'files_to_load', []):
try:
@ -117,10 +117,10 @@ class LandDualsMixin:
pair_buckets[key] = names
min_basic_cfg = getattr(bc, 'DEFAULT_BASIC_LAND_COUNT', 20)
if getattr(self, 'ideal_counts', None):
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg) # type: ignore[attr-defined]
basic_floor = self._basic_floor(min_basic_cfg) # type: ignore[attr-defined]
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg)
basic_floor = self._basic_floor(min_basic_cfg)
default_dual_target = getattr(bc, 'DUAL_LAND_DEFAULT_COUNT', 6)
remaining_capacity = max(0, land_target - self._current_land_count()) # type: ignore[attr-defined]
remaining_capacity = max(0, land_target - self._current_land_count())
effective_default = min(default_dual_target, remaining_capacity if remaining_capacity>0 else len(pool), len(pool))
desired = effective_default if requested_count is None else max(0, int(requested_count))
if desired == 0:
@ -129,14 +129,14 @@ class LandDualsMixin:
if remaining_capacity == 0 and desired > 0:
slots_needed = desired
freed_slots = 0
while freed_slots < slots_needed and self._count_basic_lands() > basic_floor: # type: ignore[attr-defined]
target_basic = self._choose_basic_to_trim() # type: ignore[attr-defined]
if not target_basic or not self._decrement_card(target_basic): # type: ignore[attr-defined]
while freed_slots < slots_needed and self._count_basic_lands() > basic_floor:
target_basic = self._choose_basic_to_trim()
if not target_basic or not self._decrement_card(target_basic):
break
freed_slots += 1
if freed_slots == 0:
desired = 0
remaining_capacity = max(0, land_target - self._current_land_count()) # type: ignore[attr-defined]
remaining_capacity = max(0, land_target - self._current_land_count())
desired = min(desired, remaining_capacity, len(pool))
if desired <= 0:
self.output_func("Dual Lands: No capacity after trimming; skipping.")
@ -146,7 +146,7 @@ class LandDualsMixin:
rng = getattr(self, 'rng', None)
try:
if rng:
rng.shuffle(bucket_keys) # type: ignore
rng.shuffle(bucket_keys)
else:
random.shuffle(bucket_keys)
except Exception:
@ -171,7 +171,7 @@ class LandDualsMixin:
break
added: List[str] = []
for name in chosen:
if self._current_land_count() >= land_target: # type: ignore[attr-defined]
if self._current_land_count() >= land_target:
break
# Determine sub_role as concatenated color pair for traceability
try:
@ -198,7 +198,7 @@ class LandDualsMixin:
role='dual',
sub_role=sub_role,
added_by='lands_step5'
) # type: ignore[attr-defined]
)
added.append(name)
self.output_func("\nDual Lands Added (Step 5):")
if not added:
@ -207,11 +207,11 @@ class LandDualsMixin:
width = max(len(n) for n in added)
for n in added:
self.output_func(f" {n.ljust(width)} : 1")
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}") # type: ignore[attr-defined]
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}")
def run_land_step5(self, requested_count: int | None = None): # type: ignore[override]
def run_land_step5(self, requested_count: int | None = None):
self.add_dual_lands(requested_count=requested_count)
self._enforce_land_cap(step_label="Duals (Step 5)") # type: ignore[attr-defined]
self._enforce_land_cap(step_label="Duals (Step 5)")
try:
from .. import builder_utils as _bu
_bu.export_current_land_pool(self, '5')

View file

@ -19,7 +19,7 @@ Host DeckBuilder must supply:
"""
class LandFetchMixin:
def add_fetch_lands(self, requested_count: int | None = None): # type: ignore[override]
def add_fetch_lands(self, requested_count: int | None = None):
"""Add fetch lands (color-specific + generic) respecting land target."""
if not getattr(self, 'files_to_load', []):
try:
@ -28,8 +28,8 @@ class LandFetchMixin:
except Exception as e: # pragma: no cover - defensive
self.output_func(f"Cannot add fetch lands until color identity resolved: {e}")
return
land_target = (getattr(self, 'ideal_counts', {}).get('lands') if getattr(self, 'ideal_counts', None) else None) or getattr(bc, 'DEFAULT_LAND_COUNT', 35) # type: ignore[attr-defined]
current = self._current_land_count() # type: ignore[attr-defined]
land_target = (getattr(self, 'ideal_counts', {}).get('lands') if getattr(self, 'ideal_counts', None) else None) or getattr(bc, 'DEFAULT_LAND_COUNT', 35)
current = self._current_land_count()
color_order = [c for c in getattr(self, 'color_identity', []) if c in ['W','U','B','R','G']]
color_map = getattr(bc, 'COLOR_TO_FETCH_LANDS', {})
candidates: List[str] = []
@ -56,7 +56,7 @@ class LandFetchMixin:
self.output_func("\nAdd Fetch Lands (Step 4):")
self.output_func("Fetch lands help fix colors & enable landfall / graveyard synergies.")
prompt = f"Enter desired number of fetch lands (default: {effective_default}):"
desired = self._prompt_int_with_default(prompt + ' ', effective_default, minimum=0, maximum=20) # type: ignore[attr-defined]
desired = self._prompt_int_with_default(prompt + ' ', effective_default, minimum=0, maximum=20)
else:
desired = max(0, int(requested_count))
if desired > remaining_fetch_slots:
@ -70,20 +70,20 @@ class LandFetchMixin:
if remaining_capacity == 0 and desired > 0:
min_basic_cfg = getattr(bc, 'DEFAULT_BASIC_LAND_COUNT', 20)
if getattr(self, 'ideal_counts', None):
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg) # type: ignore[attr-defined]
floor_basics = self._basic_floor(min_basic_cfg) # type: ignore[attr-defined]
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg)
floor_basics = self._basic_floor(min_basic_cfg)
slots_needed = desired
while slots_needed > 0 and self._count_basic_lands() > floor_basics: # type: ignore[attr-defined]
target_basic = self._choose_basic_to_trim() # type: ignore[attr-defined]
if not target_basic or not self._decrement_card(target_basic): # type: ignore[attr-defined]
while slots_needed > 0 and self._count_basic_lands() > floor_basics:
target_basic = self._choose_basic_to_trim()
if not target_basic or not self._decrement_card(target_basic):
break
slots_needed -= 1
remaining_capacity = max(0, land_target - self._current_land_count()) # type: ignore[attr-defined]
remaining_capacity = max(0, land_target - self._current_land_count())
if remaining_capacity > 0 and slots_needed == 0:
break
if slots_needed > 0 and remaining_capacity == 0:
desired -= slots_needed
remaining_capacity = max(0, land_target - self._current_land_count()) # type: ignore[attr-defined]
remaining_capacity = max(0, land_target - self._current_land_count())
desired = min(desired, remaining_capacity, len(candidates), remaining_fetch_slots)
if desired <= 0:
self.output_func("Fetch Lands: No capacity (after trimming) or desired reduced to 0; skipping.")
@ -101,7 +101,7 @@ class LandFetchMixin:
if k >= len(pool):
return pool.copy()
try:
return (rng.sample if rng else random.sample)(pool, k) # type: ignore
return (rng.sample if rng else random.sample)(pool, k)
except Exception:
return pool[:k]
need = desired
@ -117,7 +117,7 @@ class LandFetchMixin:
added: List[str] = []
for nm in chosen:
if self._current_land_count() >= land_target: # type: ignore[attr-defined]
if self._current_land_count() >= land_target:
break
note = 'generic' if nm in generic_list else 'color-specific'
self.add_card(
@ -126,11 +126,11 @@ class LandFetchMixin:
role='fetch',
sub_role=note,
added_by='lands_step4'
) # type: ignore[attr-defined]
)
added.append(nm)
# Record actual number of fetch lands added for export/replay context
try:
setattr(self, 'fetch_count', len(added)) # type: ignore[attr-defined]
setattr(self, 'fetch_count', len(added))
except Exception:
pass
self.output_func("\nFetch Lands Added (Step 4):")
@ -141,9 +141,9 @@ class LandFetchMixin:
for n in added:
note = 'generic' if n in generic_list else 'color-specific'
self.output_func(f" {n.ljust(width)} : 1 ({note})")
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}") # type: ignore[attr-defined]
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}")
def run_land_step4(self, requested_count: int | None = None): # type: ignore[override]
def run_land_step4(self, requested_count: int | None = None):
"""Public wrapper to add fetch lands.
If ideal_counts['fetch_lands'] is set, it will be used to bypass the prompt in both CLI and web builds.
@ -155,7 +155,7 @@ class LandFetchMixin:
except Exception:
desired = requested_count
self.add_fetch_lands(requested_count=desired)
self._enforce_land_cap(step_label="Fetch (Step 4)") # type: ignore[attr-defined]
self._enforce_land_cap(step_label="Fetch (Step 4)")
try:
from .. import builder_utils as _bu
_bu.export_current_land_pool(self, '4')

View file

@ -20,7 +20,7 @@ Host DeckBuilder must provide:
"""
class LandKindredMixin:
def add_kindred_lands(self): # type: ignore[override]
def add_kindred_lands(self):
"""Add kindred-oriented lands ONLY if a selected tag includes 'Kindred' or 'Tribal'.
Baseline inclusions on kindred focus:
@ -41,32 +41,32 @@ class LandKindredMixin:
self.output_func("Kindred Lands: No selected kindred/tribal tag; skipping.")
return
if hasattr(self, 'ideal_counts') and getattr(self, 'ideal_counts'):
land_target = self.ideal_counts.get('lands', getattr(bc, 'DEFAULT_LAND_COUNT', 35)) # type: ignore[attr-defined]
land_target = self.ideal_counts.get('lands', getattr(bc, 'DEFAULT_LAND_COUNT', 35))
else:
land_target = getattr(bc, 'DEFAULT_LAND_COUNT', 35)
min_basic_cfg = getattr(bc, 'DEFAULT_BASIC_LAND_COUNT', 20)
if hasattr(self, 'ideal_counts') and getattr(self, 'ideal_counts'):
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg) # type: ignore[attr-defined]
basic_floor = self._basic_floor(min_basic_cfg) # type: ignore[attr-defined]
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg)
basic_floor = self._basic_floor(min_basic_cfg)
def ensure_capacity() -> bool:
if self._current_land_count() < land_target: # type: ignore[attr-defined]
if self._current_land_count() < land_target:
return True
if self._count_basic_lands() <= basic_floor: # type: ignore[attr-defined]
if self._count_basic_lands() <= basic_floor:
return False
target_basic = self._choose_basic_to_trim() # type: ignore[attr-defined]
target_basic = self._choose_basic_to_trim()
if not target_basic:
return False
if not self._decrement_card(target_basic): # type: ignore[attr-defined]
if not self._decrement_card(target_basic):
return False
return self._current_land_count() < land_target # type: ignore[attr-defined]
return self._current_land_count() < land_target
colors = getattr(self, 'color_identity', []) or []
added: List[str] = []
reasons: Dict[str, str] = {}
def try_add(name: str, reason: str):
if name in self.card_library: # type: ignore[attr-defined]
if name in self.card_library:
return
if not ensure_capacity():
return
@ -77,7 +77,7 @@ class LandKindredMixin:
sub_role='baseline' if reason.startswith('kindred focus') else 'tribe-specific',
added_by='lands_step3',
trigger_tag='Kindred/Tribal'
) # type: ignore[attr-defined]
)
added.append(name)
reasons[name] = reason
@ -105,14 +105,14 @@ class LandKindredMixin:
if snapshot is not None and not snapshot.empty and tribe_terms:
dynamic_limit = 5
for tribe in sorted(tribe_terms):
if self._current_land_count() >= land_target or dynamic_limit <= 0: # type: ignore[attr-defined]
if self._current_land_count() >= land_target or dynamic_limit <= 0:
break
tribe_lower = tribe.lower()
matches: List[str] = []
for _, row in snapshot.iterrows():
try:
nm = str(row.get('name', ''))
if not nm or nm in self.card_library: # type: ignore[attr-defined]
if not nm or nm in self.card_library:
continue
tline = str(row.get('type', row.get('type_line', ''))).lower()
if 'land' not in tline:
@ -125,7 +125,7 @@ class LandKindredMixin:
except Exception:
continue
for nm in matches[:2]:
if self._current_land_count() >= land_target or dynamic_limit <= 0: # type: ignore[attr-defined]
if self._current_land_count() >= land_target or dynamic_limit <= 0:
break
if nm in added or nm in getattr(bc, 'BASIC_LANDS', []):
continue
@ -139,12 +139,12 @@ class LandKindredMixin:
width = max(len(n) for n in added)
for n in added:
self.output_func(f" {n.ljust(width)} : 1 ({reasons.get(n,'')})")
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}") # type: ignore[attr-defined]
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}")
def run_land_step3(self): # type: ignore[override]
def run_land_step3(self):
"""Public wrapper to add kindred-focused lands."""
self.add_kindred_lands()
self._enforce_land_cap(step_label="Kindred (Step 3)") # type: ignore[attr-defined]
self._enforce_land_cap(step_label="Kindred (Step 3)")
try:
from .. import builder_utils as _bu
_bu.export_current_land_pool(self, '3')

View file

@ -19,7 +19,7 @@ class LandMiscUtilityMixin:
- Diagnostics & CSV exports
"""
def add_misc_utility_lands(self, requested_count: Optional[int] = None): # type: ignore[override]
def add_misc_utility_lands(self, requested_count: Optional[int] = None):
# --- Initialization & candidate collection ---
if not getattr(self, 'files_to_load', None):
try:
@ -293,7 +293,7 @@ class LandMiscUtilityMixin:
if getattr(self, 'show_diagnostics', False) and filtered_out:
self.output_func(f" (Mono-color excluded candidates: {', '.join(filtered_out)})")
def run_land_step7(self, requested_count: Optional[int] = None): # type: ignore[override]
def run_land_step7(self, requested_count: Optional[int] = None):
self.add_misc_utility_lands(requested_count=requested_count)
self._enforce_land_cap(step_label="Utility (Step 7)")
self._build_tag_driven_land_suggestions()
@ -305,12 +305,12 @@ class LandMiscUtilityMixin:
pass
# ---- Tag-driven suggestion helpers (used after Step 7) ----
def _build_tag_driven_land_suggestions(self): # type: ignore[override]
def _build_tag_driven_land_suggestions(self):
suggestions = bu.build_tag_driven_suggestions(self)
if suggestions:
self.suggested_lands_queue.extend(suggestions)
def _apply_land_suggestions_if_room(self): # type: ignore[override]
def _apply_land_suggestions_if_room(self):
if not self.suggested_lands_queue:
return
land_target = getattr(self, 'ideal_counts', {}).get('lands', getattr(bc, 'DEFAULT_LAND_COUNT', 35)) if getattr(self, 'ideal_counts', None) else getattr(bc, 'DEFAULT_LAND_COUNT', 35)

View file

@ -12,7 +12,7 @@ class LandOptimizationMixin:
Provides optimize_tapped_lands and run_land_step8 (moved from monolithic builder).
"""
def optimize_tapped_lands(self): # type: ignore[override]
def optimize_tapped_lands(self):
df = getattr(self, '_combined_cards_df', None)
if df is None or df.empty:
return
@ -146,7 +146,7 @@ class LandOptimizationMixin:
new_tapped += 1
self.output_func(f" Tapped Lands After : {new_tapped} (threshold {threshold})")
def run_land_step8(self): # type: ignore[override]
def run_land_step8(self):
self.optimize_tapped_lands()
self._enforce_land_cap(step_label="Tapped Opt (Step 8)")
if self.color_source_matrix_baseline is None:

View file

@ -27,10 +27,10 @@ class LandStaplesMixin:
# ---------------------------
# Land Building Step 2: Staple Nonbasic Lands (NO Kindred yet)
# ---------------------------
def _current_land_count(self) -> int: # type: ignore[override]
def _current_land_count(self) -> int:
"""Return total number of land cards currently in the library (counts duplicates)."""
total = 0
for name, entry in self.card_library.items(): # type: ignore[attr-defined]
for name, entry in self.card_library.items():
ctype = entry.get('Card Type', '')
if ctype and 'land' in ctype.lower():
total += entry.get('Count', 1)
@ -47,7 +47,7 @@ class LandStaplesMixin:
continue
return total
def add_staple_lands(self): # type: ignore[override]
def add_staple_lands(self):
"""Add generic staple lands defined in STAPLE_LAND_CONDITIONS (excluding kindred lands).
Respects total land target (ideal_counts['lands']). Skips additions once target reached.
@ -62,25 +62,25 @@ class LandStaplesMixin:
return
land_target = None
if hasattr(self, 'ideal_counts') and getattr(self, 'ideal_counts'):
land_target = self.ideal_counts.get('lands') # type: ignore[attr-defined]
land_target = self.ideal_counts.get('lands')
if land_target is None:
land_target = getattr(bc, 'DEFAULT_LAND_COUNT', 35)
min_basic_cfg = getattr(bc, 'DEFAULT_BASIC_LAND_COUNT', 20)
if hasattr(self, 'ideal_counts') and getattr(self, 'ideal_counts'):
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg) # type: ignore[attr-defined]
basic_floor = self._basic_floor(min_basic_cfg) # type: ignore[attr-defined]
min_basic_cfg = self.ideal_counts.get('basic_lands', min_basic_cfg)
basic_floor = self._basic_floor(min_basic_cfg)
def ensure_capacity() -> bool:
if self._current_land_count() < land_target: # type: ignore[attr-defined]
if self._current_land_count() < land_target:
return True
if self._count_basic_lands() <= basic_floor: # type: ignore[attr-defined]
if self._count_basic_lands() <= basic_floor:
return False
target_basic = self._choose_basic_to_trim() # type: ignore[attr-defined]
target_basic = self._choose_basic_to_trim()
if not target_basic:
return False
if not self._decrement_card(target_basic): # type: ignore[attr-defined]
if not self._decrement_card(target_basic):
return False
return self._current_land_count() < land_target # type: ignore[attr-defined]
return self._current_land_count() < land_target
commander_tags_all = set(getattr(self, 'commander_tags', []) or []) | set(getattr(self, 'selected_tags', []) or [])
colors = getattr(self, 'color_identity', []) or []
@ -102,7 +102,7 @@ class LandStaplesMixin:
if not ensure_capacity():
self.output_func("Staple Lands: Cannot free capacity without violating basic floor; stopping additions.")
break
if land_name in self.card_library: # type: ignore[attr-defined]
if land_name in self.card_library:
continue
try:
include = cond(list(commander_tags_all), colors, commander_power)
@ -115,7 +115,7 @@ class LandStaplesMixin:
role='staple',
sub_role='generic-staple',
added_by='lands_step2'
) # type: ignore[attr-defined]
)
added.append(land_name)
if land_name == 'Command Tower':
reasons[land_name] = f"multi-color ({len(colors)} colors)"
@ -137,12 +137,12 @@ class LandStaplesMixin:
for n in added:
reason = reasons.get(n, '')
self.output_func(f" {n.ljust(width)} : 1 {('(' + reason + ')') if reason else ''}")
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}") # type: ignore[attr-defined]
self.output_func(f" Land Count Now : {self._current_land_count()} / {land_target}")
def run_land_step2(self): # type: ignore[override]
def run_land_step2(self):
"""Public wrapper for adding generic staple nonbasic lands (excluding kindred)."""
self.add_staple_lands()
self._enforce_land_cap(step_label="Staples (Step 2)") # type: ignore[attr-defined]
self._enforce_land_cap(step_label="Staples (Step 2)")
try:
from .. import builder_utils as _bu
_bu.export_current_land_pool(self, '2')

View file

@ -59,7 +59,7 @@ class LandTripleMixin:
'forest': 'G',
}
for _, row in df.iterrows(): # type: ignore
for _, row in df.iterrows():
try:
name = str(row.get('name',''))
if not name or name in self.card_library:

View file

@ -33,7 +33,7 @@ class CreatureAdditionMixin:
self.output_func("Card pool missing 'type' column; cannot add creatures.")
return
try:
context = self.get_theme_context() # type: ignore[attr-defined]
context = self.get_theme_context()
except Exception:
context = None
if context is None or not getattr(context, 'ordered_targets', []):
@ -480,7 +480,7 @@ class CreatureAdditionMixin:
drop_idx = tags_series.apply(lambda lst, nd=needles: any(any(n in t for n in nd) for t in lst))
mask_keep = [mk and (not di) for mk, di in zip(mask_keep, drop_idx.tolist())]
try:
import pandas as _pd # type: ignore
import pandas as _pd
mask_keep = _pd.Series(mask_keep, index=df.index)
except Exception:
pass

View file

@ -78,7 +78,7 @@ class SpellAdditionMixin:
# Combine into keep mask
mask_keep = [mk and (not di) for mk, di in zip(mask_keep, drop_idx.tolist())]
try:
import pandas as _pd # type: ignore
import pandas as _pd
mask_keep = _pd.Series(mask_keep, index=df.index)
except Exception:
pass
@ -742,7 +742,7 @@ class SpellAdditionMixin:
if df is None or df.empty or 'type' not in df.columns:
return
try:
context = self.get_theme_context() # type: ignore[attr-defined]
context = self.get_theme_context()
except Exception:
context = None
if context is None or not getattr(context, 'ordered_targets', []):

View file

@ -14,7 +14,7 @@ from ..shared_copy import build_land_headline, dfc_card_note
logger = logging_util.logging.getLogger(__name__)
try:
from prettytable import PrettyTable # type: ignore
from prettytable import PrettyTable
except Exception: # pragma: no cover
PrettyTable = None # type: ignore
@ -176,7 +176,7 @@ class ReportingMixin:
"""
try:
# Lazy import to avoid cycles
from deck_builder.enforcement import enforce_bracket_compliance # type: ignore
from deck_builder.enforcement import enforce_bracket_compliance
except Exception:
self.output_func("Enforcement module unavailable.")
return {}
@ -194,7 +194,7 @@ class ReportingMixin:
if int(total_cards) < 100 and hasattr(self, 'fill_remaining_theme_spells'):
before = int(total_cards)
try:
self.fill_remaining_theme_spells() # type: ignore[attr-defined]
self.fill_remaining_theme_spells()
except Exception:
pass
# Recompute after filler
@ -239,13 +239,13 @@ class ReportingMixin:
csv_name = base_stem + ".csv"
txt_name = base_stem + ".txt"
# Overwrite exports with updated library
self.export_decklist_csv(directory='deck_files', filename=csv_name, suppress_output=True) # type: ignore[attr-defined]
self.export_decklist_text(directory='deck_files', filename=txt_name, suppress_output=True) # type: ignore[attr-defined]
self.export_decklist_csv(directory='deck_files', filename=csv_name, suppress_output=True)
self.export_decklist_text(directory='deck_files', filename=txt_name, suppress_output=True)
# Re-export the JSON config to reflect any changes from enforcement
json_name = base_stem + ".json"
self.export_run_config_json(directory='config', filename=json_name, suppress_output=True) # type: ignore[attr-defined]
self.export_run_config_json(directory='config', filename=json_name, suppress_output=True)
# Recompute and write compliance next to them
self.compute_and_print_compliance(base_stem=base_stem) # type: ignore[attr-defined]
self.compute_and_print_compliance(base_stem=base_stem)
# Inject enforcement details into the saved compliance JSON for UI transparency
comp_path = _os.path.join('deck_files', f"{base_stem}_compliance.json")
try:
@ -259,18 +259,18 @@ class ReportingMixin:
pass
else:
# Fall back to default export flow
csv_path = self.export_decklist_csv() # type: ignore[attr-defined]
csv_path = self.export_decklist_csv()
try:
base, _ = _os.path.splitext(csv_path)
base_only = _os.path.basename(base)
except Exception:
base_only = None
self.export_decklist_text(filename=(base_only + '.txt') if base_only else None) # type: ignore[attr-defined]
self.export_decklist_text(filename=(base_only + '.txt') if base_only else None)
# Re-export JSON config after enforcement changes
if base_only:
self.export_run_config_json(directory='config', filename=base_only + '.json', suppress_output=True) # type: ignore[attr-defined]
self.export_run_config_json(directory='config', filename=base_only + '.json', suppress_output=True)
if base_only:
self.compute_and_print_compliance(base_stem=base_only) # type: ignore[attr-defined]
self.compute_and_print_compliance(base_stem=base_only)
# Inject enforcement into written JSON as above
try:
comp_path = _os.path.join('deck_files', f"{base_only}_compliance.json")
@ -294,7 +294,7 @@ class ReportingMixin:
"""
try:
# Late import to avoid circulars in some environments
from deck_builder.brackets_compliance import evaluate_deck # type: ignore
from deck_builder.brackets_compliance import evaluate_deck
except Exception:
self.output_func("Bracket compliance module unavailable.")
return {}
@ -373,7 +373,7 @@ class ReportingMixin:
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
snapshot = full_df if full_df is not None else combined_df
row_lookup: Dict[str, any] = {}
row_lookup: Dict[str, Any] = {}
if snapshot is not None and hasattr(snapshot, 'empty') and not snapshot.empty and 'name' in snapshot.columns:
for _, r in snapshot.iterrows():
nm = str(r.get('name'))
@ -429,7 +429,7 @@ class ReportingMixin:
# Surface land vs. MDFC counts for CLI users to mirror web summary copy
try:
summary = self.build_deck_summary() # type: ignore[attr-defined]
summary = self.build_deck_summary()
except Exception:
summary = None
if isinstance(summary, dict):
@ -483,9 +483,9 @@ class ReportingMixin:
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
snapshot = full_df if full_df is not None else combined_df
row_lookup: Dict[str, any] = {}
row_lookup: Dict[str, Any] = {}
if snapshot is not None and not getattr(snapshot, 'empty', True) and 'name' in snapshot.columns:
for _, r in snapshot.iterrows(): # type: ignore[attr-defined]
for _, r in snapshot.iterrows():
nm = str(r.get('name'))
if nm and nm not in row_lookup:
row_lookup[nm] = r
@ -521,7 +521,7 @@ class ReportingMixin:
builder_utils_module = None
try:
from deck_builder import builder_utils as _builder_utils # type: ignore
from deck_builder import builder_utils as _builder_utils
builder_utils_module = _builder_utils
color_matrix = builder_utils_module.compute_color_source_matrix(self.card_library, full_df)
except Exception:
@ -856,7 +856,7 @@ class ReportingMixin:
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
snapshot = full_df if full_df is not None else combined_df
row_lookup: Dict[str, any] = {}
row_lookup: Dict[str, Any] = {}
if snapshot is not None and not snapshot.empty and 'name' in snapshot.columns:
for _, r in snapshot.iterrows():
nm = str(r.get('name'))
@ -1128,7 +1128,7 @@ class ReportingMixin:
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
snapshot = full_df if full_df is not None else combined_df
row_lookup: Dict[str, any] = {}
row_lookup: Dict[str, Any] = {}
if snapshot is not None and not snapshot.empty and 'name' in snapshot.columns:
for _, r in snapshot.iterrows():
nm = str(r.get('name'))
@ -1136,7 +1136,7 @@ class ReportingMixin:
row_lookup[nm] = r
try:
from deck_builder import builder_utils as _builder_utils # type: ignore
from deck_builder import builder_utils as _builder_utils
color_matrix = _builder_utils.compute_color_source_matrix(self.card_library, full_df)
except Exception:
color_matrix = {}
@ -1387,3 +1387,4 @@ class ReportingMixin:
"""
# Card library printout suppressed; use CSV and text export for card list.
pass

View file

@ -167,7 +167,7 @@ def _reset_metrics_for_test() -> None:
def _sanitize_theme_list(values: Iterable[Any]) -> list[str]:
sanitized: list[str] = []
seen: set[str] = set()
for raw in values or []: # type: ignore[arg-type]
for raw in values or []:
text = str(raw or "").strip()
if not text:
continue

View file

@ -87,7 +87,7 @@ class ThemeCatalog(BaseModel):
def theme_names(self) -> List[str]: # convenience
return [t.theme for t in self.themes]
def model_post_init(self, __context: Any) -> None: # type: ignore[override]
def model_post_init(self, __context: Any) -> None:
# If only legacy 'provenance' provided, alias to metadata_info
if self.metadata_info is None and self.provenance is not None:
object.__setattr__(self, 'metadata_info', self.provenance)
@ -135,7 +135,7 @@ class ThemeYAMLFile(BaseModel):
model_config = ConfigDict(extra='forbid')
def model_post_init(self, __context: Any) -> None: # type: ignore[override]
def model_post_init(self, __context: Any) -> None:
if not self.metadata_info and self.provenance:
object.__setattr__(self, 'metadata_info', self.provenance)
if self.metadata_info and self.provenance:

View file

@ -19,9 +19,9 @@ from contextlib import asynccontextmanager
from code.deck_builder.summary_telemetry import get_mdfc_metrics, get_partner_metrics, get_theme_metrics
from tagging.multi_face_merger import load_merge_summary
from .services.combo_utils import detect_all as _detect_all
from .services.theme_catalog_loader import prewarm_common_filters, load_index # type: ignore
from .services.commander_catalog_loader import load_commander_catalog # type: ignore
from .services.tasks import get_session, new_sid, set_session_value # type: ignore
from .services.theme_catalog_loader import prewarm_common_filters, load_index
from .services.commander_catalog_loader import load_commander_catalog
from .services.tasks import get_session, new_sid, set_session_value
# Logger for app-level logging
logger = logging.getLogger(__name__)
@ -56,18 +56,18 @@ async def _lifespan(app: FastAPI): # pragma: no cover - simple infra glue
except Exception:
pass
try:
commanders_routes.prewarm_default_page() # type: ignore[attr-defined]
commanders_routes.prewarm_default_page()
except Exception:
pass
# Warm preview card index once (updated Phase A: moved to card_index module)
try: # local import to avoid cost if preview unused
from .services.card_index import maybe_build_index # type: ignore
from .services.card_index import maybe_build_index
maybe_build_index()
except Exception:
pass
# Warm card browser theme catalog (fast CSV read) and theme index (slower card parsing)
try:
from .routes.card_browser import get_theme_catalog, get_theme_index # type: ignore
from .routes.card_browser import get_theme_catalog, get_theme_index
get_theme_catalog() # Fast: just reads CSV
get_theme_index() # Slower: parses cards for theme-to-card mapping
except Exception:
@ -76,7 +76,7 @@ async def _lifespan(app: FastAPI): # pragma: no cover - simple infra glue
try:
from code.settings import ENABLE_CARD_DETAILS
if ENABLE_CARD_DETAILS:
from .routes.card_browser import get_similarity # type: ignore
from .routes.card_browser import get_similarity
get_similarity() # Pre-initialize singleton (one-time cost: ~2-3s)
except Exception:
pass
@ -89,7 +89,7 @@ app.add_middleware(GZipMiddleware, minimum_size=500)
# Mount static if present
if _STATIC_DIR.exists():
class CacheStatic(StaticFiles):
async def get_response(self, path, scope): # type: ignore[override]
async def get_response(self, path, scope):
resp = await super().get_response(path, scope)
try:
# Add basic cache headers for static assets
@ -133,7 +133,7 @@ templates.env.filters["card_image"] = card_image_url
# Prevents DeprecationWarning noise in tests without touching all call sites.
_orig_template_response = templates.TemplateResponse
def _compat_template_response(*args, **kwargs): # type: ignore[override]
def _compat_template_response(*args, **kwargs):
try:
if args and isinstance(args[0], str):
name = args[0]
@ -151,7 +151,7 @@ def _compat_template_response(*args, **kwargs): # type: ignore[override]
pass
return _orig_template_response(*args, **kwargs)
templates.TemplateResponse = _compat_template_response # type: ignore[assignment]
templates.TemplateResponse = _compat_template_response
# (Startup prewarm moved to lifespan handler _lifespan)
@ -327,7 +327,7 @@ templates.env.globals.update({
# Expose catalog hash (for cache versioning / service worker) best-effort, fallback to 'dev'
def _load_catalog_hash() -> str:
try: # local import to avoid circular on early load
from .services.theme_catalog_loader import CATALOG_JSON # type: ignore
from .services.theme_catalog_loader import CATALOG_JSON
if CATALOG_JSON.exists():
raw = _json.loads(CATALOG_JSON.read_text(encoding="utf-8") or "{}")
meta = raw.get("metadata_info") or {}
@ -951,7 +951,7 @@ async def status_random_theme_stats():
if not SHOW_DIAGNOSTICS:
raise HTTPException(status_code=404, detail="Not Found")
try:
from deck_builder.random_entrypoint import get_theme_tag_stats # type: ignore
from deck_builder.random_entrypoint import get_theme_tag_stats
stats = get_theme_tag_stats()
return JSONResponse({"ok": True, "stats": stats})
@ -1038,8 +1038,8 @@ async def api_random_build(request: Request):
except Exception:
timeout_s = max(0.1, float(RANDOM_TIMEOUT_MS) / 1000.0)
# Import on-demand to avoid heavy costs at module import time
from deck_builder.random_entrypoint import build_random_deck, RandomConstraintsImpossibleError # type: ignore
from deck_builder.random_entrypoint import RandomThemeNoMatchError # type: ignore
from deck_builder.random_entrypoint import build_random_deck, RandomConstraintsImpossibleError
from deck_builder.random_entrypoint import RandomThemeNoMatchError
res = build_random_deck(
theme=theme,
@ -1170,7 +1170,7 @@ async def api_random_full_build(request: Request):
timeout_s = max(0.1, float(RANDOM_TIMEOUT_MS) / 1000.0)
# Build a full deck deterministically
from deck_builder.random_entrypoint import build_random_full_deck, RandomConstraintsImpossibleError # type: ignore
from deck_builder.random_entrypoint import build_random_full_deck, RandomConstraintsImpossibleError
res = build_random_full_deck(
theme=theme,
constraints=constraints,
@ -1394,7 +1394,7 @@ async def api_random_reroll(request: Request):
except Exception:
new_seed = None
if new_seed is None:
from random_util import generate_seed # type: ignore
from random_util import generate_seed
new_seed = int(generate_seed())
# Build with the new seed
@ -1405,7 +1405,7 @@ async def api_random_reroll(request: Request):
timeout_s = max(0.1, float(RANDOM_TIMEOUT_MS) / 1000.0)
attempts = body.get("attempts", int(RANDOM_MAX_ATTEMPTS))
from deck_builder.random_entrypoint import build_random_full_deck # type: ignore
from deck_builder.random_entrypoint import build_random_full_deck
res = build_random_full_deck(
theme=theme,
constraints=constraints,
@ -1786,10 +1786,10 @@ async def hx_random_reroll(request: Request):
except Exception:
new_seed = None
if new_seed is None:
from random_util import generate_seed # type: ignore
from random_util import generate_seed
new_seed = int(generate_seed())
# Import outside conditional to avoid UnboundLocalError when branch not taken
from deck_builder.random_entrypoint import build_random_full_deck # type: ignore
from deck_builder.random_entrypoint import build_random_full_deck
try:
t0 = time.time()
_attempts = int(attempts_override) if attempts_override is not None else int(RANDOM_MAX_ATTEMPTS)
@ -1800,7 +1800,7 @@ async def hx_random_reroll(request: Request):
_timeout_s = max(0.1, float(_timeout_ms) / 1000.0)
if is_reroll_same:
build_t0 = time.time()
from headless_runner import run as _run # type: ignore
from headless_runner import run as _run
# Suppress builder's internal initial export to control artifact generation (matches full random path logic)
try:
import os as _os
@ -1813,18 +1813,18 @@ async def hx_random_reroll(request: Request):
summary = None
try:
if hasattr(builder, 'build_deck_summary'):
summary = builder.build_deck_summary() # type: ignore[attr-defined]
summary = builder.build_deck_summary()
except Exception:
summary = None
decklist = []
try:
if hasattr(builder, 'deck_list_final'):
decklist = getattr(builder, 'deck_list_final') # type: ignore[attr-defined]
decklist = getattr(builder, 'deck_list_final')
except Exception:
decklist = []
# Controlled artifact export (single pass)
csv_path = getattr(builder, 'last_csv_path', None) # type: ignore[attr-defined]
txt_path = getattr(builder, 'last_txt_path', None) # type: ignore[attr-defined]
csv_path = getattr(builder, 'last_csv_path', None)
txt_path = getattr(builder, 'last_txt_path', None)
compliance = None
try:
import os as _os
@ -1832,7 +1832,7 @@ async def hx_random_reroll(request: Request):
# Perform exactly one export sequence now
if not csv_path and hasattr(builder, 'export_decklist_csv'):
try:
csv_path = builder.export_decklist_csv() # type: ignore[attr-defined]
csv_path = builder.export_decklist_csv()
except Exception:
csv_path = None
if csv_path and isinstance(csv_path, str):
@ -1842,7 +1842,7 @@ async def hx_random_reroll(request: Request):
try:
base_name = _os.path.basename(base_path) + '.txt'
if hasattr(builder, 'export_decklist_text'):
txt_path = builder.export_decklist_text(filename=base_name) # type: ignore[attr-defined]
txt_path = builder.export_decklist_text(filename=base_name)
except Exception:
# Fallback: if a txt already exists from a prior build reuse it
if _os.path.isfile(base_path + '.txt'):
@ -1857,7 +1857,7 @@ async def hx_random_reroll(request: Request):
else:
try:
if hasattr(builder, 'compute_and_print_compliance'):
compliance = builder.compute_and_print_compliance(base_stem=_os.path.basename(base_path)) # type: ignore[attr-defined]
compliance = builder.compute_and_print_compliance(base_stem=_os.path.basename(base_path))
except Exception:
compliance = None
if summary:
@ -2051,7 +2051,7 @@ async def hx_random_reroll(request: Request):
except Exception:
_permalink = None
resp = templates.TemplateResponse(
"partials/random_result.html", # type: ignore
"partials/random_result.html",
{
"request": request,
"seed": int(res.seed),
@ -2467,7 +2467,7 @@ async def logs_page(
# Respect feature flag
raise HTTPException(status_code=404, detail="Not Found")
# Reuse status_logs logic
data = await status_logs(tail=tail, q=q, level=level) # type: ignore[arg-type]
data = await status_logs(tail=tail, q=q, level=level)
lines: list[str]
if isinstance(data, JSONResponse):
payload = data.body

View file

@ -30,7 +30,7 @@ from ..services.build_utils import (
from ..app import templates
from deck_builder import builder_constants as bc
from ..services import orchestrator as orch
from ..services.orchestrator import is_setup_ready as _is_setup_ready, is_setup_stale as _is_setup_stale # type: ignore
from ..services.orchestrator import is_setup_ready as _is_setup_ready, is_setup_stale as _is_setup_stale
from ..services.build_utils import owned_names as owned_names_helper
from ..services.tasks import get_session, new_sid
from html import escape as _esc
@ -119,7 +119,7 @@ def _available_cards_normalized() -> tuple[set[str], dict[str, str]]:
from deck_builder.include_exclude_utils import normalize_punctuation
except Exception:
# Fallback: identity normalization
def normalize_punctuation(x: str) -> str: # type: ignore
def normalize_punctuation(x: str) -> str:
return str(x).strip().casefold()
norm_map: dict[str, str] = {}
for name in names:
@ -470,7 +470,7 @@ def _background_options_from_commander_catalog() -> list[dict[str, Any]]:
seen: set[str] = set()
options: list[dict[str, Any]] = []
for record in getattr(catalog, "entries", ()): # type: ignore[attr-defined]
for record in getattr(catalog, "entries", ()):
if not getattr(record, "is_background", False):
continue
name = getattr(record, "display_name", None)
@ -2865,7 +2865,7 @@ async def build_step5_rewind(request: Request, to: str = Form(...)) -> HTMLRespo
snap = h.get("snapshot")
break
if snap is not None:
orch._restore_builder(ctx["builder"], snap) # type: ignore[attr-defined]
orch._restore_builder(ctx["builder"], snap)
ctx["idx"] = int(target_i) - 1
ctx["last_visible_idx"] = int(target_i) - 1
except Exception:
@ -3869,7 +3869,7 @@ async def build_step5_reset_stage(request: Request) -> HTMLResponse:
if not ctx or not ctx.get("snapshot"):
return await build_step5_get(request)
try:
orch._restore_builder(ctx["builder"], ctx["snapshot"]) # type: ignore[attr-defined]
orch._restore_builder(ctx["builder"], ctx["snapshot"])
except Exception:
return await build_step5_get(request)
# Re-render step 5 with cleared added list
@ -4293,7 +4293,7 @@ async def build_alternatives(
try:
if rng is not None:
return rng.sample(seq, limit) if len(seq) >= limit else list(seq)
import random as _rnd # type: ignore
import random as _rnd
return _rnd.sample(seq, limit) if len(seq) >= limit else list(seq)
except Exception:
return list(seq[:limit])
@ -4344,7 +4344,7 @@ async def build_alternatives(
# Helper: map display names
def _display_map_for(lower_pool: set[str]) -> dict[str, str]:
try:
return builder_display_map(b, lower_pool) # type: ignore[arg-type]
return builder_display_map(b, lower_pool)
except Exception:
return {nm: nm for nm in lower_pool}
@ -4522,7 +4522,7 @@ async def build_alternatives(
pass
# Sort by priority like the builder
try:
pool = bu.sort_by_priority(pool, ["edhrecRank","manaValue"]) # type: ignore[arg-type]
pool = bu.sort_by_priority(pool, ["edhrecRank","manaValue"])
except Exception:
pass
# Exclusions and ownership (for non-random roles this stays before slicing)
@ -5020,13 +5020,13 @@ async def build_compliance_panel(request: Request) -> HTMLResponse:
comp = None
try:
if hasattr(b, 'compute_and_print_compliance'):
comp = b.compute_and_print_compliance(base_stem=None) # type: ignore[attr-defined]
comp = b.compute_and_print_compliance(base_stem=None)
except Exception:
comp = None
try:
if comp:
from ..services import orchestrator as orch
comp = orch._attach_enforcement_plan(b, comp) # type: ignore[attr-defined]
comp = orch._attach_enforcement_plan(b, comp)
except Exception:
pass
if not comp:
@ -5151,11 +5151,11 @@ async def build_enforce_apply(request: Request) -> HTMLResponse:
# If missing, export once to establish base
if not base_stem:
try:
ctx["csv_path"] = b.export_decklist_csv() # type: ignore[attr-defined]
ctx["csv_path"] = b.export_decklist_csv()
import os as _os
base_stem = _os.path.splitext(_os.path.basename(ctx["csv_path"]))[0]
# Also produce a text export for completeness
ctx["txt_path"] = b.export_decklist_text(filename=base_stem + '.txt') # type: ignore[attr-defined]
ctx["txt_path"] = b.export_decklist_text(filename=base_stem + '.txt')
except Exception:
base_stem = None
# Add lock placeholders into the library before enforcement so user choices are present
@ -5200,7 +5200,7 @@ async def build_enforce_apply(request: Request) -> HTMLResponse:
pass
# Run enforcement + re-exports (tops up to 100 internally)
try:
rep = b.enforce_and_reexport(base_stem=base_stem, mode='auto') # type: ignore[attr-defined]
rep = b.enforce_and_reexport(base_stem=base_stem, mode='auto')
except Exception as e:
err_ctx = step5_error_ctx(request, sess, f"Enforcement failed: {e}")
resp = templates.TemplateResponse("build/_step5.html", err_ctx)
@ -5274,13 +5274,13 @@ async def build_enforcement_fullpage(request: Request) -> HTMLResponse:
comp = None
try:
if hasattr(b, 'compute_and_print_compliance'):
comp = b.compute_and_print_compliance(base_stem=None) # type: ignore[attr-defined]
comp = b.compute_and_print_compliance(base_stem=None)
except Exception:
comp = None
try:
if comp:
from ..services import orchestrator as orch
comp = orch._attach_enforcement_plan(b, comp) # type: ignore[attr-defined]
comp = orch._attach_enforcement_plan(b, comp)
except Exception:
pass
try:

View file

@ -425,7 +425,7 @@ async def decks_compare(request: Request, A: Optional[str] = None, B: Optional[s
mt_val = str(int(mt))
except Exception:
mt_val = "0"
options.append({"name": it.get("name"), "label": label, "mtime": mt_val}) # type: ignore[arg-type]
options.append({"name": it.get("name"), "label": label, "mtime": mt_val})
diffs = None
metaA: Dict[str, str] = {}

View file

@ -7,7 +7,7 @@ from pathlib import Path
import json as _json
from fastapi.responses import HTMLResponse, JSONResponse
from ..app import templates
from ..services.orchestrator import _ensure_setup_ready # type: ignore
from ..services.orchestrator import _ensure_setup_ready
router = APIRouter(prefix="/setup")
@ -21,7 +21,7 @@ def _kickoff_setup_async(force: bool = False):
def runner():
try:
print(f"[SETUP THREAD] Starting setup/tagging (force={force})...")
_ensure_setup_ready(print, force=force) # type: ignore[arg-type]
_ensure_setup_ready(print, force=force)
print("[SETUP THREAD] Setup/tagging completed successfully")
except Exception as e: # pragma: no cover - background best effort
try:
@ -36,7 +36,7 @@ def _kickoff_setup_async(force: bool = False):
@router.get("/running", response_class=HTMLResponse)
async def setup_running(request: Request, start: Optional[int] = 0, next: Optional[str] = None, force: Optional[bool] = None) -> HTMLResponse: # type: ignore[override]
async def setup_running(request: Request, start: Optional[int] = 0, next: Optional[str] = None, force: Optional[bool] = None) -> HTMLResponse:
# Optionally start the setup/tagging in the background if requested
try:
if start and int(start) != 0:

View file

@ -7,7 +7,7 @@ from typing import Optional, Dict, Any
from fastapi import APIRouter, Request, HTTPException, Query
from fastapi import BackgroundTasks
from ..services.orchestrator import _ensure_setup_ready, _run_theme_metadata_enrichment # type: ignore
from ..services.orchestrator import _ensure_setup_ready, _run_theme_metadata_enrichment
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from ..services.theme_catalog_loader import (
@ -17,10 +17,10 @@ from ..services.theme_catalog_loader import (
filter_slugs_fast,
summaries_for_slugs,
)
from ..services.theme_preview import get_theme_preview # type: ignore
from ..services.theme_catalog_loader import catalog_metrics, prewarm_common_filters # type: ignore
from ..services.theme_preview import preview_metrics # type: ignore
from ..services import theme_preview as _theme_preview_mod # type: ignore # for error counters
from ..services.theme_preview import get_theme_preview
from ..services.theme_catalog_loader import catalog_metrics, prewarm_common_filters
from ..services.theme_preview import preview_metrics
from ..services import theme_preview as _theme_preview_mod # for error counters
import os
from fastapi import Body
@ -36,7 +36,7 @@ router = APIRouter(prefix="/themes", tags=["themes"]) # /themes/status
# Reuse the main app's template environment so nav globals stay consistent.
try: # circular-safe import: app defines templates before importing this router
from ..app import templates as _templates # type: ignore
from ..app import templates as _templates
except Exception: # Fallback (tests/minimal contexts)
_templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / 'templates'))
@ -131,7 +131,7 @@ async def theme_suggest(
# Optional rate limit using app helper if available
rl_result = None
try:
from ..app import rate_limit_check # type: ignore
from ..app import rate_limit_check
rl_result = rate_limit_check(request, "suggest")
except HTTPException as http_ex: # propagate 429 with headers
raise http_ex
@ -231,7 +231,7 @@ async def theme_status():
yaml_file_count = 0
if yaml_catalog_exists:
try:
yaml_file_count = len([p for p in CATALOG_DIR.iterdir() if p.suffix == ".yml"]) # type: ignore[arg-type]
yaml_file_count = len([p for p in CATALOG_DIR.iterdir() if p.suffix == ".yml"])
except Exception:
yaml_file_count = -1
tagged_time = _load_tag_flag_time()
@ -547,7 +547,7 @@ async def theme_yaml(theme_id: str):
raise HTTPException(status_code=404, detail="yaml_not_found")
# Reconstruct minimal YAML (we have dict already)
import yaml as _yaml # local import to keep top-level lean
text = _yaml.safe_dump(y, sort_keys=False) # type: ignore
text = _yaml.safe_dump(y, sort_keys=False)
headers = {"Content-Type": "text/plain; charset=utf-8"}
return HTMLResponse(text, headers=headers)
@ -631,7 +631,7 @@ async def api_theme_search(
prefix: list[dict[str, Any]] = []
substr: list[dict[str, Any]] = []
seen: set[str] = set()
themes_iter = list(idx.catalog.themes) # type: ignore[attr-defined]
themes_iter = list(idx.catalog.themes)
# Phase 1 + 2: exact / prefix
for t in themes_iter:
name = t.theme

View file

@ -202,7 +202,7 @@ def commander_hover_context(
from .summary_utils import format_theme_label, format_theme_list
except Exception:
# Fallbacks in the unlikely event of circular import issues
def format_theme_label(value: Any) -> str: # type: ignore[redef]
def format_theme_label(value: Any) -> str:
text = str(value or "").strip().replace("_", " ")
if not text:
return ""
@ -214,10 +214,10 @@ def commander_hover_context(
parts.append(chunk[:1].upper() + chunk[1:].lower())
return " ".join(parts)
def format_theme_list(values: Iterable[Any]) -> list[str]: # type: ignore[redef]
def format_theme_list(values: Iterable[Any]) -> list[str]:
seen: set[str] = set()
result: list[str] = []
for raw in values or []: # type: ignore[arg-type]
for raw in values or []:
label = format_theme_label(raw)
if not label or len(label) <= 1:
continue
@ -420,7 +420,7 @@ def step5_ctx_from_result(
else:
entry = {}
try:
entry.update(vars(item)) # type: ignore[arg-type]
entry.update(vars(item))
except Exception:
pass
# Preserve common attributes when vars() empty

View file

@ -359,7 +359,7 @@ def _global_prune_disallowed_pool(b: DeckBuilder) -> None:
drop_idx = tags_series.apply(lambda lst, nd=needles: _has_any(lst, nd))
mask_keep = [mk and (not di) for mk, di in zip(mask_keep, drop_idx.tolist())]
try:
import pandas as _pd # type: ignore
import pandas as _pd
mask_keep = _pd.Series(mask_keep, index=work.index)
except Exception:
pass
@ -480,7 +480,7 @@ def commander_candidates(query: str, limit: int = 10) -> List[Tuple[str, int, Li
tmp = DeckBuilder()
try:
if hasattr(tmp, '_normalize_commander_query'):
query = tmp._normalize_commander_query(query) # type: ignore[attr-defined]
query = tmp._normalize_commander_query(query)
else:
# Light fallback: basic title case
query = ' '.join([w[:1].upper() + w[1:].lower() if w else w for w in str(query).split(' ')])
@ -653,7 +653,7 @@ def commander_select(name: str) -> Dict[str, Any]:
if row.empty:
try:
if hasattr(tmp, '_normalize_commander_query'):
name2 = tmp._normalize_commander_query(name) # type: ignore[attr-defined]
name2 = tmp._normalize_commander_query(name)
else:
name2 = ' '.join([w[:1].upper() + w[1:].lower() if w else w for w in str(name).split(' ')])
row = df[df["name"] == name2]
@ -1288,8 +1288,8 @@ def _ensure_setup_ready(out, force: bool = False) -> None:
pass
# Bust theme-related in-memory caches so new catalog reflects immediately
try:
from .theme_catalog_loader import bust_filter_cache # type: ignore
from .theme_preview import bust_preview_cache # type: ignore
from .theme_catalog_loader import bust_filter_cache
from .theme_preview import bust_preview_cache
bust_filter_cache("catalog_refresh")
bust_preview_cache("catalog_refresh")
try:
@ -1327,7 +1327,7 @@ def _ensure_setup_ready(out, force: bool = False) -> None:
try:
# M4 (Parquet Migration): Check for processed Parquet file instead of CSV
from path_util import get_processed_cards_path # type: ignore
from path_util import get_processed_cards_path
cards_path = get_processed_cards_path()
flag_path = os.path.join('csv_files', '.tagging_complete.json')
auto_setup_enabled = _is_truthy_env('WEB_AUTO_SETUP', '1')
@ -1416,7 +1416,7 @@ def _ensure_setup_ready(out, force: bool = False) -> None:
_write_status({"running": True, "phase": "setup", "message": "GitHub download failed, running local setup...", "percent": 0})
try:
from file_setup.setup import initial_setup # type: ignore
from file_setup.setup import initial_setup
# Always run initial_setup when forced or when cards are missing/stale
initial_setup()
except Exception as e:
@ -1425,7 +1425,7 @@ def _ensure_setup_ready(out, force: bool = False) -> None:
return
# M4 (Parquet Migration): Use unified run_tagging with parallel support
try:
from tagging import tagger as _tagger # type: ignore
from tagging import tagger as _tagger
use_parallel = str(os.getenv('WEB_TAG_PARALLEL', '1')).strip().lower() in {"1","true","yes","on"}
max_workers_env = os.getenv('WEB_TAG_WORKERS')
try:
@ -1466,7 +1466,7 @@ def _ensure_setup_ready(out, force: bool = False) -> None:
try:
_write_status({"running": True, "phase": "aggregating", "message": "Consolidating card data...", "percent": 90})
out("Aggregating card CSVs into Parquet files...")
from file_setup.card_aggregator import CardAggregator # type: ignore
from file_setup.card_aggregator import CardAggregator
aggregator = CardAggregator()
# Aggregate all_cards.parquet
@ -1474,7 +1474,7 @@ def _ensure_setup_ready(out, force: bool = False) -> None:
out(f"Aggregated {stats['total_cards']} cards into all_cards.parquet ({stats['file_size_mb']} MB)")
# Convert commander_cards.csv and background_cards.csv to Parquet
import pandas as pd # type: ignore
import pandas as pd
# Convert commander_cards.csv
commander_csv = 'csv_files/commander_cards.csv'
@ -1524,8 +1524,8 @@ def _ensure_setup_ready(out, force: bool = False) -> None:
# Generate / refresh theme catalog (JSON + per-theme YAML) BEFORE marking done so UI sees progress
_refresh_theme_catalog(out, force=True, fast_path=False)
try:
from .theme_catalog_loader import bust_filter_cache # type: ignore
from .theme_preview import bust_preview_cache # type: ignore
from .theme_catalog_loader import bust_filter_cache
from .theme_preview import bust_preview_cache
bust_filter_cache("tagging_complete")
bust_preview_cache("tagging_complete")
except Exception:
@ -1721,19 +1721,19 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
# Owned/Prefer-owned integration (optional for headless runs)
try:
if use_owned_only:
b.use_owned_only = True # type: ignore[attr-defined]
b.use_owned_only = True
# Prefer explicit owned_names list if provided; else let builder discover from files
if owned_names:
try:
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip()) # type: ignore[attr-defined]
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip())
except Exception:
b.owned_card_names = set() # type: ignore[attr-defined]
b.owned_card_names = set()
# Soft preference flag does not filter; only biases selection order
if prefer_owned:
try:
b.prefer_owned = True # type: ignore[attr-defined]
b.prefer_owned = True
if owned_names and not getattr(b, 'owned_card_names', None):
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip()) # type: ignore[attr-defined]
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip())
except Exception:
pass
except Exception:
@ -1751,13 +1751,13 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
# Thread combo preferences (if provided)
try:
if prefer_combos is not None:
b.prefer_combos = bool(prefer_combos) # type: ignore[attr-defined]
b.prefer_combos = bool(prefer_combos)
if combo_target_count is not None:
b.combo_target_count = int(combo_target_count) # type: ignore[attr-defined]
b.combo_target_count = int(combo_target_count)
if combo_balance:
bal = str(combo_balance).strip().lower()
if bal in ('early','late','mix'):
b.combo_balance = bal # type: ignore[attr-defined]
b.combo_balance = bal
except Exception:
pass
@ -1934,7 +1934,7 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
except Exception:
pass
if hasattr(b, 'export_decklist_csv'):
csv_path = b.export_decklist_csv() # type: ignore[attr-defined]
csv_path = b.export_decklist_csv()
except Exception as e:
out(f"CSV export failed: {e}")
try:
@ -1942,7 +1942,7 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
# Try to mirror build_deck_full behavior by displaying the contents
import os as _os
base, _ext = _os.path.splitext(_os.path.basename(csv_path)) if csv_path else (f"deck_{b.timestamp}", "")
txt_path = b.export_decklist_text(filename=base + '.txt') # type: ignore[attr-defined]
txt_path = b.export_decklist_text(filename=base + '.txt')
try:
b._display_txt_contents(txt_path)
except Exception:
@ -1950,7 +1950,7 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
# Compute bracket compliance and save JSON alongside exports
try:
if hasattr(b, 'compute_and_print_compliance'):
rep0 = b.compute_and_print_compliance(base_stem=base) # type: ignore[attr-defined]
rep0 = b.compute_and_print_compliance(base_stem=base)
# Attach planning preview (no mutation) and only auto-enforce if explicitly enabled
rep0 = _attach_enforcement_plan(b, rep0)
try:
@ -1959,7 +1959,7 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
except Exception:
_auto = False
if _auto and isinstance(rep0, dict) and rep0.get('overall') == 'FAIL' and hasattr(b, 'enforce_and_reexport'):
b.enforce_and_reexport(base_stem=base, mode='auto') # type: ignore[attr-defined]
b.enforce_and_reexport(base_stem=base, mode='auto')
except Exception:
pass
# Load compliance JSON for UI consumption
@ -1981,7 +1981,7 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
# Build structured summary for UI
try:
if hasattr(b, 'build_deck_summary'):
summary = b.build_deck_summary() # type: ignore[attr-defined]
summary = b.build_deck_summary()
except Exception:
summary = None
# Write sidecar summary JSON next to CSV (if available)
@ -1999,7 +1999,7 @@ def run_build(commander: str, tags: List[str], bracket: int, ideals: Dict[str, i
"txt": txt_path,
}
try:
commander_meta = b.get_commander_export_metadata() # type: ignore[attr-defined]
commander_meta = b.get_commander_export_metadata()
except Exception:
commander_meta = {}
names = commander_meta.get("commander_names") or []
@ -2383,21 +2383,21 @@ def _apply_combined_commander_to_builder(builder: DeckBuilder, combined: Any) ->
"""Attach combined commander metadata to the builder."""
try:
builder.combined_commander = combined # type: ignore[attr-defined]
builder.combined_commander = combined
except Exception:
pass
try:
builder.partner_mode = getattr(combined, "partner_mode", None) # type: ignore[attr-defined]
builder.partner_mode = getattr(combined, "partner_mode", None)
except Exception:
pass
try:
builder.secondary_commander = getattr(combined, "secondary_name", None) # type: ignore[attr-defined]
builder.secondary_commander = getattr(combined, "secondary_name", None)
except Exception:
pass
try:
builder.combined_color_identity = getattr(combined, "color_identity", None) # type: ignore[attr-defined]
builder.combined_theme_tags = getattr(combined, "theme_tags", None) # type: ignore[attr-defined]
builder.partner_warnings = getattr(combined, "warnings", None) # type: ignore[attr-defined]
builder.combined_color_identity = getattr(combined, "color_identity", None)
builder.combined_theme_tags = getattr(combined, "theme_tags", None)
builder.partner_warnings = getattr(combined, "warnings", None)
except Exception:
pass
commander_dict = getattr(builder, "commander_dict", None)
@ -2583,17 +2583,17 @@ def start_build_ctx(
# Owned-only / prefer-owned (if requested)
try:
if use_owned_only:
b.use_owned_only = True # type: ignore[attr-defined]
b.use_owned_only = True
if owned_names:
try:
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip()) # type: ignore[attr-defined]
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip())
except Exception:
b.owned_card_names = set() # type: ignore[attr-defined]
b.owned_card_names = set()
if prefer_owned:
try:
b.prefer_owned = True # type: ignore[attr-defined]
b.prefer_owned = True
if owned_names and not getattr(b, 'owned_card_names', None):
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip()) # type: ignore[attr-defined]
b.owned_card_names = set(str(n).strip() for n in owned_names if str(n).strip())
except Exception:
pass
except Exception:
@ -2646,14 +2646,14 @@ def start_build_ctx(
# Thread combo config
try:
if combo_target_count is not None:
b.combo_target_count = int(combo_target_count) # type: ignore[attr-defined]
b.combo_target_count = int(combo_target_count)
except Exception:
pass
try:
if combo_balance:
bal = str(combo_balance).strip().lower()
if bal in ('early','late','mix'):
b.combo_balance = bal # type: ignore[attr-defined]
b.combo_balance = bal
except Exception:
pass
# Stages
@ -2735,23 +2735,23 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
pass
if not ctx.get("txt_path") and hasattr(b, 'export_decklist_text'):
try:
ctx["csv_path"] = b.export_decklist_csv() # type: ignore[attr-defined]
ctx["csv_path"] = b.export_decklist_csv()
except Exception as e:
logs.append(f"CSV export failed: {e}")
if not ctx.get("txt_path") and hasattr(b, 'export_decklist_text'):
try:
import os as _os
base, _ext = _os.path.splitext(_os.path.basename(ctx.get("csv_path") or f"deck_{b.timestamp}.csv"))
ctx["txt_path"] = b.export_decklist_text(filename=base + '.txt') # type: ignore[attr-defined]
ctx["txt_path"] = b.export_decklist_text(filename=base + '.txt')
# Export the run configuration JSON for manual builds
try:
b.export_run_config_json(directory='config', filename=base + '.json') # type: ignore[attr-defined]
b.export_run_config_json(directory='config', filename=base + '.json')
except Exception:
pass
# Compute bracket compliance and save JSON alongside exports
try:
if hasattr(b, 'compute_and_print_compliance'):
rep0 = b.compute_and_print_compliance(base_stem=base) # type: ignore[attr-defined]
rep0 = b.compute_and_print_compliance(base_stem=base)
rep0 = _attach_enforcement_plan(b, rep0)
try:
import os as __os
@ -2759,7 +2759,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
except Exception:
_auto = False
if _auto and isinstance(rep0, dict) and rep0.get('overall') == 'FAIL' and hasattr(b, 'enforce_and_reexport'):
b.enforce_and_reexport(base_stem=base, mode='auto') # type: ignore[attr-defined]
b.enforce_and_reexport(base_stem=base, mode='auto')
except Exception:
pass
# Load compliance JSON for UI consumption
@ -2811,7 +2811,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
summary = None
try:
if hasattr(b, 'build_deck_summary'):
summary = b.build_deck_summary() # type: ignore[attr-defined]
summary = b.build_deck_summary()
except Exception:
summary = None
# Write sidecar summary JSON next to CSV (if available)
@ -2830,7 +2830,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
"txt": ctx.get("txt_path"),
}
try:
commander_meta = b.get_commander_export_metadata() # type: ignore[attr-defined]
commander_meta = b.get_commander_export_metadata()
except Exception:
commander_meta = {}
names = commander_meta.get("commander_names") or []
@ -2890,12 +2890,12 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
comp_now = None
try:
if hasattr(b, 'compute_and_print_compliance'):
comp_now = b.compute_and_print_compliance(base_stem=None) # type: ignore[attr-defined]
comp_now = b.compute_and_print_compliance(base_stem=None)
except Exception:
comp_now = None
try:
if comp_now:
comp_now = _attach_enforcement_plan(b, comp_now) # type: ignore[attr-defined]
comp_now = _attach_enforcement_plan(b, comp_now)
except Exception:
pass
# If still FAIL, return the saved result without advancing or rerunning
@ -3407,7 +3407,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
comp = None
try:
if hasattr(b, 'compute_and_print_compliance'):
comp = b.compute_and_print_compliance(base_stem=None) # type: ignore[attr-defined]
comp = b.compute_and_print_compliance(base_stem=None)
except Exception:
comp = None
try:
@ -3508,7 +3508,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
comp = None
try:
if hasattr(b, 'compute_and_print_compliance'):
comp = b.compute_and_print_compliance(base_stem=None) # type: ignore[attr-defined]
comp = b.compute_and_print_compliance(base_stem=None)
except Exception:
comp = None
try:
@ -3575,7 +3575,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
comp = None
try:
if hasattr(b, 'compute_and_print_compliance'):
comp = b.compute_and_print_compliance(base_stem=None) # type: ignore[attr-defined]
comp = b.compute_and_print_compliance(base_stem=None)
except Exception:
comp = None
try:
@ -3617,23 +3617,23 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
pass
if not ctx.get("csv_path") and hasattr(b, 'export_decklist_csv'):
try:
ctx["csv_path"] = b.export_decklist_csv() # type: ignore[attr-defined]
ctx["csv_path"] = b.export_decklist_csv()
except Exception as e:
logs.append(f"CSV export failed: {e}")
if not ctx.get("txt_path") and hasattr(b, 'export_decklist_text'):
try:
import os as _os
base, _ext = _os.path.splitext(_os.path.basename(ctx.get("csv_path") or f"deck_{b.timestamp}.csv"))
ctx["txt_path"] = b.export_decklist_text(filename=base + '.txt') # type: ignore[attr-defined]
ctx["txt_path"] = b.export_decklist_text(filename=base + '.txt')
# Export the run configuration JSON for manual builds
try:
b.export_run_config_json(directory='config', filename=base + '.json') # type: ignore[attr-defined]
b.export_run_config_json(directory='config', filename=base + '.json')
except Exception:
pass
# Compute bracket compliance and save JSON alongside exports
try:
if hasattr(b, 'compute_and_print_compliance'):
rep0 = b.compute_and_print_compliance(base_stem=base) # type: ignore[attr-defined]
rep0 = b.compute_and_print_compliance(base_stem=base)
rep0 = _attach_enforcement_plan(b, rep0)
try:
import os as __os
@ -3641,7 +3641,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
except Exception:
_auto = False
if _auto and isinstance(rep0, dict) and rep0.get('overall') == 'FAIL' and hasattr(b, 'enforce_and_reexport'):
b.enforce_and_reexport(base_stem=base, mode='auto') # type: ignore[attr-defined]
b.enforce_and_reexport(base_stem=base, mode='auto')
except Exception:
pass
# Load compliance JSON for UI consumption
@ -3662,7 +3662,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
summary = None
try:
if hasattr(b, 'build_deck_summary'):
summary = b.build_deck_summary() # type: ignore[attr-defined]
summary = b.build_deck_summary()
except Exception:
summary = None
# Write sidecar summary JSON next to CSV (if available)
@ -3681,7 +3681,7 @@ def run_stage(ctx: Dict[str, Any], rerun: bool = False, show_skipped: bool = Fal
"txt": ctx.get("txt_path"),
}
try:
commander_meta = b.get_commander_export_metadata() # type: ignore[attr-defined]
commander_meta = b.get_commander_export_metadata()
except Exception:
commander_meta = {}
names = commander_meta.get("commander_names") or []

View file

@ -362,7 +362,7 @@ def load_dataset(*, force: bool = False, refresh: bool = False) -> Optional[Part
if allow_auto_refresh:
_DATASET_REFRESH_ATTEMPTED = True
try:
from .orchestrator import _maybe_refresh_partner_synergy # type: ignore
from .orchestrator import _maybe_refresh_partner_synergy
_maybe_refresh_partner_synergy(None, force=True)
except Exception as refresh_exc: # pragma: no cover - best-effort

View file

@ -21,7 +21,7 @@ import json
import threading
import math
from .preview_metrics import record_eviction # type: ignore
from .preview_metrics import record_eviction
# Phase 2 extraction: adaptive TTL band policy moved into preview_policy
from .preview_policy import (
@ -30,7 +30,7 @@ from .preview_policy import (
DEFAULT_TTL_MIN as _POLICY_TTL_MIN,
DEFAULT_TTL_MAX as _POLICY_TTL_MAX,
)
from .preview_cache_backend import redis_store # type: ignore
from .preview_cache_backend import redis_store
TTL_SECONDS = 600
# Backward-compat variable names retained (tests may reference) mapping to policy constants

View file

@ -24,9 +24,9 @@ import os
import time
try: # lazy optional dependency
import redis # type: ignore
import redis
except Exception: # pragma: no cover - absence path
redis = None # type: ignore
redis = None
_URL = os.getenv("THEME_PREVIEW_REDIS_URL")
_DISABLED = (os.getenv("THEME_PREVIEW_REDIS_DISABLE") or "").lower() in {"1","true","yes","on"}
@ -42,7 +42,7 @@ def _init() -> None:
_INIT_ERR = "disabled_or_missing"
return
try:
_CLIENT = redis.Redis.from_url(_URL, socket_timeout=0.25) # type: ignore
_CLIENT = redis.Redis.from_url(_URL, socket_timeout=0.25)
# lightweight ping (non-fatal)
try:
_CLIENT.ping()
@ -86,7 +86,7 @@ def redis_get(key: Tuple[str, int, str | None, str | None, str]) -> Optional[Dic
return None
try:
skey = "tpv:" + "|".join([str(part) for part in key])
raw: bytes | None = _CLIENT.get(skey) # type: ignore
raw: bytes | None = _CLIENT.get(skey)
if not raw:
return None
obj = json.loads(raw.decode("utf-8"))

View file

@ -130,7 +130,7 @@ def sample_real_cards_for_theme(theme: str, limit: int, colors_filter: Optional[
if allow_splash:
off = ci - commander_colors
if len(off) == 1:
c["_splash_off_color"] = True # type: ignore
c["_splash_off_color"] = True
new_pool.append(c)
continue
pool = new_pool

View file

@ -7,7 +7,7 @@ from .combo_utils import detect_for_summary as _detect_for_summary
def _owned_set_helper() -> set[str]:
try:
from .build_utils import owned_set as _owned_set # type: ignore
from .build_utils import owned_set as _owned_set
return _owned_set()
except Exception:
@ -21,7 +21,7 @@ def _owned_set_helper() -> set[str]:
def _sanitize_tag_list(values: Iterable[Any]) -> List[str]:
cleaned: List[str] = []
for raw in values or []: # type: ignore[arg-type]
for raw in values or []:
text = str(raw or "").strip()
if not text:
continue
@ -78,7 +78,7 @@ def format_theme_label(raw: Any) -> str:
def format_theme_list(values: Iterable[Any]) -> List[str]:
seen: set[str] = set()
result: List[str] = []
for raw in values or []: # type: ignore[arg-type]
for raw in values or []:
label = format_theme_label(raw)
if not label:
continue

View file

@ -26,10 +26,10 @@ from pydantic import BaseModel
# - Docker (WORKDIR /app/code): modules also available top-level.
# - Package/zip installs (rare): may require 'code.' prefix.
try:
from type_definitions_theme_catalog import ThemeCatalog, ThemeEntry # type: ignore
from type_definitions_theme_catalog import ThemeCatalog, ThemeEntry
except ImportError: # pragma: no cover - fallback path
try:
from code.type_definitions_theme_catalog import ThemeCatalog, ThemeEntry # type: ignore
from code.type_definitions_theme_catalog import ThemeCatalog, ThemeEntry
except ImportError: # pragma: no cover - last resort (avoid beyond top-level relative import)
raise
@ -97,7 +97,7 @@ def _needs_reload() -> bool:
if not CATALOG_JSON.exists():
return bool(_CACHE)
mtime = CATALOG_JSON.stat().st_mtime
idx: SlugThemeIndex | None = _CACHE.get("index") # type: ignore
idx: SlugThemeIndex | None = _CACHE.get("index")
if idx is None:
return True
if mtime > idx.mtime:
@ -121,7 +121,7 @@ def _needs_reload() -> bool:
# Fast path: use os.scandir for lower overhead vs Path.glob
newest = 0.0
try:
with _os.scandir(YAML_DIR) as it: # type: ignore[arg-type]
with _os.scandir(YAML_DIR) as it:
for entry in it:
if entry.is_file() and entry.name.endswith('.yml'):
try:
@ -164,7 +164,7 @@ def _compute_etag(size: int, mtime: float, yaml_mtime: float) -> str:
def load_index() -> SlugThemeIndex:
if not _needs_reload():
return _CACHE["index"] # type: ignore
return _CACHE["index"]
if not CATALOG_JSON.exists():
raise FileNotFoundError("theme_list.json missing")
raw = json.loads(CATALOG_JSON.read_text(encoding="utf-8") or "{}")
@ -220,7 +220,7 @@ def validate_catalog_integrity(rebuild: bool = True) -> Dict[str, Any]:
out.update({"ok": False, "error": f"read_error:{e}"})
return out
# Recompute hash using same heuristic as build script
from scripts.build_theme_catalog import load_catalog_yaml # type: ignore
from scripts.build_theme_catalog import load_catalog_yaml
try:
yaml_catalog = load_catalog_yaml(verbose=False) # keyed by display_name
except Exception:
@ -495,7 +495,7 @@ def prewarm_common_filters(max_archetypes: int = 12) -> None:
# Gather archetypes & buckets (limited)
archetypes: List[str] = []
try:
archetypes = [a for a in {t.deck_archetype for t in idx.catalog.themes if t.deck_archetype}][:max_archetypes] # type: ignore[arg-type]
archetypes = [a for a in {t.deck_archetype for t in idx.catalog.themes if t.deck_archetype}][:max_archetypes]
except Exception:
archetypes = []
buckets = ["Very Common", "Common", "Uncommon", "Niche", "Rare"]

View file

@ -17,7 +17,7 @@ import json
try:
import yaml # type: ignore
except Exception: # pragma: no cover - PyYAML already in requirements; defensive
yaml = None # type: ignore
yaml = None
from .preview_metrics import (
record_build_duration,
record_role_counts,
@ -51,8 +51,8 @@ from .preview_cache import (
store_cache_entry,
evict_if_needed,
)
from .preview_cache_backend import redis_get # type: ignore
from .preview_metrics import record_redis_get, record_redis_store # type: ignore
from .preview_cache_backend import redis_get
from .preview_metrics import record_redis_get, record_redis_store
# Local alias to maintain existing internal variable name usage
_PREVIEW_CACHE = PREVIEW_CACHE
@ -66,7 +66,7 @@ __all__ = ["get_theme_preview", "preview_metrics", "bust_preview_cache"]
## (duplicate imports removed)
# Legacy constant alias retained for any external references; now a function in cache module.
TTL_SECONDS = ttl_seconds # type: ignore
TTL_SECONDS = ttl_seconds
# Per-theme error histogram (P2 observability)
_PREVIEW_PER_THEME_ERRORS: Dict[str, int] = {}
@ -89,7 +89,7 @@ def _load_curated_synergy_matrix() -> None:
# Expect top-level key 'pairs' but allow raw mapping
pairs = data.get('pairs', data)
if isinstance(pairs, dict):
_CURATED_SYNERGY_MATRIX = pairs # type: ignore
_CURATED_SYNERGY_MATRIX = pairs
else:
_CURATED_SYNERGY_MATRIX = None
else: