Began work on overhauling the deck_builder

This commit is contained in:
mwisnowski 2025-01-14 12:07:49 -08:00
parent e0dd09adee
commit 319f7848d3
10 changed files with 2589 additions and 761 deletions

View file

@ -4,57 +4,204 @@ 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.
"""
from __future__ import annotations
# Standard library imports
import logging
import time
from functools import lru_cache
from typing import Optional
from typing import Dict, List, Optional, Tuple, Union
# Third-party imports
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.
from scrython.cards import Named as ScryfallCard
# Local imports
from exceptions import (
PriceAPIError,
PriceLimitError,
PriceTimeoutError,
PriceValidationError
)
from settings import (
BATCH_PRICE_CHECK_SIZE,
DEFAULT_MAX_CARD_PRICE,
DEFAULT_MAX_DECK_PRICE,
DEFAULT_PRICE_DELAY,
MAX_PRICE_CHECK_ATTEMPTS,
PRICE_CACHE_SIZE,
PRICE_CHECK_TIMEOUT,
PRICE_TOLERANCE_MULTIPLIER
)
from type_definitions import PriceCache
class PriceChecker:
"""Class for handling MTG card price checking and validation.
This class provides functionality for checking card prices via the Scryfall API,
validating prices against thresholds, and managing price caching.
Attributes:
price_cache (Dict[str, float]): Cache of card prices
max_card_price (float): Maximum allowed price per card
max_deck_price (float): Maximum allowed total deck price
current_deck_price (float): Current total price of the deck
"""
retries = 0
last_error = None
while retries < PRICE_CHECK_CONFIG['max_retries']:
def __init__(
self,
max_card_price: float = DEFAULT_MAX_CARD_PRICE,
max_deck_price: float = DEFAULT_MAX_DECK_PRICE
) -> None:
"""Initialize the PriceChecker.
Args:
max_card_price: Maximum allowed price per card
max_deck_price: Maximum allowed total deck price
"""
self.price_cache: PriceCache = {}
self.max_card_price: float = max_card_price
self.max_deck_price: float = max_deck_price
self.current_deck_price: float = 0.0
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
@lru_cache(maxsize=PRICE_CACHE_SIZE)
def get_card_price(self, card_name: str, attempts: int = 0) -> float:
"""Get the price of a card with caching and retry logic.
Args:
card_name: Name of the card to check
attempts: Current number of retry attempts
Returns:
Float price of the card in USD
Raises:
PriceAPIError: If price lookup fails after max attempts
PriceTimeoutError: If request times out
PriceValidationError: If received price data is invalid
"""
# Check cache first
if card_name in self.price_cache:
return self.price_cache[card_name]
try:
card = Named(fuzzy=card_name)
price = card.prices('usd')
print(price)
# Add delay between API calls
time.sleep(DEFAULT_PRICE_DELAY)
if price is None:
raise PriceCheckError(
"No price data available",
card_name,
"Card may be too new or not available in USD"
)
# Make API request with type hints
card: ScryfallCard = scrython.cards.Named(fuzzy=card_name, timeout=PRICE_CHECK_TIMEOUT)
price: Optional[str] = card.prices('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}"
)
# Handle None or empty string cases
if price is None or price == "":
return 0.0
# Validate and cache price
if isinstance(price, (int, float, str)):
try:
# Convert string or numeric price to float
price_float = float(price)
self.price_cache[card_name] = price_float
return price_float
except ValueError:
raise PriceValidationError(card_name, str(price))
return 0.0
except scrython.foundation.ScryfallError as e:
if attempts < MAX_PRICE_CHECK_ATTEMPTS:
logging.warning(f"Retrying price check for {card_name} (attempt {attempts + 1})")
return self.get_card_price(card_name, attempts + 1)
raise PriceAPIError(card_name, {"error": str(e)})
except TimeoutError:
raise PriceTimeoutError(card_name, PRICE_CHECK_TIMEOUT)
except Exception as e:
if attempts < MAX_PRICE_CHECK_ATTEMPTS:
logging.warning(f"Unexpected error checking price for {card_name}, retrying")
return self.get_card_price(card_name, attempts + 1)
raise PriceAPIError(card_name, {"error": str(e)})
def validate_card_price(self, card_name: str, price: float) -> bool | None:
"""Validate if a card's price is within allowed limits.
Args:
card_name: Name of the card to validate
price: Price to validate
Returns:
True if price is valid, False otherwise
Raises:
PriceLimitError: If price exceeds maximum allowed
"""
if price > self.max_card_price * PRICE_TOLERANCE_MULTIPLIER:
raise PriceLimitError(card_name, price, self.max_card_price)
return True
def validate_deck_price(self) -> bool | None:
"""Validate if the current deck price is within allowed limits.
Returns:
True if deck price is valid, False otherwise
Raises:
PriceLimitError: If deck price exceeds maximum allowed
"""
if self.current_deck_price > self.max_deck_price * PRICE_TOLERANCE_MULTIPLIER:
raise PriceLimitError("deck", self.current_deck_price, self.max_deck_price)
return True
def batch_check_prices(self, card_names: List[str]) -> Dict[str, float]:
"""Check prices for multiple cards efficiently.
Args:
card_names: List of card names to check prices for
Returns:
Dictionary mapping card names to their prices
Raises:
PriceAPIError: If batch price lookup fails
"""
results: Dict[str, float] = {}
errors: List[Tuple[str, Exception]] = []
# Process in batches
for i in range(0, len(card_names), BATCH_PRICE_CHECK_SIZE):
batch = card_names[i:i + BATCH_PRICE_CHECK_SIZE]
for card_name in batch:
try:
price = self.get_card_price(card_name)
results[card_name] = price
except Exception as e:
errors.append((card_name, e))
logging.error(f"Error checking price for {card_name}: {e}")
if errors:
logging.warning(f"Failed to get prices for {len(errors)} cards")
return results
def update_deck_price(self, price: float) -> None:
"""Update the current deck price.
Args:
price: Price to add to current deck total
"""
self.current_deck_price += price
logging.debug(f"Updated deck price to ${self.current_deck_price:.2f}")
def clear_cache(self) -> None:
"""Clear the price cache."""
self.price_cache.clear()
self.get_card_price.cache_clear()
logging.info("Price cache cleared")