mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
Began work on refactoring deck_builder
Fixed logging for the other files such that they actually log to the file instead of just creating it
This commit is contained in:
parent
503068b20c
commit
e0dd09adee
8 changed files with 1228 additions and 807 deletions
|
@ -119,4 +119,78 @@ class CommanderValidationError(MTGSetupError):
|
|||
self.validation_type = validation_type
|
||||
self.details = details
|
||||
error_info = f" - {details}" if details else ""
|
||||
super().__init__(f"{message} [{validation_type}]{error_info}")
|
||||
super().__init__(f"{message} [{validation_type}]{error_info}")
|
||||
|
||||
|
||||
class InputValidationError(MTGSetupError):
|
||||
"""Exception raised when input validation fails.
|
||||
|
||||
This exception is raised when there are issues validating user input,
|
||||
such as invalid text formats, number ranges, or confirmation responses.
|
||||
|
||||
Args:
|
||||
message: Explanation of the error
|
||||
input_type: Type of input validation that failed (e.g., 'text', 'number', 'confirm')
|
||||
details: Additional error details
|
||||
|
||||
Examples:
|
||||
>>> raise InputValidationError(
|
||||
... "Invalid number input",
|
||||
... "number",
|
||||
... "Value must be between 1 and 100"
|
||||
... )
|
||||
|
||||
>>> raise InputValidationError(
|
||||
... "Invalid confirmation response",
|
||||
... "confirm",
|
||||
... "Please enter 'y' or 'n'"
|
||||
... )
|
||||
|
||||
>>> raise InputValidationError(
|
||||
... "Invalid text format",
|
||||
... "text",
|
||||
... "Input contains invalid characters"
|
||||
... )
|
||||
"""
|
||||
def __init__(self, message: str, input_type: str, details: str = None) -> None:
|
||||
self.input_type = input_type
|
||||
self.details = details
|
||||
error_info = f" - {details}" if details else ""
|
||||
super().__init__(f"{message} [{input_type}]{error_info}")
|
||||
|
||||
|
||||
class PriceCheckError(MTGSetupError):
|
||||
"""Exception raised when price checking operations fail.
|
||||
|
||||
This exception is raised when there are issues retrieving or processing
|
||||
card prices, such as API failures, invalid responses, or parsing errors.
|
||||
|
||||
Args:
|
||||
message: Explanation of the error
|
||||
card_name: Name of the card that caused the error
|
||||
details: Additional error details
|
||||
|
||||
Examples:
|
||||
>>> raise PriceCheckError(
|
||||
... "Failed to retrieve price",
|
||||
... "Black Lotus",
|
||||
... "API request timeout"
|
||||
... )
|
||||
|
||||
>>> raise PriceCheckError(
|
||||
... "Invalid price data format",
|
||||
... "Lightning Bolt",
|
||||
... "Unexpected response structure"
|
||||
... )
|
||||
|
||||
>>> raise PriceCheckError(
|
||||
... "Price data unavailable",
|
||||
... "Underground Sea",
|
||||
... "No price information found"
|
||||
... )
|
||||
"""
|
||||
def __init__(self, message: str, card_name: str, details: str = None) -> None:
|
||||
self.card_name = card_name
|
||||
self.details = details
|
||||
error_info = f" - {details}" if details else ""
|
||||
super().__init__(f"{message} for card '{card_name}'{error_info}")
|
212
input_handler.py
Normal file
212
input_handler.py
Normal file
|
@ -0,0 +1,212 @@
|
|||
"""Input validation and handling for MTG Python Deckbuilder.
|
||||
|
||||
This module provides the InputHandler class which encapsulates all input validation
|
||||
and handling logic. It supports different types of input validation including text,
|
||||
numbers, confirmations, and multiple choice questions.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Optional, Union
|
||||
import inquirer
|
||||
import logging
|
||||
import os
|
||||
|
||||
from exceptions import InputValidationError
|
||||
from settings import INPUT_VALIDATION, QUESTION_TYPES
|
||||
|
||||
# Create logs directory if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('logs/input_handlers.log', mode='a', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class InputHandler:
|
||||
"""Handles input validation and user interaction.
|
||||
|
||||
This class provides methods for validating different types of user input
|
||||
and handling user interaction through questionnaires. It uses constants
|
||||
from settings.py for validation messages and configuration.
|
||||
"""
|
||||
|
||||
def validate_text(self, result: str) -> bool:
|
||||
"""Validate text input is not empty.
|
||||
|
||||
Args:
|
||||
result: Text input to validate
|
||||
|
||||
Returns:
|
||||
bool: True if text is not empty after stripping whitespace
|
||||
|
||||
Raises:
|
||||
InputValidationError: If text validation fails
|
||||
"""
|
||||
try:
|
||||
if not result or not result.strip():
|
||||
raise InputValidationError(
|
||||
INPUT_VALIDATION['default_text_message'],
|
||||
'text',
|
||||
'Input cannot be empty'
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
raise InputValidationError(
|
||||
str(e),
|
||||
'text',
|
||||
'Unexpected error during text validation'
|
||||
)
|
||||
|
||||
def validate_number(self, result: str) -> Optional[float]:
|
||||
"""Validate and convert string input to float.
|
||||
|
||||
Args:
|
||||
result: Number input to validate
|
||||
|
||||
Returns:
|
||||
float | None: Converted float value or None if invalid
|
||||
|
||||
Raises:
|
||||
InputValidationError: If number validation fails
|
||||
"""
|
||||
try:
|
||||
if not result:
|
||||
raise InputValidationError(
|
||||
INPUT_VALIDATION['default_number_message'],
|
||||
'number',
|
||||
'Input cannot be empty'
|
||||
)
|
||||
return float(result)
|
||||
except ValueError:
|
||||
raise InputValidationError(
|
||||
INPUT_VALIDATION['default_number_message'],
|
||||
'number',
|
||||
'Input must be a valid number'
|
||||
)
|
||||
except Exception as e:
|
||||
raise InputValidationError(
|
||||
str(e),
|
||||
'number',
|
||||
'Unexpected error during number validation'
|
||||
)
|
||||
|
||||
def validate_confirm(self, result: Any) -> bool:
|
||||
"""Validate confirmation input.
|
||||
|
||||
Args:
|
||||
result: Confirmation input to validate
|
||||
|
||||
Returns:
|
||||
bool: True for positive confirmation, False otherwise
|
||||
|
||||
Raises:
|
||||
InputValidationError: If confirmation validation fails
|
||||
"""
|
||||
try:
|
||||
if isinstance(result, bool):
|
||||
return result
|
||||
if isinstance(result, str):
|
||||
result = result.lower().strip()
|
||||
if result in ('y', 'yes', 'true', '1'):
|
||||
return True
|
||||
if result in ('n', 'no', 'false', '0'):
|
||||
return False
|
||||
raise InputValidationError(
|
||||
INPUT_VALIDATION['default_confirm_message'],
|
||||
'confirm',
|
||||
'Invalid confirmation response'
|
||||
)
|
||||
except InputValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise InputValidationError(
|
||||
str(e),
|
||||
'confirm',
|
||||
'Unexpected error during confirmation validation'
|
||||
)
|
||||
|
||||
def questionnaire(
|
||||
self,
|
||||
question_type: str,
|
||||
default_value: Union[str, bool, float] = '',
|
||||
choices_list: List[str] = []
|
||||
) -> Union[str, bool, float]:
|
||||
"""Present questions to user and validate input.
|
||||
|
||||
Args:
|
||||
question_type: Type of question ('Text', 'Number', 'Confirm', 'Choice')
|
||||
default_value: Default value for the question
|
||||
choices_list: List of choices for Choice type questions
|
||||
|
||||
Returns:
|
||||
Union[str, bool, float]: Validated user input
|
||||
|
||||
Raises:
|
||||
InputValidationError: If input validation fails
|
||||
ValueError: If question type is not supported
|
||||
"""
|
||||
if question_type not in QUESTION_TYPES:
|
||||
raise ValueError(f"Unsupported question type: {question_type}")
|
||||
|
||||
attempts = 0
|
||||
while attempts < INPUT_VALIDATION['max_attempts']:
|
||||
try:
|
||||
if question_type == 'Text':
|
||||
question = [inquirer.Text('text')]
|
||||
result = inquirer.prompt(question)['text']
|
||||
if self.validate_text(result):
|
||||
return result
|
||||
|
||||
elif question_type == 'Number':
|
||||
question = [inquirer.Text('number', default=str(default_value))]
|
||||
result = inquirer.prompt(question)['number']
|
||||
validated = self.validate_number(result)
|
||||
if validated is not None:
|
||||
return validated
|
||||
|
||||
elif question_type == 'Confirm':
|
||||
question = [inquirer.Confirm('confirm', default=default_value)]
|
||||
result = inquirer.prompt(question)['confirm']
|
||||
return self.validate_confirm(result)
|
||||
|
||||
elif question_type == 'Choice':
|
||||
if not choices_list:
|
||||
raise InputValidationError(
|
||||
INPUT_VALIDATION['default_choice_message'],
|
||||
'choice',
|
||||
'No choices provided'
|
||||
)
|
||||
question = [
|
||||
inquirer.List('selection',
|
||||
choices=choices_list,
|
||||
carousel=True)
|
||||
]
|
||||
return inquirer.prompt(question)['selection']
|
||||
|
||||
except InputValidationError as e:
|
||||
attempts += 1
|
||||
if attempts >= INPUT_VALIDATION['max_attempts']:
|
||||
raise InputValidationError(
|
||||
"Maximum input attempts reached",
|
||||
question_type,
|
||||
str(e)
|
||||
)
|
||||
logger.warning(f"Invalid input ({attempts}/{INPUT_VALIDATION['max_attempts']}): {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
raise InputValidationError(
|
||||
str(e),
|
||||
question_type,
|
||||
'Unexpected error during questionnaire'
|
||||
)
|
||||
|
||||
raise InputValidationError(
|
||||
"Maximum input attempts reached",
|
||||
question_type,
|
||||
"Failed to get valid input"
|
||||
)
|
34
main.py
34
main.py
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
# Standard library imports
|
||||
import sys
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import NoReturn, Optional
|
||||
|
||||
|
@ -21,15 +22,19 @@ MTG Python Deckbuilder. It handles menu display, user input processing, and
|
|||
routing to different application features like setup, deck building, card info
|
||||
lookup and CSV file tagging.
|
||||
"""
|
||||
# Configure logging
|
||||
# Create logs directory if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('main.log', mode='w')
|
||||
logging.FileHandler('logs/main.log', mode='a', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Menu constants
|
||||
MENU_SETUP = 'Setup'
|
||||
|
@ -62,8 +67,9 @@ def get_menu_choice() -> Optional[str]:
|
|||
answer = inquirer.prompt(question) # type: ignore
|
||||
return answer['menu'] if answer else None
|
||||
except (KeyError, TypeError) as e:
|
||||
logging.error(f"Error getting menu choice: {e}")
|
||||
logger.error(f"Error getting menu choice: {e}")
|
||||
return None
|
||||
|
||||
def handle_card_info() -> None:
|
||||
"""Handle the card info menu option with proper error handling.
|
||||
|
||||
|
@ -91,12 +97,13 @@ def handle_card_info() -> None:
|
|||
if not answer or not answer['continue']:
|
||||
break
|
||||
except (KeyError, TypeError) as e:
|
||||
logging.error(f"Error in card info continuation prompt: {e}")
|
||||
logger.error(f"Error in card info continuation prompt: {e}")
|
||||
break
|
||||
except Exception as e:
|
||||
logging.error(f"Error in card info handling: {e}")
|
||||
logger.error(f"Error in card info handling: {e}")
|
||||
|
||||
def run_menu() -> NoReturn:
|
||||
"""Main menu loop with improved error handling and logging.
|
||||
"""Main menu loop with improved error handling and logger.
|
||||
|
||||
Provides the main application loop that displays the menu and handles user selections.
|
||||
Creates required directories, processes menu choices, and handles errors gracefully.
|
||||
|
@ -117,7 +124,7 @@ def run_menu() -> NoReturn:
|
|||
4. Tag CSV Files
|
||||
5. Quit
|
||||
"""
|
||||
logging.info("Starting MTG Python Deckbuilder")
|
||||
logger.info("Starting MTG Python Deckbuilder")
|
||||
Path('csv_files').mkdir(parents=True, exist_ok=True)
|
||||
|
||||
while True:
|
||||
|
@ -126,29 +133,30 @@ def run_menu() -> NoReturn:
|
|||
choice = get_menu_choice()
|
||||
|
||||
if choice is None:
|
||||
logging.info("Menu operation cancelled")
|
||||
logger.info("Menu operation cancelled")
|
||||
continue
|
||||
|
||||
logging.info(f"User selected: {choice}")
|
||||
logger.info(f"User selected: {choice}")
|
||||
|
||||
match choice:
|
||||
case 'Setup':
|
||||
setup.setup()
|
||||
tagger.run_tagging()
|
||||
case 'Build a Deck':
|
||||
logging.info("Deck building not yet implemented")
|
||||
logger.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")
|
||||
logger.info("Exiting application")
|
||||
sys.exit(0)
|
||||
case _:
|
||||
logging.warning(f"Invalid menu choice: {choice}")
|
||||
logger.warning(f"Invalid menu choice: {choice}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error in main menu: {e}")
|
||||
logger.error(f"Unexpected error in main menu: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_menu()
|
60
price_check.py
Normal file
60
price_check.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
"""Price checking functionality for MTG Python Deckbuilder.
|
||||
|
||||
This module provides functionality to check card prices using the Scryfall API
|
||||
through the scrython library. It includes caching and error handling for reliable
|
||||
price lookups.
|
||||
"""
|
||||
|
||||
import time
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
import scrython
|
||||
from scrython.cards import Named
|
||||
|
||||
from exceptions import PriceCheckError
|
||||
from settings import PRICE_CHECK_CONFIG
|
||||
|
||||
@lru_cache(maxsize=PRICE_CHECK_CONFIG['cache_size'])
|
||||
def check_price(card_name: str) -> float:
|
||||
"""Retrieve the current price of a Magic: The Gathering card.
|
||||
|
||||
Args:
|
||||
card_name: The name of the card to check.
|
||||
|
||||
Returns:
|
||||
float: The current price of the card in USD.
|
||||
|
||||
Raises:
|
||||
PriceCheckError: If there are any issues retrieving the price.
|
||||
"""
|
||||
retries = 0
|
||||
last_error = None
|
||||
|
||||
while retries < PRICE_CHECK_CONFIG['max_retries']:
|
||||
try:
|
||||
card = Named(fuzzy=card_name)
|
||||
price = card.prices('usd')
|
||||
print(price)
|
||||
|
||||
if price is None:
|
||||
raise PriceCheckError(
|
||||
"No price data available",
|
||||
card_name,
|
||||
"Card may be too new or not available in USD"
|
||||
)
|
||||
|
||||
return float(price)
|
||||
|
||||
except (scrython.ScryfallError, ValueError) as e:
|
||||
last_error = str(e)
|
||||
retries += 1
|
||||
if retries < PRICE_CHECK_CONFIG['max_retries']:
|
||||
time.sleep(0.1) # Brief delay before retry
|
||||
continue
|
||||
|
||||
raise PriceCheckError(
|
||||
"Failed to retrieve price after multiple attempts",
|
||||
card_name,
|
||||
f"Last error: {last_error}"
|
||||
)
|
31
settings.py
31
settings.py
|
@ -10,6 +10,23 @@ and enable static type checking with mypy.
|
|||
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# Constants for input validation
|
||||
INPUT_VALIDATION = {
|
||||
'max_attempts': 3,
|
||||
'default_text_message': 'Please enter a valid text response.',
|
||||
'default_number_message': 'Please enter a valid number.',
|
||||
'default_confirm_message': 'Please enter Y/N or Yes/No.',
|
||||
'default_choice_message': 'Please select a valid option from the list.'
|
||||
}
|
||||
|
||||
QUESTION_TYPES = [
|
||||
'Text',
|
||||
'Number',
|
||||
'Confirm',
|
||||
'Choice'
|
||||
]
|
||||
|
||||
# Card type constants
|
||||
artifact_tokens: List[str] = ['Blood', 'Clue', 'Food', 'Gold', 'Incubator',
|
||||
'Junk','Map','Powerstone', 'Treasure']
|
||||
|
||||
|
@ -777,6 +794,20 @@ VOLTRON_PATTERNS = [
|
|||
'reconfigure'
|
||||
]
|
||||
|
||||
# Constants for price checking functionality
|
||||
PRICE_CHECK_CONFIG: Dict[str, float] = {
|
||||
# Maximum number of retry attempts for price checking requests
|
||||
'max_retries': 3,
|
||||
|
||||
# Timeout in seconds for price checking requests
|
||||
'timeout': 0.1,
|
||||
|
||||
# Maximum size of the price check cache
|
||||
'cache_size': 128,
|
||||
|
||||
# Price tolerance factor (e.g., 1.1 means accept prices within 10% difference)
|
||||
'price_tolerance': 1.1
|
||||
}
|
||||
# Constants for setup and CSV processing
|
||||
MTGJSON_API_URL = 'https://mtgjson.com/api/v5/csv/cards.csv'
|
||||
|
||||
|
|
25
setup.py
25
setup.py
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
import os
|
||||
from typing import Union, List, Dict, Any
|
||||
|
||||
# Third-party imports
|
||||
|
@ -21,11 +22,17 @@ from exceptions import (
|
|||
CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError,
|
||||
ColorFilterError, CommanderValidationError
|
||||
)
|
||||
# Configure logging
|
||||
# Create logs directory if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('logs/setup.log', mode='w', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -288,23 +295,23 @@ def setup() -> bool:
|
|||
choice = _display_setup_menu()
|
||||
|
||||
if choice == SetupOption.INITIAL_SETUP:
|
||||
logging.info('Starting initial setup')
|
||||
logger.info('Starting initial setup')
|
||||
initial_setup()
|
||||
logging.info('Initial setup completed successfully')
|
||||
logger.info('Initial setup completed successfully')
|
||||
return True
|
||||
|
||||
elif choice == SetupOption.REGENERATE_CSV:
|
||||
logging.info('Starting CSV regeneration')
|
||||
logger.info('Starting CSV regeneration')
|
||||
regenerate_csvs_all()
|
||||
logging.info('CSV regeneration completed successfully')
|
||||
logger.info('CSV regeneration completed successfully')
|
||||
return True
|
||||
|
||||
elif choice == SetupOption.BACK:
|
||||
logging.info('Setup cancelled by user')
|
||||
logger.info('Setup cancelled by user')
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f'Error during setup: {e}')
|
||||
logger.error(f'Error during setup: {e}')
|
||||
raise
|
||||
|
||||
return False
|
||||
return False
|
||||
|
|
|
@ -1,7 +1,24 @@
|
|||
"""MTG Python Deckbuilder setup utilities.
|
||||
|
||||
This module provides utility functions for setting up and managing the MTG Python Deckbuilder
|
||||
application. It handles tasks such as downloading card data, filtering cards by various criteria,
|
||||
and processing legendary creatures for commander format.
|
||||
|
||||
Key Features:
|
||||
- Card data download from MTGJSON
|
||||
- DataFrame filtering and processing
|
||||
- Color identity filtering
|
||||
- Commander validation
|
||||
- CSV file management
|
||||
|
||||
The module integrates with settings.py for configuration and exceptions.py for error handling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Standard library imports
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Union, TypedDict
|
||||
|
@ -27,21 +44,19 @@ from exceptions import (
|
|||
CommanderValidationError
|
||||
)
|
||||
|
||||
"""MTG Python Deckbuilder setup utilities.
|
||||
# Create logs directory if it doesn't exist
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
This module provides utility functions for setting up and managing the MTG Python Deckbuilder
|
||||
application. It handles tasks such as downloading card data, filtering cards by various criteria,
|
||||
and processing legendary creatures for commander format.
|
||||
|
||||
Key Features:
|
||||
- Card data download from MTGJSON
|
||||
- DataFrame filtering and processing
|
||||
- Color identity filtering
|
||||
- Commander validation
|
||||
- CSV file management
|
||||
|
||||
The module integrates with settings.py for configuration and exceptions.py for error handling.
|
||||
"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('logs/setup_utils.log', mode='a', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Type definitions
|
||||
class FilterRule(TypedDict):
|
||||
|
@ -83,7 +98,7 @@ def download_cards_csv(url: str, output_path: Union[str, Path]) -> None:
|
|||
pbar.update(size)
|
||||
|
||||
except requests.RequestException as e:
|
||||
logging.error(f'Failed to download cards data from {url}')
|
||||
logger.error(f'Failed to download cards data from {url}')
|
||||
raise MTGJSONDownloadError(
|
||||
"Failed to download cards data",
|
||||
url,
|
||||
|
@ -128,14 +143,14 @@ def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
|
|||
>>> filtered_df = filter_dataframe(cards_df, ['Channel', 'Black Lotus'])
|
||||
"""
|
||||
try:
|
||||
logging.info('Starting standard DataFrame filtering')
|
||||
logger.info('Starting standard DataFrame filtering')
|
||||
|
||||
# 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}')
|
||||
logger.debug(f'Filled NA values in {col} with {fill_value}')
|
||||
|
||||
# Apply basic filters from configuration
|
||||
filtered_df = df.copy()
|
||||
|
@ -148,22 +163,22 @@ def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
|
|||
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}')
|
||||
logger.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)]
|
||||
logging.debug('Removed illegal sets')
|
||||
logger.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')
|
||||
logger.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')
|
||||
logger.debug('Removed special card types')
|
||||
|
||||
# Select columns, sort, and drop duplicates
|
||||
filtered_df = filtered_df[CSV_PROCESSING_COLUMNS]
|
||||
|
@ -172,12 +187,12 @@ def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
|
|||
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')
|
||||
logger.info('Completed standard DataFrame filtering')
|
||||
|
||||
return filtered_df
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f'Failed to filter DataFrame: {str(e)}')
|
||||
logger.error(f'Failed to filter DataFrame: {str(e)}')
|
||||
raise DataFrameProcessingError(
|
||||
"Failed to filter DataFrame",
|
||||
"standard_filtering",
|
||||
|
@ -202,7 +217,7 @@ def filter_by_color_identity(df: pd.DataFrame, color_identity: str) -> pd.DataFr
|
|||
DataFrameProcessingError: If general filtering operations fail
|
||||
"""
|
||||
try:
|
||||
logging.info(f'Filtering cards for color identity: {color_identity}')
|
||||
logger.info(f'Filtering cards for color identity: {color_identity}')
|
||||
|
||||
# Validate color identity
|
||||
with tqdm(total=1, desc='Validating color identity') as pbar:
|
||||
|
@ -222,14 +237,14 @@ def filter_by_color_identity(df: pd.DataFrame, color_identity: str) -> pd.DataFr
|
|||
# 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}')
|
||||
logger.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}')
|
||||
logger.info(f'Completed color identity filtering for {color_identity}')
|
||||
return filtered_df
|
||||
|
||||
except DataFrameProcessingError as e:
|
||||
|
@ -259,7 +274,7 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
|
|||
DataFrameProcessingError: If general processing fails
|
||||
"""
|
||||
try:
|
||||
logging.info('Starting commander validation process')
|
||||
logger.info('Starting commander validation process')
|
||||
|
||||
filtered_df = df.copy()
|
||||
# Step 1: Check legendary status
|
||||
|
@ -273,7 +288,7 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
|
|||
"DataFrame contains no cards matching legendary criteria"
|
||||
)
|
||||
filtered_df = filtered_df[mask].copy()
|
||||
logging.debug(f'Found {len(filtered_df)} legendary cards')
|
||||
logger.debug(f'Found {len(filtered_df)} legendary cards')
|
||||
pbar.update(1)
|
||||
except Exception as e:
|
||||
raise CommanderValidationError(
|
||||
|
@ -288,7 +303,7 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
|
|||
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')
|
||||
logger.debug(f'Added {len(special_commanders)} special commander cards')
|
||||
pbar.update(1)
|
||||
except Exception as e:
|
||||
raise CommanderValidationError(
|
||||
|
@ -306,7 +321,7 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
|
|||
~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')
|
||||
logger.debug(f'Removed {removed_count} cards from illegal sets')
|
||||
pbar.update(1)
|
||||
except Exception as e:
|
||||
raise CommanderValidationError(
|
||||
|
@ -314,7 +329,7 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
|
|||
"set_legality",
|
||||
str(e)
|
||||
) from e
|
||||
logging.info(f'Commander validation complete. {len(filtered_df)} valid commanders found')
|
||||
logger.info(f'Commander validation complete. {len(filtered_df)} valid commanders found')
|
||||
return filtered_df
|
||||
|
||||
except CommanderValidationError:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue