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:
mwisnowski 2025-01-14 09:06:59 -08:00
parent 000d804ba7
commit b8d9958564
8 changed files with 592 additions and 466 deletions

1
.gitignore vendored
View file

@ -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
View file

@ -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
View 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

View file

@ -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']
}, },

View file

@ -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'])

View file

@ -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

View file

@ -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:

765
tagger.py

File diff suppressed because it is too large Load diff