mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
Fleshed out docstrings, added typings, cleaned up imports, and added a requirements.txt file
Additionally, renamed utility.ty to tag_utils.py to fit the naming pattern used with setup.py and setup.utils.py
This commit is contained in:
parent
000d804ba7
commit
b8d9958564
8 changed files with 592 additions and 466 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,6 +2,7 @@
|
||||||
*.json
|
*.json
|
||||||
*.log
|
*.log
|
||||||
*.txt
|
*.txt
|
||||||
|
!requirements.txt
|
||||||
test.py
|
test.py
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
__pycache__/
|
__pycache__/
|
67
main.py
67
main.py
|
@ -1,15 +1,26 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inquirer.prompt # type: ignore
|
# Standard library imports
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NoReturn, Optional
|
from typing import NoReturn, Optional
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
import inquirer.prompt # type: ignore
|
||||||
|
|
||||||
|
# Local imports
|
||||||
import setup
|
import setup
|
||||||
import card_info
|
import card_info
|
||||||
import tagger
|
import tagger
|
||||||
|
|
||||||
|
"""Command-line interface for the MTG Python Deckbuilder application.
|
||||||
|
|
||||||
|
This module provides the main menu and user interaction functionality for the
|
||||||
|
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
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
@ -31,8 +42,16 @@ MENU_CHOICES = [MENU_SETUP, MENU_BUILD_DECK, MENU_CARD_INFO, MAIN_TAG, MENU_QUIT
|
||||||
def get_menu_choice() -> Optional[str]:
|
def get_menu_choice() -> Optional[str]:
|
||||||
"""Display the main menu and get user choice.
|
"""Display the main menu and get user choice.
|
||||||
|
|
||||||
|
Presents a menu of options to the user using inquirer and returns their selection.
|
||||||
|
Handles potential errors from inquirer gracefully.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[str]: The selected menu option or None if cancelled
|
Optional[str]: The selected menu option or None if cancelled/error occurs
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> choice = get_menu_choice()
|
||||||
|
>>> if choice == MENU_SETUP:
|
||||||
|
... setup.setup()
|
||||||
"""
|
"""
|
||||||
question = [
|
question = [
|
||||||
inquirer.List('menu',
|
inquirer.List('menu',
|
||||||
|
@ -40,14 +59,26 @@ def get_menu_choice() -> Optional[str]:
|
||||||
carousel=True)
|
carousel=True)
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
answer = inquirer.prompt(question)
|
answer = inquirer.prompt(question) # type: ignore
|
||||||
return answer['menu'] if answer else None
|
return answer['menu'] if answer else None
|
||||||
except (KeyError, TypeError) as e:
|
except (KeyError, TypeError) as e:
|
||||||
logging.error(f"Error getting menu choice: {e}")
|
logging.error(f"Error getting menu choice: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_card_info() -> None:
|
def handle_card_info() -> None:
|
||||||
"""Handle the card info menu option with proper error handling."""
|
"""Handle the card info menu option with proper error handling.
|
||||||
|
|
||||||
|
Provides an interface for looking up card information repeatedly until the user
|
||||||
|
chooses to stop. Handles potential errors from card info lookup and user input.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> handle_card_info()
|
||||||
|
Enter card name: Lightning Bolt
|
||||||
|
[Card info displayed]
|
||||||
|
Would you like to look up another card? [y/N]: n
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
card_info.get_card_info()
|
card_info.get_card_info()
|
||||||
|
@ -56,7 +87,7 @@ def handle_card_info() -> None:
|
||||||
message='Would you like to look up another card?')
|
message='Would you like to look up another card?')
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
answer = inquirer.prompt(question)
|
answer = inquirer.prompt(question) # type: ignore
|
||||||
if not answer or not answer['continue']:
|
if not answer or not answer['continue']:
|
||||||
break
|
break
|
||||||
except (KeyError, TypeError) as e:
|
except (KeyError, TypeError) as e:
|
||||||
|
@ -64,9 +95,28 @@ def handle_card_info() -> None:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in card info handling: {e}")
|
logging.error(f"Error in card info handling: {e}")
|
||||||
|
|
||||||
def run_menu() -> NoReturn:
|
def run_menu() -> NoReturn:
|
||||||
"""Main menu loop with improved error handling and logging."""
|
"""Main menu loop with improved error handling and logging.
|
||||||
|
|
||||||
|
Provides the main application loop that displays the menu and handles user selections.
|
||||||
|
Creates required directories, processes menu choices, and handles errors gracefully.
|
||||||
|
Never returns normally - exits via sys.exit().
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
NoReturn: Function never returns normally
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: When user selects Quit option
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> run_menu()
|
||||||
|
What would you like to do?
|
||||||
|
1. Setup
|
||||||
|
2. Build a Deck
|
||||||
|
3. Get Card Info
|
||||||
|
4. Tag CSV Files
|
||||||
|
5. Quit
|
||||||
|
"""
|
||||||
logging.info("Starting MTG Python Deckbuilder")
|
logging.info("Starting MTG Python Deckbuilder")
|
||||||
Path('csv_files').mkdir(parents=True, exist_ok=True)
|
Path('csv_files').mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
@ -100,6 +150,5 @@ def run_menu() -> NoReturn:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Unexpected error in main menu: {e}")
|
logging.error(f"Unexpected error in main menu: {e}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run_menu()
|
run_menu()
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
pandas>=1.5.0
|
||||||
|
inquirer>=3.1.3
|
||||||
|
typing-extensions>=4.5.0
|
||||||
|
|
||||||
|
# Development dependencies
|
||||||
|
mypy>=1.3.0
|
||||||
|
pandas-stubs>=2.0.0
|
||||||
|
types-inquirer>=3.1.3
|
16
settings.py
16
settings.py
|
@ -3,8 +3,14 @@
|
||||||
This module contains all the constant values and configuration settings used throughout
|
This module contains all the constant values and configuration settings used throughout
|
||||||
the application for card filtering, processing, and analysis. Constants are organized
|
the application for card filtering, processing, and analysis. Constants are organized
|
||||||
into logical sections with clear documentation.
|
into logical sections with clear documentation.
|
||||||
|
|
||||||
|
All constants are properly typed according to PEP 484 standards to ensure type safety
|
||||||
|
and enable static type checking with mypy.
|
||||||
"""
|
"""
|
||||||
artifact_tokens = ['Blood', 'Clue', 'Food', 'Gold', 'Incubator',
|
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
artifact_tokens: List[str] = ['Blood', 'Clue', 'Food', 'Gold', 'Incubator',
|
||||||
'Junk','Map','Powerstone', 'Treasure']
|
'Junk','Map','Powerstone', 'Treasure']
|
||||||
|
|
||||||
banned_cards = [# in commander
|
banned_cards = [# in commander
|
||||||
|
@ -33,7 +39,7 @@ basic_lands = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest']
|
||||||
basic_lands = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest']
|
basic_lands = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest']
|
||||||
|
|
||||||
# Constants for lands matter functionality
|
# Constants for lands matter functionality
|
||||||
LANDS_MATTER_PATTERNS = {
|
LANDS_MATTER_PATTERNS: Dict[str, List[str]] = {
|
||||||
'land_play': [
|
'land_play': [
|
||||||
'play a land',
|
'play a land',
|
||||||
'play an additional land',
|
'play an additional land',
|
||||||
|
@ -661,7 +667,7 @@ DRAW_EXCLUSION_PATTERNS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
# Constants for DataFrame validation and processing
|
# Constants for DataFrame validation and processing
|
||||||
REQUIRED_COLUMNS = [
|
REQUIRED_COLUMNS: List[str] = [
|
||||||
'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors',
|
'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors',
|
||||||
'manaCost', 'manaValue', 'type', 'creatureTypes', 'text',
|
'manaCost', 'manaValue', 'type', 'creatureTypes', 'text',
|
||||||
'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side'
|
'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side'
|
||||||
|
@ -833,7 +839,7 @@ COLOR_ABRV = ['Colorless', 'W', 'U', 'B', 'G', 'R',
|
||||||
'B, R, U, W', 'B, G, R, U, W']
|
'B, R, U, W', 'B, G, R, U, W']
|
||||||
|
|
||||||
# Configuration for handling null/NA values in DataFrame columns
|
# Configuration for handling null/NA values in DataFrame columns
|
||||||
FILL_NA_COLUMNS = {
|
FILL_NA_COLUMNS: Dict[str, Optional[str]] = {
|
||||||
'colorIdentity': 'Colorless', # Default color identity for cards without one
|
'colorIdentity': 'Colorless', # Default color identity for cards without one
|
||||||
'faceName': None # Use card's name column value when face name is not available
|
'faceName': None # Use card's name column value when face name is not available
|
||||||
}
|
}
|
||||||
|
@ -844,7 +850,7 @@ SORT_CONFIG = {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration for DataFrame filtering operations
|
# Configuration for DataFrame filtering operations
|
||||||
FILTER_CONFIG = {
|
FILTER_CONFIG: Dict[str, Dict[str, List[str]]] = {
|
||||||
'layout': {
|
'layout': {
|
||||||
'exclude': ['reversible_card']
|
'exclude': ['reversible_card']
|
||||||
},
|
},
|
||||||
|
|
37
setup.py
37
setup.py
|
@ -1,15 +1,26 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
# Standard library imports
|
||||||
|
|
||||||
import pandas as pd # type: ignore
|
|
||||||
import inquirer.prompt # type: ignore
|
|
||||||
import logging
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union, List, Dict, Any
|
||||||
|
|
||||||
from settings import banned_cards, csv_directory, SETUP_COLORS, COLOR_ABRV, MTGJSON_API_URL
|
# Third-party imports
|
||||||
from setup_utils import download_cards_csv, filter_dataframe, process_legendary_cards, filter_by_color_identity
|
import pandas as pd
|
||||||
from exceptions import CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError, ColorFilterError, CommanderValidationError
|
import inquirer
|
||||||
|
|
||||||
|
# Local application imports
|
||||||
|
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, filter_by_color_identity
|
||||||
|
)
|
||||||
|
from exceptions import (
|
||||||
|
CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError,
|
||||||
|
ColorFilterError, CommanderValidationError
|
||||||
|
)
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
@ -18,7 +29,7 @@ logging.basicConfig(
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def check_csv_exists(file_path: str) -> bool:
|
def check_csv_exists(file_path: Union[str, Path]) -> bool:
|
||||||
"""Check if a CSV file exists at the specified path.
|
"""Check if a CSV file exists at the specified path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -83,7 +94,7 @@ def initial_setup() -> None:
|
||||||
logger.error(f'Error during initial setup: {str(e)}')
|
logger.error(f'Error during initial setup: {str(e)}')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def filter_by_color(df: pd.DataFrame, column_name: str, value: str, new_csv_name: str) -> None:
|
def filter_by_color(df: pd.DataFrame, column_name: str, value: str, new_csv_name: Union[str, Path]) -> None:
|
||||||
"""Filter DataFrame by color identity and save to CSV.
|
"""Filter DataFrame by color identity and save to CSV.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -248,11 +259,11 @@ def _display_setup_menu() -> SetupOption:
|
||||||
Returns:
|
Returns:
|
||||||
SetupOption: The selected menu option
|
SetupOption: The selected menu option
|
||||||
"""
|
"""
|
||||||
question = [
|
question: List[Dict[str, Any]] = [
|
||||||
inquirer.List('menu',
|
inquirer.List(
|
||||||
|
'menu',
|
||||||
choices=[option.value for option in SetupOption],
|
choices=[option.value for option in SetupOption],
|
||||||
carousel=True)
|
carousel=True)]
|
||||||
]
|
|
||||||
answer = inquirer.prompt(question)
|
answer = inquirer.prompt(question)
|
||||||
return SetupOption(answer['menu'])
|
return SetupOption(answer['menu'])
|
||||||
|
|
||||||
|
|
112
setup_utils.py
112
setup_utils.py
|
@ -1,12 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pandas as pd
|
# Standard library imports
|
||||||
import requests
|
|
||||||
import logging
|
import logging
|
||||||
from tqdm import tqdm
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Union, Dict, Any
|
from typing import List, Optional, Union, TypedDict
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
import pandas as pd
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
# Local application imports
|
||||||
from settings import (
|
from settings import (
|
||||||
CSV_PROCESSING_COLUMNS,
|
CSV_PROCESSING_COLUMNS,
|
||||||
CARD_TYPES_TO_EXCLUDE,
|
CARD_TYPES_TO_EXCLUDE,
|
||||||
|
@ -14,26 +18,58 @@ from settings import (
|
||||||
LEGENDARY_OPTIONS,
|
LEGENDARY_OPTIONS,
|
||||||
FILL_NA_COLUMNS,
|
FILL_NA_COLUMNS,
|
||||||
SORT_CONFIG,
|
SORT_CONFIG,
|
||||||
FILTER_CONFIG
|
FILTER_CONFIG,
|
||||||
)
|
)
|
||||||
from exceptions import CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError, ColorFilterError, CommanderValidationError
|
from exceptions import (
|
||||||
from settings import (
|
MTGJSONDownloadError,
|
||||||
CSV_PROCESSING_COLUMNS,
|
DataFrameProcessingError,
|
||||||
CARD_TYPES_TO_EXCLUDE,
|
ColorFilterError,
|
||||||
NON_LEGAL_SETS,
|
CommanderValidationError
|
||||||
LEGENDARY_OPTIONS
|
|
||||||
)
|
)
|
||||||
from exceptions import CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError
|
|
||||||
|
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Type definitions
|
||||||
|
class FilterRule(TypedDict):
|
||||||
|
"""Type definition for filter rules configuration."""
|
||||||
|
exclude: Optional[List[str]]
|
||||||
|
require: Optional[List[str]]
|
||||||
|
|
||||||
|
class FilterConfig(TypedDict):
|
||||||
|
"""Type definition for complete filter configuration."""
|
||||||
|
layout: FilterRule
|
||||||
|
availability: FilterRule
|
||||||
|
promoTypes: FilterRule
|
||||||
|
securityStamp: FilterRule
|
||||||
def download_cards_csv(url: str, output_path: Union[str, Path]) -> None:
|
def download_cards_csv(url: str, output_path: Union[str, Path]) -> None:
|
||||||
"""Download cards data from MTGJSON and save to CSV.
|
"""Download cards data from MTGJSON and save to CSV.
|
||||||
|
|
||||||
|
Downloads card data from the specified MTGJSON URL and saves it to a local CSV file.
|
||||||
|
Shows a progress bar during download using tqdm.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: URL to download cards data from
|
url: URL to download cards data from (typically MTGJSON API endpoint)
|
||||||
output_path: Path to save the downloaded CSV file
|
output_path: Path where the downloaded CSV file will be saved
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
MTGJSONDownloadError: If download fails or response is invalid
|
MTGJSONDownloadError: If download fails due to network issues or invalid response
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> download_cards_csv('https://mtgjson.com/api/v5/cards.csv', 'cards.csv')
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, stream=True)
|
response = requests.get(url, stream=True)
|
||||||
|
@ -47,35 +83,49 @@ def download_cards_csv(url: str, output_path: Union[str, Path]) -> None:
|
||||||
pbar.update(size)
|
pbar.update(size)
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
|
logging.error(f'Failed to download cards data from {url}')
|
||||||
raise MTGJSONDownloadError(
|
raise MTGJSONDownloadError(
|
||||||
"Failed to download cards data",
|
"Failed to download cards data",
|
||||||
url,
|
url,
|
||||||
getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None
|
getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
def check_csv_exists(filepath: Union[str, Path]) -> bool:
|
def check_csv_exists(filepath: Union[str, Path]) -> bool:
|
||||||
"""Check if a CSV file exists at the specified path.
|
"""Check if a CSV file exists at the specified path.
|
||||||
|
|
||||||
|
Verifies the existence of a CSV file at the given path. This function is used
|
||||||
|
to determine if card data needs to be downloaded or if it already exists locally.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filepath: Path to check for CSV file
|
filepath: Path to the CSV file to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if file exists, False otherwise
|
bool: True if the file exists, False otherwise
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> if not check_csv_exists('cards.csv'):
|
||||||
|
... download_cards_csv(MTGJSON_API_URL, 'cards.csv')
|
||||||
"""
|
"""
|
||||||
return Path(filepath).is_file()
|
return Path(filepath).is_file()
|
||||||
|
|
||||||
def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
|
def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
|
||||||
"""Apply standard filters to the cards DataFrame using configuration from settings.
|
"""Apply standard filters to the cards DataFrame using configuration from settings.
|
||||||
|
|
||||||
|
Applies a series of filters to the cards DataFrame based on configuration from settings.py.
|
||||||
|
This includes handling null values, applying basic filters, removing illegal sets and banned cards,
|
||||||
|
and processing special card types.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
df: DataFrame to filter
|
df: pandas DataFrame containing card data to filter
|
||||||
banned_cards: List of banned card names to exclude
|
banned_cards: List of card names that are banned and should be excluded
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Filtered DataFrame
|
pd.DataFrame: A new DataFrame containing only the cards that pass all filters
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
DataFrameProcessingError: If filtering operations fail
|
DataFrameProcessingError: If any filtering operation fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> filtered_df = filter_dataframe(cards_df, ['Channel', 'Black Lotus'])
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info('Starting standard DataFrame filtering')
|
logging.info('Starting standard DataFrame filtering')
|
||||||
|
@ -89,7 +139,8 @@ def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
|
||||||
|
|
||||||
# Apply basic filters from configuration
|
# Apply basic filters from configuration
|
||||||
filtered_df = df.copy()
|
filtered_df = df.copy()
|
||||||
for field, rules in FILTER_CONFIG.items():
|
filter_config: FilterConfig = FILTER_CONFIG # Type hint for configuration
|
||||||
|
for field, rules in filter_config.items():
|
||||||
for rule_type, values in rules.items():
|
for rule_type, values in rules.items():
|
||||||
if rule_type == 'exclude':
|
if rule_type == 'exclude':
|
||||||
for value in values:
|
for value in values:
|
||||||
|
@ -126,12 +177,12 @@ def filter_dataframe(df: pd.DataFrame, banned_cards: List[str]) -> pd.DataFrame:
|
||||||
return filtered_df
|
return filtered_df
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.error(f'Failed to filter DataFrame: {str(e)}')
|
||||||
raise DataFrameProcessingError(
|
raise DataFrameProcessingError(
|
||||||
"Failed to filter DataFrame",
|
"Failed to filter DataFrame",
|
||||||
"standard_filtering",
|
"standard_filtering",
|
||||||
str(e)
|
str(e)
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
def filter_by_color_identity(df: pd.DataFrame, color_identity: str) -> pd.DataFrame:
|
def filter_by_color_identity(df: pd.DataFrame, color_identity: str) -> pd.DataFrame:
|
||||||
"""Filter DataFrame by color identity with additional color-specific processing.
|
"""Filter DataFrame by color identity with additional color-specific processing.
|
||||||
|
|
||||||
|
@ -153,14 +204,6 @@ def filter_by_color_identity(df: pd.DataFrame, color_identity: str) -> pd.DataFr
|
||||||
try:
|
try:
|
||||||
logging.info(f'Filtering cards for color identity: {color_identity}')
|
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
|
# Validate color identity
|
||||||
with tqdm(total=1, desc='Validating color identity') as pbar:
|
with tqdm(total=1, desc='Validating color identity') as pbar:
|
||||||
if not isinstance(color_identity, str):
|
if not isinstance(color_identity, str):
|
||||||
|
@ -217,11 +260,6 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info('Starting commander validation process')
|
logging.info('Starting commander validation process')
|
||||||
validation_steps = [
|
|
||||||
'Checking legendary status',
|
|
||||||
'Validating special cases',
|
|
||||||
'Verifying set legality'
|
|
||||||
]
|
|
||||||
|
|
||||||
filtered_df = df.copy()
|
filtered_df = df.copy()
|
||||||
# Step 1: Check legendary status
|
# Step 1: Check legendary status
|
||||||
|
|
|
@ -1,12 +1,28 @@
|
||||||
import pandas as pd
|
"""Utility module for tag manipulation and pattern matching in card data processing.
|
||||||
|
|
||||||
|
This module provides a collection of functions for working with card tags, types, and text patterns
|
||||||
|
in a card game context. It includes utilities for:
|
||||||
|
|
||||||
|
- Creating boolean masks for filtering cards based on various criteria
|
||||||
|
- Manipulating and extracting card types
|
||||||
|
- Managing theme tags and card attributes
|
||||||
|
- Pattern matching in card text and types
|
||||||
|
- Mass effect detection (damage, removal, etc.)
|
||||||
|
|
||||||
|
The module is designed to work with pandas DataFrames containing card data and provides
|
||||||
|
vectorized operations for efficient processing of large card collections.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
import re
|
import re
|
||||||
import logging
|
from typing import List, Set, Union, Any
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Set, Union
|
# Third-party imports
|
||||||
from time import perf_counter
|
import pandas as pd
|
||||||
|
|
||||||
|
# Local application imports
|
||||||
import settings
|
import settings
|
||||||
|
|
||||||
def pluralize(word: str) -> str:
|
def pluralize(word: str) -> str:
|
||||||
"""Convert a word to its plural form using basic English pluralization rules.
|
"""Convert a word to its plural form using basic English pluralization rules.
|
||||||
|
|
||||||
|
@ -25,7 +41,7 @@ def pluralize(word: str) -> str:
|
||||||
else:
|
else:
|
||||||
return word + 's'
|
return word + 's'
|
||||||
|
|
||||||
def sort_list(items: Union[List, pd.Series]) -> Union[List, pd.Series]:
|
def sort_list(items: Union[List[Any], pd.Series]) -> Union[List[Any], pd.Series]:
|
||||||
"""Sort a list or pandas Series in ascending order.
|
"""Sort a list or pandas Series in ascending order.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -38,7 +54,7 @@ def sort_list(items: Union[List, pd.Series]) -> Union[List, pd.Series]:
|
||||||
return sorted(items) if isinstance(items, list) else items.sort_values()
|
return sorted(items) if isinstance(items, list) else items.sort_values()
|
||||||
return items
|
return items
|
||||||
|
|
||||||
def create_type_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True) -> pd.Series:
|
def create_type_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True) -> pd.Series[bool]:
|
||||||
"""Create a boolean mask for rows where type matches one or more patterns.
|
"""Create a boolean mask for rows where type matches one or more patterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -68,7 +84,7 @@ def create_type_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex:
|
||||||
masks = [df['type'].str.contains(p, case=False, na=False, regex=False) for p in type_text]
|
masks = [df['type'].str.contains(p, case=False, na=False, regex=False) for p in type_text]
|
||||||
return pd.concat(masks, axis=1).any(axis=1)
|
return pd.concat(masks, axis=1).any(axis=1)
|
||||||
|
|
||||||
def create_text_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True, combine_with_or: bool = True) -> pd.Series:
|
def create_text_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True, combine_with_or: bool = True) -> pd.Series[bool]:
|
||||||
"""Create a boolean mask for rows where text matches one or more patterns.
|
"""Create a boolean mask for rows where text matches one or more patterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -102,7 +118,7 @@ def create_text_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex:
|
||||||
else:
|
else:
|
||||||
return pd.concat(masks, axis=1).all(axis=1)
|
return pd.concat(masks, axis=1).all(axis=1)
|
||||||
|
|
||||||
def create_keyword_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True) -> pd.Series:
|
def create_keyword_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True) -> pd.Series[bool]:
|
||||||
"""Create a boolean mask for rows where keyword text matches one or more patterns.
|
"""Create a boolean mask for rows where keyword text matches one or more patterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -146,7 +162,8 @@ def create_keyword_mask(df: pd.DataFrame, type_text: Union[str, List[str]], rege
|
||||||
else:
|
else:
|
||||||
masks = [keywords.str.contains(p, case=False, na=False, regex=False) for p in type_text]
|
masks = [keywords.str.contains(p, case=False, na=False, regex=False) for p in type_text]
|
||||||
return pd.concat(masks, axis=1).any(axis=1)
|
return pd.concat(masks, axis=1).any(axis=1)
|
||||||
def create_name_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True) -> pd.Series:
|
|
||||||
|
def create_name_mask(df: pd.DataFrame, type_text: Union[str, List[str]], regex: bool = True) -> pd.Series[bool]:
|
||||||
"""Create a boolean mask for rows where name matches one or more patterns.
|
"""Create a boolean mask for rows where name matches one or more patterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -229,7 +246,7 @@ def add_outlaw_type(types: List[str], outlaw_types: List[str]) -> List[str]:
|
||||||
return types + ['Outlaw']
|
return types + ['Outlaw']
|
||||||
return types
|
return types
|
||||||
|
|
||||||
def create_tag_mask(df: pd.DataFrame, tag_patterns: Union[str, List[str]], column: str = 'themeTags') -> pd.Series:
|
def create_tag_mask(df: pd.DataFrame, tag_patterns: Union[str, List[str]], column: str = 'themeTags') -> pd.Series[bool]:
|
||||||
"""Create a boolean mask for rows where tags match specified patterns.
|
"""Create a boolean mask for rows where tags match specified patterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -272,7 +289,7 @@ def validate_dataframe_columns(df: pd.DataFrame, required_columns: Set[str]) ->
|
||||||
if missing:
|
if missing:
|
||||||
raise ValueError(f"Missing required columns: {missing}")
|
raise ValueError(f"Missing required columns: {missing}")
|
||||||
|
|
||||||
def apply_tag_vectorized(df: pd.DataFrame, mask: pd.Series, tags: List[str]) -> None:
|
def apply_tag_vectorized(df: pd.DataFrame, mask: pd.Series[bool], tags: Union[str, List[str]]) -> None:
|
||||||
"""Apply tags to rows in a dataframe based on a boolean mask.
|
"""Apply tags to rows in a dataframe based on a boolean mask.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -288,7 +305,8 @@ def apply_tag_vectorized(df: pd.DataFrame, mask: pd.Series, tags: List[str]) ->
|
||||||
|
|
||||||
# Add new tags
|
# Add new tags
|
||||||
df.loc[mask, 'themeTags'] = current_tags.apply(lambda x: sorted(list(set(x + tags))))
|
df.loc[mask, 'themeTags'] = current_tags.apply(lambda x: sorted(list(set(x + tags))))
|
||||||
def create_mass_effect_mask(df: pd.DataFrame, effect_type: str) -> pd.Series:
|
|
||||||
|
def create_mass_effect_mask(df: pd.DataFrame, effect_type: str) -> pd.Series[bool]:
|
||||||
"""Create a boolean mask for cards with mass removal effects of a specific type.
|
"""Create a boolean mask for cards with mass removal effects of a specific type.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -306,6 +324,7 @@ def create_mass_effect_mask(df: pd.DataFrame, effect_type: str) -> pd.Series:
|
||||||
|
|
||||||
patterns = settings.BOARD_WIPE_TEXT_PATTERNS[effect_type]
|
patterns = settings.BOARD_WIPE_TEXT_PATTERNS[effect_type]
|
||||||
return create_text_mask(df, patterns)
|
return create_text_mask(df, patterns)
|
||||||
|
|
||||||
def create_damage_pattern(number: Union[int, str]) -> str:
|
def create_damage_pattern(number: Union[int, str]) -> str:
|
||||||
"""Create a pattern for matching X damage effects.
|
"""Create a pattern for matching X damage effects.
|
||||||
|
|
||||||
|
@ -316,7 +335,8 @@ def create_damage_pattern(number: Union[int, str]) -> str:
|
||||||
Pattern string for matching damage effects
|
Pattern string for matching damage effects
|
||||||
"""
|
"""
|
||||||
return f'deals {number} damage'
|
return f'deals {number} damage'
|
||||||
def create_mass_damage_mask(df: pd.DataFrame) -> pd.Series:
|
|
||||||
|
def create_mass_damage_mask(df: pd.DataFrame) -> pd.Series[bool]:
|
||||||
"""Create a boolean mask for cards with mass damage effects.
|
"""Create a boolean mask for cards with mass damage effects.
|
||||||
|
|
||||||
Args:
|
Args:
|
Loading…
Add table
Add a link
Reference in a new issue