Refactored setup.py again, confirmed that all filters are now working as expected. Work will resume on main branch now

This commit is contained in:
mwisnowski 2025-01-13 11:35:11 -08:00
parent c4d773d663
commit 000d804ba7
6 changed files with 584 additions and 262 deletions

View file

@ -82,4 +82,41 @@ class ColorFilterError(MTGSetupError):
self.color = color
self.details = details
error_info = f" - {details}" if details else ""
super().__init__(f"{message} for color '{color}'{error_info}")
super().__init__(f"{message} for color '{color}'{error_info}")
class CommanderValidationError(MTGSetupError):
"""Exception raised when commander validation fails.
This exception is raised when there are issues validating commander cards,
such as non-legendary creatures, color identity mismatches, or banned cards.
Args:
message: Explanation of the error
validation_type: Type of validation that failed (e.g., 'legendary_check', 'color_identity', 'banned_set')
details: Additional error details
Examples:
>>> raise CommanderValidationError(
... "Card must be legendary",
... "legendary_check",
... "Lightning Bolt is not a legendary creature"
... )
>>> raise CommanderValidationError(
... "Commander color identity mismatch",
... "color_identity",
... "Omnath, Locus of Creation cannot be used in Golgari deck"
... )
>>> raise CommanderValidationError(
... "Commander banned in format",
... "banned_set",
... "Golos, Tireless Pilgrim is banned in Commander"
... )
"""
def __init__(self, message: str, validation_type: str, details: str = None) -> None:
self.validation_type = validation_type
self.details = details
error_info = f" - {details}" if details else ""
super().__init__(f"{message} [{validation_type}]{error_info}")

143
main.py
View file

@ -2,69 +2,104 @@ from __future__ import annotations
import inquirer.prompt # type: ignore
import sys
import logging
from pathlib import Path
from typing import NoReturn, Optional
import setup
import card_info
import tagger
Path('csv_files').mkdir(parents=True, exist_ok=True)
Path('staples').mkdir(parents=True, exist_ok=True)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('main.log', mode='w')
]
)
while True:
print('What would you like to do?')
choice = 'Menu'
while choice == 'Menu':
question = [
inquirer.List('menu',
choices=['Setup', 'Build a Deck', 'Get Card Info', 'Quit'],
carousel=True)
]
# Menu constants
MENU_SETUP = 'Setup'
MENU_BUILD_DECK = 'Build a Deck'
MENU_CARD_INFO = 'Get Card Info'
MAIN_TAG = 'Tag CSV Files'
MENU_QUIT = 'Quit'
MENU_CHOICES = [MENU_SETUP, MENU_BUILD_DECK, MENU_CARD_INFO, MAIN_TAG, MENU_QUIT]
def get_menu_choice() -> Optional[str]:
"""Display the main menu and get user choice.
Returns:
Optional[str]: The selected menu option or None if cancelled
"""
question = [
inquirer.List('menu',
choices=MENU_CHOICES,
carousel=True)
]
try:
answer = inquirer.prompt(question)
return answer['menu'] if answer else None
except (KeyError, TypeError) as e:
logging.error(f"Error getting menu choice: {e}")
return None
def handle_card_info() -> None:
"""Handle the card info menu option with proper error handling."""
try:
while True:
card_info.get_card_info()
question = [
inquirer.Confirm('continue',
message='Would you like to look up another card?')
]
try:
answer = inquirer.prompt(question)
if not answer or not answer['continue']:
break
except (KeyError, TypeError) as e:
logging.error(f"Error in card info continuation prompt: {e}")
break
except Exception as e:
logging.error(f"Error in card info handling: {e}")
def run_menu() -> NoReturn:
"""Main menu loop with improved error handling and logging."""
logging.info("Starting MTG Python Deckbuilder")
Path('csv_files').mkdir(parents=True, exist_ok=True)
while True:
try:
answer = inquirer.prompt(question)
if answer is None:
print("Operation cancelled. Returning to menu...")
choice = 'Menu'
print('What would you like to do?')
choice = get_menu_choice()
if choice is None:
logging.info("Menu operation cancelled")
continue
choice = answer['menu']
except (KeyError, TypeError):
print("Invalid input. Please try again.")
choice = 'Menu'
# Run through initial setup
while choice == 'Setup':
setup.setup()
choice = 'Menu'
logging.info(f"User selected: {choice}")
# Make a new deck
while choice == 'Build a Deck':
print('Deck building not yet implemented')
choice = 'Menu'
match choice:
case 'Setup':
setup.setup()
tagger.run_tagging()
case 'Build a Deck':
logging.info("Deck building not yet implemented")
print('Deck building not yet implemented')
case 'Get Card Info':
handle_card_info()
case 'Tag CSV Files':
tagger.run_tagging()
case 'Quit':
logging.info("Exiting application")
sys.exit(0)
case _:
logging.warning(f"Invalid menu choice: {choice}")
# Get a cards info
while choice == 'Get Card Info':
card_info.get_card_info()
question = [
inquirer.Confirm('continue',
message='Would you like to look up another card?'
)
]
try:
answer = inquirer.prompt(question)
if answer is None:
print("Operation cancelled. Returning to menu...")
choice = 'Menu'
continue
new_card = answer['continue']
if new_card:
choice = 'Get Card Info' # Fixed == to = for assignment
except (KeyError, TypeError):
print("Invalid input. Returning to menu...")
choice = 'Menu'
except Exception as e:
logging.error(f"Unexpected error in main menu: {e}")
# Quit
while choice == 'Quit':
sys.exit()
if __name__ == "__main__":
run_menu()

View file

@ -1,3 +1,9 @@
"""Constants and configuration settings for the MTG Python Deckbuilder.
This module contains all the constant values and configuration settings used throughout
the application for card filtering, processing, and analysis. Constants are organized
into logical sections with clear documentation.
"""
artifact_tokens = ['Blood', 'Clue', 'Food', 'Gold', 'Incubator',
'Junk','Map','Powerstone', 'Treasure']
@ -793,21 +799,22 @@ CARD_TYPES_TO_EXCLUDE = [
'Contraption'
]
# Columns to keep when processing CSV files
CSV_PROCESSING_COLUMNS = [
'name',
'faceName',
'edhrecRank',
'colorIdentity',
'colors',
'manaCost',
'manaValue',
'type',
'layout',
'text',
'power',
'toughness',
'keywords',
'side'
'name', # Card name
'faceName', # Name of specific face for multi-faced cards
'edhrecRank', # Card's rank on EDHREC
'colorIdentity', # Color identity for Commander format
'colors', # Actual colors in card's mana cost
'manaCost', # Mana cost string
'manaValue', # Converted mana cost
'type', # Card type line
'layout', # Card layout (normal, split, etc)
'text', # Card text/rules
'power', # Power (for creatures)
'toughness', # Toughness (for creatures)
'keywords', # Card's keywords
'side' # Side identifier for multi-faced cards
]
SETUP_COLORS = ['colorless', 'white', 'blue', 'black', 'green', 'red',
@ -824,3 +831,30 @@ COLOR_ABRV = ['Colorless', 'W', 'U', 'B', 'G', 'R',
'B, G, W', 'R, U, W', 'B, R, W', 'B, G, U', 'G, R, U',
'B, G, R, W', 'B, G, R, U', 'G, R, U, W', 'B, G, U, W',
'B, R, U, W', 'B, G, R, U, W']
# Configuration for handling null/NA values in DataFrame columns
FILL_NA_COLUMNS = {
'colorIdentity': 'Colorless', # Default color identity for cards without one
'faceName': None # Use card's name column value when face name is not available
}
# Configuration for DataFrame sorting operations
SORT_CONFIG = {
'columns': ['name', 'side'], # Columns to sort by
'case_sensitive': False # Ignore case when sorting
}
# Configuration for DataFrame filtering operations
FILTER_CONFIG = {
'layout': {
'exclude': ['reversible_card']
},
'availability': {
'require': ['paper']
},
'promoTypes': {
'exclude': ['playtest']
},
'securityStamp': {
'exclude': ['Heart', 'Acorn']
}
}

385
setup.py
View file

@ -1,12 +1,14 @@
from __future__ import annotations
from enum import Enum
import pandas as pd # type: ignore
import requests # type: ignore
import inquirer.prompt # type: ignore
import logging
from settings import banned_cards, csv_directory, SETUP_COLORS, COLOR_ABRV, MTGJSON_API_URL
from setup_utils import download_cards_csv, filter_dataframe, process_legendary_cards
from setup_utils import download_cards_csv, filter_dataframe, process_legendary_cards, filter_by_color_identity
from exceptions import CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError, ColorFilterError, CommanderValidationError
# Configure logging
logging.basicConfig(
@ -16,77 +18,38 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
def filter_by_color(df, column_name, value, new_csv_name):
# Filter dataframe
filtered_df = df[df[column_name] == value]
"""
Save the filtered dataframe to a new csv file, and narrow down/rearranges the columns it
keeps to increase readability/trim some extra data.
Additionally attempts to remove as many duplicates (including cards with reversible prints,
as well as taking out Arena-only cards.
"""
filtered_df.sort_values('name')
filtered_df = filtered_df.loc[filtered_df['layout'] != 'reversible_card']
filtered_df = filtered_df[filtered_df['availability'].str.contains('paper')]
filtered_df = filtered_df.loc[filtered_df['promoTypes'] != 'playtest']
filtered_df = filtered_df.loc[filtered_df['securityStamp'] != 'heart']
filtered_df = filtered_df.loc[filtered_df['securityStamp'] != 'acorn']
def check_csv_exists(file_path: str) -> bool:
"""Check if a CSV file exists at the specified path.
for card in banned_cards:
filtered_df = filtered_df[~filtered_df['name'].str.contains(card)]
card_types = ['Plane —', 'Conspiracy', 'Vanguard', 'Scheme', 'Phenomenon', 'Stickers', 'Attraction', 'Hero', 'Contraption']
for card_type in card_types:
filtered_df = filtered_df[~filtered_df['type'].str.contains(card_type)]
filtered_df['faceName'] = filtered_df['faceName'].fillna(filtered_df['name'])
filtered_df.drop_duplicates(subset='faceName', keep='first', inplace=True)
columns_to_keep = ['name', 'faceName','edhrecRank','colorIdentity', 'colors', 'manaCost', 'manaValue', 'type', 'layout', 'text', 'power', 'toughness', 'keywords', 'side']
filtered_df = filtered_df[columns_to_keep]
filtered_df.sort_values(by=['name', 'side'], key=lambda col: col.str.lower(), inplace=True)
Args:
file_path: Path to the CSV file to check
filtered_df.to_csv(new_csv_name, index=False)
def determine_commanders():
print('Generating commander_cards.csv, containing all cards elligible to be commanders.')
Returns:
bool: True if file exists, False otherwise
Raises:
CSVFileNotFoundError: If there are issues accessing the file path
"""
try:
# Check for cards.csv
cards_file = f'{csv_directory}/cards.csv'
try:
with open(cards_file, 'r', encoding='utf-8'):
print('cards.csv exists.')
except FileNotFoundError:
print('cards.csv not found, downloading from mtgjson')
download_cards_csv(MTGJSON_API_URL, cards_file)
# Load and process cards data
df = pd.read_csv(cards_file, low_memory=False)
df['colorIdentity'] = df['colorIdentity'].fillna('Colorless')
# Process legendary cards
filtered_df = process_legendary_cards(df)
# Apply standard filters
filtered_df = filter_dataframe(filtered_df, banned_cards)
# Save commander cards
filtered_df.to_csv(f'{csv_directory}/commander_cards.csv', index=False)
print('commander_cards.csv file generated.')
with open(file_path, 'r', encoding='utf-8'):
return True
except FileNotFoundError:
return False
except Exception as e:
print(f'Error generating commander cards: {str(e)}')
raise
def initial_setup():
raise CSVFileNotFoundError(f'Error checking CSV file: {str(e)}')
def initial_setup() -> None:
"""Perform initial setup by downloading card data and creating filtered CSV files.
This function:
1. Downloads the latest card data from MTGJSON if needed
2. Creates color-filtered CSV files
3. Generates commander-eligible cards list
Downloads the latest card data from MTGJSON if needed, creates color-filtered CSV files,
and generates commander-eligible cards list. Uses utility functions from setup_utils.py
for file operations and data processing.
Uses utility functions from setup_utils.py for file operations and data processing.
Implements proper error handling for file operations and data processing.
Raises:
CSVFileNotFoundError: If required CSV files cannot be found
MTGJSONDownloadError: If card data download fails
DataFrameProcessingError: If data processing fails
ColorFilterError: If color filtering fails
"""
logger.info('Checking for cards.csv file')
@ -119,104 +82,218 @@ def initial_setup():
except Exception as e:
logger.error(f'Error during initial setup: {str(e)}')
raise
def regenerate_csvs_all():
"""
Pull the original cards.csv file and remake the {color}_cards.csv files.
This is useful if a new set has since come out to ensure the databases are up-to-date
"""
print('Downloading cards.csv from mtgjson')
url = 'https://mtgjson.com/api/v5/csv/cards.csv'
r = requests.get(url)
with open('csv_files/cards.csv', 'wb') as outputfile:
outputfile.write(r.content)
# Load cards.csv file into pandas dataframe so it can be further broken down
df = pd.read_csv('csv_files/cards.csv', low_memory=False)#, converters={'printings': pd.eval})
# Set frames that have nothing for color identity to be 'Colorless' instead
df['colorIdentity'] = df['colorIdentity'].fillna('Colorless')
rows_to_drop = []
non_legel_sets = ['PHTR', 'PH17', 'PH18' ,'PH19', 'PH20', 'PH21', 'UGL', 'UND', 'UNH', 'UST',]
for index, row in df.iterrows():
for illegal_set in non_legel_sets:
if illegal_set in row['printings']:
rows_to_drop.append(index)
df = df.drop(rows_to_drop)
# Color identity sorted cards
print('Regenerating color identity sorted files.\n')
# For loop to iterate through the colors
for i in range(min(len(SETUP_COLORS), len(COLOR_ABRV))):
print(f'Regenerating {SETUP_COLORS[i]}_cards.csv.')
filter_by_color(df, 'colorIdentity', COLOR_ABRV[i], f'csv_files/{SETUP_COLORS[i]}_cards.csv')
print(f'A new {SETUP_COLORS[i]}_cards.csv file has been made.\n')
def filter_by_color(df: pd.DataFrame, column_name: str, value: str, new_csv_name: str) -> None:
"""Filter DataFrame by color identity and save to CSV.
Args:
df: DataFrame to filter
column_name: Column to filter on (should be 'colorIdentity')
value: Color identity value to filter for
new_csv_name: Path to save filtered CSV
Raises:
ColorFilterError: If filtering fails
DataFrameProcessingError: If DataFrame processing fails
CSVFileNotFoundError: If CSV file operations fail
"""
try:
# Check if target CSV already exists
if check_csv_exists(new_csv_name):
logger.info(f'{new_csv_name} already exists, will be overwritten')
filtered_df = filter_by_color_identity(df, value)
filtered_df.to_csv(new_csv_name, index=False)
logger.info(f'Successfully created {new_csv_name}')
except (ColorFilterError, DataFrameProcessingError, CSVFileNotFoundError) as e:
logger.error(f'Failed to filter by color {value}: {str(e)}')
raise
def determine_commanders() -> None:
"""Generate commander_cards.csv containing all cards eligible to be commanders.
This function processes the card database to identify and validate commander-eligible cards,
applying comprehensive validation steps and filtering criteria.
Raises:
CSVFileNotFoundError: If cards.csv is missing and cannot be downloaded
MTGJSONDownloadError: If downloading cards data fails
CommanderValidationError: If commander validation fails
DataFrameProcessingError: If data processing operations fail
"""
logger.info('Starting commander card generation process')
try:
# Check for cards.csv with progress tracking
cards_file = f'{csv_directory}/cards.csv'
if not check_csv_exists(cards_file):
logger.info('cards.csv not found, initiating download')
download_cards_csv(MTGJSON_API_URL, cards_file)
else:
logger.info('cards.csv found, proceeding with processing')
# Load and process cards data
logger.info('Loading card data from CSV')
df = pd.read_csv(cards_file, low_memory=False)
df['colorIdentity'] = df['colorIdentity'].fillna('Colorless')
# Process legendary cards with validation
logger.info('Processing and validating legendary cards')
try:
filtered_df = process_legendary_cards(df)
except CommanderValidationError as e:
logger.error(f'Commander validation failed: {str(e)}')
raise
# Apply standard filters
logger.info('Applying standard card filters')
filtered_df = filter_dataframe(filtered_df, banned_cards)
# Save commander cards
logger.info('Saving validated commander cards')
filtered_df.to_csv(f'{csv_directory}/commander_cards.csv', index=False)
logger.info('Commander card generation completed successfully')
except (CSVFileNotFoundError, MTGJSONDownloadError) as e:
logger.error(f'File operation error: {str(e)}')
raise
except CommanderValidationError as e:
logger.error(f'Commander validation error: {str(e)}')
raise
except Exception as e:
logger.error(f'Unexpected error during commander generation: {str(e)}')
raise
def regenerate_csvs_all() -> None:
"""Regenerate all color-filtered CSV files from latest card data.
Downloads fresh card data and recreates all color-filtered CSV files.
Useful for updating the card database when new sets are released.
Raises:
MTGJSONDownloadError: If card data download fails
DataFrameProcessingError: If data processing fails
ColorFilterError: If color filtering fails
"""
try:
logger.info('Downloading latest card data from MTGJSON')
download_cards_csv(MTGJSON_API_URL, f'{csv_directory}/cards.csv')
logger.info('Loading and processing card data')
df = pd.read_csv(f'{csv_directory}/cards.csv', low_memory=False)
df['colorIdentity'] = df['colorIdentity'].fillna('Colorless')
logger.info('Regenerating color identity sorted files')
for i in range(min(len(SETUP_COLORS), len(COLOR_ABRV))):
color = SETUP_COLORS[i]
color_id = COLOR_ABRV[i]
logger.info(f'Processing {color} cards')
filter_by_color(df, 'colorIdentity', color_id, f'{csv_directory}/{color}_cards.csv')
logger.info('Regenerating commander cards')
determine_commanders()
logger.info('Card database regeneration complete')
except Exception as e:
logger.error(f'Failed to regenerate card database: {str(e)}')
raise
# Once files are regenerated, create a new legendary list
determine_commanders()
def regenerate_csv_by_color(color):
def regenerate_csv_by_color(color: str) -> None:
"""Regenerate CSV file for a specific color identity.
Args:
color: Color name to regenerate CSV for (e.g. 'white', 'blue')
Raises:
ValueError: If color is not valid
MTGJSONDownloadError: If card data download fails
DataFrameProcessingError: If data processing fails
ColorFilterError: If color filtering fails
"""
Pull the original cards.csv file and remake the {color}_cards.csv files
try:
if color not in SETUP_COLORS:
raise ValueError(f'Invalid color: {color}')
color_abv = COLOR_ABRV[SETUP_COLORS.index(color)]
logger.info(f'Downloading latest card data for {color} cards')
download_cards_csv(MTGJSON_API_URL, f'{csv_directory}/cards.csv')
logger.info('Loading and processing card data')
df = pd.read_csv(f'{csv_directory}/cards.csv', low_memory=False)
df['colorIdentity'] = df['colorIdentity'].fillna('Colorless')
logger.info(f'Regenerating {color} cards CSV')
filter_by_color(df, 'colorIdentity', color_abv, f'{csv_directory}/{color}_cards.csv')
logger.info(f'Successfully regenerated {color} cards database')
except Exception as e:
logger.error(f'Failed to regenerate {color} cards: {str(e)}')
raise
class SetupOption(Enum):
"""Enum for setup menu options."""
INITIAL_SETUP = 'Initial Setup'
REGENERATE_CSV = 'Regenerate CSV Files'
BACK = 'Back'
def _display_setup_menu() -> SetupOption:
"""Display the setup menu and return the selected option.
Returns:
SetupOption: The selected menu option
"""
# Determine the color_abv to use
COLOR_ABRV_index = SETUP_COLORS.index(color)
color_abv = COLOR_ABRV[COLOR_ABRV_index]
print('Downloading cards.csv from mtgjson')
url = 'https://mtgjson.com/api/v5/csv/cards.csv'
r = requests.get(url)
with open(f'{csv_directory}/cards.csv', 'wb') as outputfile:
outputfile.write(r.content)
# Load cards.csv file into pandas dataframe so it can be further broken down
df = pd.read_csv(f'{csv_directory}/cards.csv', low_memory=False)
# Set frames that have nothing for color identity to be 'Colorless' instead
df['colorIdentity'] = df['colorIdentity'].fillna('Colorless')
# Color identity sorted cards
print(f'Regenerating {color}_cards.csv file.\n')
# Regenerate the file
print(f'Regenerating {color}_cards.csv.')
filter_by_color(df, 'colorIdentity', color_abv, f'{csv_directory}/{color}_cards.csv')
print(f'A new {color}_cards.csv file has been made.\n')
question = [
inquirer.List('menu',
choices=[option.value for option in SetupOption],
carousel=True)
]
answer = inquirer.prompt(question)
return SetupOption(answer['menu'])
# Once files are regenerated, create a new legendary list
determine_commanders()
def add_tags():
pass
def setup():
while True:
print('Which setup operation would you like to perform?\n'
def setup() -> bool:
"""Run the setup process for the MTG Python Deckbuilder.
This function provides a menu-driven interface to:
1. Perform initial setup by downloading and processing card data
2. Regenerate CSV files with updated card data
3. Perform all tagging processes on the color-sorted csv files
The function handles errors gracefully and provides feedback through logging.
Returns:
bool: True if setup completed successfully, False otherwise
"""
try:
print('Which setup operation would you like to perform?\n'
'If this is your first time setting up, do the initial setup.\n'
'If you\'ve done the basic setup before, you can regenerate the CSV files\n')
choice = 'Menu'
while choice == 'Menu':
question = [
inquirer.List('menu',
choices=['Initial Setup', 'Regenerate CSV Files', 'Back'],
carousel=True)
]
answer = inquirer.prompt(question)
choice = answer['menu']
choice = _display_setup_menu()
# Run through initial setup
while choice == 'Initial Setup':
if choice == SetupOption.INITIAL_SETUP:
logging.info('Starting initial setup')
initial_setup()
break
# Regenerate CSV files
while choice == 'Regenerate CSV Files':
logging.info('Initial setup completed successfully')
return True
elif choice == SetupOption.REGENERATE_CSV:
logging.info('Starting CSV regeneration')
regenerate_csvs_all()
break
# Go back
while choice == 'Back':
break
break
initial_setup()
logging.info('CSV regeneration completed successfully')
return True
elif choice == SetupOption.BACK:
logging.info('Setup cancelled by user')
return False
except Exception as e:
logging.error(f'Error during setup: {e}')
raise
return False

View file

@ -5,8 +5,18 @@ import requests
import logging
from tqdm import tqdm
from pathlib import Path
from typing import List, Optional, Union
from typing import List, Optional, Union, Dict, Any
from settings import (
CSV_PROCESSING_COLUMNS,
CARD_TYPES_TO_EXCLUDE,
NON_LEGAL_SETS,
LEGENDARY_OPTIONS,
FILL_NA_COLUMNS,
SORT_CONFIG,
FILTER_CONFIG
)
from exceptions import CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError, ColorFilterError, CommanderValidationError
from settings import (
CSV_PROCESSING_COLUMNS,
CARD_TYPES_TO_EXCLUDE,
@ -42,6 +52,7 @@ def download_cards_csv(url: str, output_path: Union[str, Path]) -> None:
url,
getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None
) from e
def check_csv_exists(filepath: Union[str, Path]) -> bool:
"""Check if a CSV file exists at the specified path.
@ -54,7 +65,7 @@ def check_csv_exists(filepath: Union[str, Path]) -> bool:
return Path(filepath).is_file()
def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
"""Apply standard filters to the cards DataFrame.
"""Apply standard filters to the cards DataFrame using configuration from settings.
Args:
df: DataFrame to filter
@ -67,40 +78,52 @@ def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
DataFrameProcessingError: If filtering operations fail
"""
try:
# Fill null color identities
df['colorIdentity'] = df['colorIdentity'].fillna('Colorless')
logging.info('Starting standard DataFrame filtering')
# Basic filters
filtered_df = df[
(df['layout'] != 'reversible_card') &
(df['availability'].str.contains('paper', na=False)) &
(df['promoTypes'] != 'playtest') &
(~df['securityStamp'].str.contains('Heart|Acorn', na=False))
]
# Fill null values according to configuration
for col, fill_value in FILL_NA_COLUMNS.items():
if col == 'faceName':
fill_value = df['name']
df[col] = df[col].fillna(fill_value)
logging.debug(f'Filled NA values in {col} with {fill_value}')
# Apply basic filters from configuration
filtered_df = df.copy()
for field, rules in FILTER_CONFIG.items():
for rule_type, values in rules.items():
if rule_type == 'exclude':
for value in values:
filtered_df = filtered_df[~filtered_df[field].str.contains(value, na=False)]
elif rule_type == 'require':
for value in values:
filtered_df = filtered_df[filtered_df[field].str.contains(value, na=False)]
logging.debug(f'Applied {rule_type} filter for {field}: {values}')
# Remove illegal sets
for set_code in NON_LEGAL_SETS:
filtered_df = filtered_df[
~filtered_df['printings'].str.contains(set_code, na=False)
]
filtered_df = filtered_df[~filtered_df['printings'].str.contains(set_code, na=False)]
logging.debug('Removed illegal sets')
# Remove banned cards
for card in banned_cards:
filtered_df = filtered_df[~filtered_df['name'].str.contains(card, na=False)]
logging.debug('Removed banned cards')
# Remove special card types
for card_type in CARD_TYPES_TO_EXCLUDE:
filtered_df = filtered_df[~filtered_df['type'].str.contains(card_type, na=False)]
logging.debug('Removed special card types')
# Handle face names and duplicates
filtered_df['faceName'] = filtered_df['faceName'].fillna(filtered_df['name'])
filtered_df = filtered_df.drop_duplicates(subset='faceName', keep='first')
# Select and sort columns
# Select columns, sort, and drop duplicates
filtered_df = filtered_df[CSV_PROCESSING_COLUMNS]
filtered_df = filtered_df.sort_values(
by=SORT_CONFIG['columns'],
key=lambda col: col.str.lower() if not SORT_CONFIG['case_sensitive'] else col
)
filtered_df = filtered_df.drop_duplicates(subset='faceName', keep='first')
logging.info('Completed standard DataFrame filtering')
return filtered_df.sort_values(by=['name', 'side'],
key=lambda col: col.str.lower())
return filtered_df
except Exception as e:
raise DataFrameProcessingError(
@ -109,8 +132,78 @@ def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
str(e)
) from e
def filter_by_color_identity(df: pd.DataFrame, color_identity: str) -> pd.DataFrame:
"""Filter DataFrame by color identity with additional color-specific processing.
This function extends the base filter_dataframe functionality with color-specific
filtering logic. It is used by setup.py's filter_by_color function but provides
a more robust and configurable implementation.
Args:
df: DataFrame to filter
color_identity: Color identity to filter by (e.g., 'W', 'U,B', 'Colorless')
Returns:
DataFrame filtered by color identity
Raises:
ColorFilterError: If color identity is invalid or filtering fails
DataFrameProcessingError: If general filtering operations fail
"""
try:
logging.info(f'Filtering cards for color identity: {color_identity}')
# Define processing steps for progress tracking
steps = [
'Validating color identity',
'Applying base filtering',
'Filtering by color identity',
'Performing color-specific processing'
]
# Validate color identity
with tqdm(total=1, desc='Validating color identity') as pbar:
if not isinstance(color_identity, str):
raise ColorFilterError(
"Invalid color identity type",
str(color_identity),
"Color identity must be a string"
)
pbar.update(1)
# Apply base filtering
with tqdm(total=1, desc='Applying base filtering') as pbar:
filtered_df = filter_dataframe(df, [])
pbar.update(1)
# Filter by color identity
with tqdm(total=1, desc='Filtering by color identity') as pbar:
filtered_df = filtered_df[filtered_df['colorIdentity'] == color_identity]
logging.debug(f'Applied color identity filter: {color_identity}')
pbar.update(1)
# Additional color-specific processing
with tqdm(total=1, desc='Performing color-specific processing') as pbar:
# Placeholder for future color-specific processing
pbar.update(1)
logging.info(f'Completed color identity filtering for {color_identity}')
return filtered_df
except DataFrameProcessingError as e:
raise ColorFilterError(
"Color filtering failed",
color_identity,
str(e)
) from e
except Exception as e:
raise ColorFilterError(
"Unexpected error during color filtering",
color_identity,
str(e)
) from e
def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
"""Process and filter legendary cards for commander eligibility.
"""Process and filter legendary cards for commander eligibility with comprehensive validation.
Args:
df: DataFrame containing all cards
@ -119,28 +212,75 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
DataFrame containing only commander-eligible cards
Raises:
DataFrameProcessingError: If processing fails
CommanderValidationError: If validation fails for legendary status, special cases, or set legality
DataFrameProcessingError: If general processing fails
"""
try:
# Filter for legendary creatures and eligible cards
mask = df['type'].str.contains('|'.join(LEGENDARY_OPTIONS), na=False)
# Add cards that can be commanders
can_be_commander = df['text'].str.contains(
'can be your commander',
na=False
)
filtered_df = df[mask | can_be_commander].copy()
logging.info('Starting commander validation process')
validation_steps = [
'Checking legendary status',
'Validating special cases',
'Verifying set legality'
]
# Remove illegal sets
for set_code in NON_LEGAL_SETS:
filtered_df = filtered_df[
~filtered_df['printings'].str.contains(set_code, na=False)
]
filtered_df = df.copy()
# Step 1: Check legendary status
try:
with tqdm(total=1, desc='Checking legendary status') as pbar:
mask = filtered_df['type'].str.contains('|'.join(LEGENDARY_OPTIONS), na=False)
if not mask.any():
raise CommanderValidationError(
"No legendary creatures found",
"legendary_check",
"DataFrame contains no cards matching legendary criteria"
)
filtered_df = filtered_df[mask].copy()
logging.debug(f'Found {len(filtered_df)} legendary cards')
pbar.update(1)
except Exception as e:
raise CommanderValidationError(
"Legendary status check failed",
"legendary_check",
str(e)
) from e
# Step 2: Validate special cases
try:
with tqdm(total=1, desc='Validating special cases') as pbar:
special_cases = df['text'].str.contains('can be your commander', na=False)
special_commanders = df[special_cases].copy()
filtered_df = pd.concat([filtered_df, special_commanders]).drop_duplicates()
logging.debug(f'Added {len(special_commanders)} special commander cards')
pbar.update(1)
except Exception as e:
raise CommanderValidationError(
"Special case validation failed",
"special_cases",
str(e)
) from e
# Step 3: Verify set legality
try:
with tqdm(total=1, desc='Verifying set legality') as pbar:
initial_count = len(filtered_df)
for set_code in NON_LEGAL_SETS:
filtered_df = filtered_df[
~filtered_df['printings'].str.contains(set_code, na=False)
]
removed_count = initial_count - len(filtered_df)
logging.debug(f'Removed {removed_count} cards from illegal sets')
pbar.update(1)
except Exception as e:
raise CommanderValidationError(
"Set legality verification failed",
"set_legality",
str(e)
) from e
logging.info(f'Commander validation complete. {len(filtered_df)} valid commanders found')
return filtered_df
except CommanderValidationError:
raise
except Exception as e:
raise DataFrameProcessingError(
"Failed to process legendary cards",

View file

@ -6417,7 +6417,6 @@ def tag_for_removal(df: pd.DataFrame, color: str) -> None:
#start_time = pd.Timestamp.now()
#regenerate_csvs_all()
#for color in settings.colors:
# load_dataframe(color)
#duration = (pd.Timestamp.now() - start_time).total_seconds()