From c1de207f59787d9acefd1b4e2490a7d0320e2df0 Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sat, 30 Jan 2021 14:50:03 +0000 Subject: [PATCH 1/3] Simplify archidekt deck fetching logic --- archiTop/base_classes/deck_fetcher.py | 82 +++------------------ archiTop/data_types.py | 3 +- archiTop/deck_builder/final_deck_builder.py | 5 +- archiTop/deck_fetcher/__init__.py | 1 + archiTop/deck_fetcher/archidekt.py | 50 +++++-------- 5 files changed, 32 insertions(+), 109 deletions(-) diff --git a/archiTop/base_classes/deck_fetcher.py b/archiTop/base_classes/deck_fetcher.py index 2dd5e29..61a5c74 100644 --- a/archiTop/base_classes/deck_fetcher.py +++ b/archiTop/base_classes/deck_fetcher.py @@ -19,7 +19,7 @@ class DeckFetcherError(Exception): class DeckFetcher(ABC): """Abstract baseclass to for deck fetcher""" base_url = None - mainboard_cards = [] + mainboard_cards: List[RawCard] = [] def __init__(self, deck_id: int): """Initializes deck fetcher with id of deck. @@ -45,7 +45,7 @@ def _request_raw_deck(self) -> requests.Response: return response @staticmethod - def _parse_raw_deck_data(request: requests.Response) -> dict: + def _parse_data(request: requests.Response) -> dict: return request.json() def get_deck(self) -> RawDeck: @@ -55,42 +55,23 @@ def get_deck(self) -> RawDeck: Deck of cards, containing deck information fetched """ raw_deck_response = self._request_raw_deck() + self._handle_response(raw_deck_response) - self._handle_raw_deck_request(raw_deck_response) + data = self._parse_data(raw_deck_response) + deck_name = self._parse_deck_name(data) + thumbnail = self._get_thumbnail(data) - raw_deck_data = self._parse_raw_deck_data(raw_deck_response) - - deck_name = self._parse_deck_name(raw_deck_data) - thumbnail_url = self._parse_deck_thumbnail_url(raw_deck_data) - - thumbnail = requests.get(thumbnail_url).content - - mainboard_identifier = self._parse_mainboard_identifier(raw_deck_data) - filtered_mainboard_card_data = [ - card for card in self._parse_card_data(raw_deck_data) - if self._validate_single_card_mainboard(card, mainboard_identifier)] - - self.mainboard_cards = [self._parse_single_card(card) for card in - filtered_mainboard_card_data] + self.mainboard_cards = self._parse_mainboard_cards(data) return RawDeck(self.mainboard_cards, deck_name, thumbnail) @abstractmethod - def _parse_single_card(self, card: dict) -> RawCard: - """Abstractmethod to be implemented by child class. - Parses single card information from deck service into Card object. - - Args: - card: Card json object to parse information from - - Returns: - Card class containing parsed information from card json object - """ + def _parse_mainboard_cards(self, data: dict) -> List[RawCard]: raise NotImplemented @staticmethod @abstractmethod - def _handle_raw_deck_request(response: requests.Response): + def _handle_response(response: requests.Response): """Abstractmethod to be implemented by child class. Validates whether request to server was successful. @@ -99,20 +80,6 @@ def _handle_raw_deck_request(response: requests.Response): """ raise NotImplemented - @staticmethod - @abstractmethod - def _parse_card_data(raw_deck_data: dict) -> List[dict]: - """Abstractmethod to be implemented by child class. - Parses card information from deck data fetched by `_get_raw_deck_data()`. - - Args: - raw_deck_data: Raw server data fetched by deck data request - - Returns: - List of card json objects contained in deck - """ - raise NotImplemented - @staticmethod @abstractmethod def _parse_deck_name(raw_deck_data: dict) -> str: @@ -129,7 +96,7 @@ def _parse_deck_name(raw_deck_data: dict) -> str: @staticmethod @abstractmethod - def _parse_deck_thumbnail_url(raw_deck_data: dict) -> str: + def _get_thumbnail(raw_deck_data: dict) -> bytes: """Abstractmethod to be implemented by child class. Parses thumbnail url from deck data fetched by `_get_raw_deck_data()`. @@ -140,32 +107,3 @@ def _parse_deck_thumbnail_url(raw_deck_data: dict) -> str: Thumbnail url for fetched deck information """ raise NotImplemented - - @staticmethod - @abstractmethod - def _validate_single_card_mainboard(card: dict, mainboard_identifier: Any) -> bool: - """Abstractmethod to be implemented by child class. - Validates whether a single card belongs to mainboard using the passed mainboard_identifier. - - Args: - card: Card json object contained in fetched deck information - mainboard_identifier: Identifier to validate card belongs to mainboard - - Returns: - True when card is contained in mainboard, False otherwise - """ - raise NotImplemented - - @staticmethod - @abstractmethod - def _parse_mainboard_identifier(raw_deck_data: dict) -> Any: - """Abstractmethod to be implemented by child class. - Parses the identifier for mainboard cards from raw data fetched. - - Args: - raw_deck_data: Raw data fetched from server - - Returns: - Identifier object - """ - raise NotImplemented diff --git a/archiTop/data_types.py b/archiTop/data_types.py index e15ccf1..5889f38 100644 --- a/archiTop/data_types.py +++ b/archiTop/data_types.py @@ -9,11 +9,10 @@ class RawCard: name: str # unique card name quantity: int # quantity for card uid: str # unique card identifier - editioncode: str = None # edition code for card commander: bool = False # flag whether card is commander def __repr__(self): - return f'RawCard({self.quantity: <3}x {self.name} - {self.editioncode} - {self.uid})' + return f'RawCard({self.quantity: <3}x {self.name} - {self.uid})' @dataclass diff --git a/archiTop/deck_builder/final_deck_builder.py b/archiTop/deck_builder/final_deck_builder.py index fbeb370..f5bf1a6 100644 --- a/archiTop/deck_builder/final_deck_builder.py +++ b/archiTop/deck_builder/final_deck_builder.py @@ -101,8 +101,9 @@ def save_deck(self, export_location: str = None): # save deck json json.dump(self.final_deck_json, open(Path(export_location, deck_name), 'w')) # save deck thumbnail - with open(Path(export_location, thumbnail_name), 'wb') as file: - file.write(self.deck.thumbnail) + if self.deck.thumbnail: + with open(Path(export_location, thumbnail_name), 'wb') as file: + file.write(self.deck.thumbnail) spin_logger.debug('Saved %s', log_message, extra={'user_waiting': False}) def _construct_card_deck(self, card_list: List[ScryfallCard], diff --git a/archiTop/deck_fetcher/__init__.py b/archiTop/deck_fetcher/__init__.py index 8a59e82..5bd14f9 100644 --- a/archiTop/deck_fetcher/__init__.py +++ b/archiTop/deck_fetcher/__init__.py @@ -1 +1,2 @@ from .archidekt import ArchidektFetcher +from .moxfield import MoxfieldFetcher diff --git a/archiTop/deck_fetcher/archidekt.py b/archiTop/deck_fetcher/archidekt.py index 575e9de..aaf234d 100644 --- a/archiTop/deck_fetcher/archidekt.py +++ b/archiTop/deck_fetcher/archidekt.py @@ -11,7 +11,8 @@ class ArchidektFetcher(DeckFetcher): """ArchidektFetcher class, implementing abstract baseclass DeckFetcher""" base_url = 'https://archidekt.com/api/decks/%s/small/' - def _parse_single_card(self, card: dict) -> RawCard: + @staticmethod + def _parse_single_card(card: dict) -> RawCard: """Parses single card information from deck service into Card object. Args: @@ -26,13 +27,12 @@ def _parse_single_card(self, card: dict) -> RawCard: uid = card_data['uid'] quantity = card['quantity'] - edition_code = card_data['edition']['editioncode'] commander = card['category'] == 'Commander' or 'Commander' in card.get('categories', ()) - return RawCard(name, quantity, uid, edition_code, commander) + return RawCard(name, quantity, uid, commander) @staticmethod - def _handle_raw_deck_request(response: requests.Response): + def _handle_response(response: requests.Response): """Handles response from archidekt api, validating request was successful. Raises: @@ -53,18 +53,6 @@ def _handle_raw_deck_request(response: requests.Response): raise DeckFetcherError(f'Failed to fetch archidekt deck with error:\n{error_message}') - @staticmethod - def _parse_card_data(raw_deck_data: dict) -> List[dict]: - """Parses card information from deck data fetched by `_get_raw_deck_data()`. - - Args: - raw_deck_data: Raw server data fetched by deck data request - - Returns: - List of card json objects contained in deck - """ - return raw_deck_data['cards'] - @staticmethod def _parse_deck_name(raw_deck_data: dict) -> str: """Parses deck name from deck data fetched by `_get_raw_deck_data()`. @@ -78,7 +66,7 @@ def _parse_deck_name(raw_deck_data: dict) -> str: return raw_deck_data['name'] @staticmethod - def _parse_deck_thumbnail_url(raw_deck_data: dict) -> str: + def _get_thumbnail(raw_deck_data: dict) -> bytes: """Parses thumbnail url from deck data fetched by `_get_raw_deck_data()`. Args: raw_deck_data: Raw server data fetched by deck data request @@ -86,26 +74,22 @@ def _parse_deck_thumbnail_url(raw_deck_data: dict) -> str: Returns: Thumbnail url for fetched deck information """ - return raw_deck_data['featured'] + url = raw_deck_data['featured'] + return requests.get(url).content - @staticmethod - def _parse_mainboard_identifier(raw_deck_data: dict) -> Set[str]: - """Extracts valid categories for cards belonging to mainboard. - Archidekt has functionality of marking categories as not included in the deck, - these are being filtered out. + def _parse_mainboard_cards(self, data: dict) -> List[RawCard]: + # identify categories for all valid mainboard cards + valid_categories = {category['name'] for category in data['categories'] + if category['includedInDeck']} - {'Sideboard'} - Args: - raw_deck_data: Raw json response data from archidekt api + mainboard_cards = [card for card in data['cards'] + if self._validate_mainboard_cards(card, valid_categories)] + + return [self._parse_single_card(card) for card in mainboard_cards] - Returns: - Set containing valid categories - """ - valid_categories = {category['name'] for category in raw_deck_data['categories'] - if category['includedInDeck']} - return valid_categories - {'Sideboard'} @staticmethod - def _validate_single_card_mainboard(card: dict, mainboard_identifier: Set[str]) -> bool: + def _validate_mainboard_cards(card: dict, mainboard_categories: Set[str]) -> bool: """Validates whether a single card belongs to mainboard. Args: @@ -118,4 +102,4 @@ def _validate_single_card_mainboard(card: dict, mainboard_identifier: Set[str]) # return category_check and categories_checks category = card['categories'][0] if len(card['categories']) > 0 else None - return category in mainboard_identifier + return category in mainboard_categories From f5c2ef3ad794aef5dd438c5a7fa166b8dd76102f Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sat, 30 Jan 2021 14:50:13 +0000 Subject: [PATCH 2/3] Add feck fetcher for moxfield decks --- archiTop/__main__.py | 11 ++-- archiTop/config.ini | 2 +- archiTop/deck_fetcher/moxfield.py | 88 +++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 archiTop/deck_fetcher/moxfield.py diff --git a/archiTop/__main__.py b/archiTop/__main__.py index dac49a0..0bb8ffe 100644 --- a/archiTop/__main__.py +++ b/archiTop/__main__.py @@ -4,7 +4,7 @@ from archiTop.config import get_spin_logger from archiTop.deck_builder import DeckBuilderWrapper -from archiTop.deck_fetcher import ArchidektFetcher +from archiTop.deck_fetcher import ArchidektFetcher, MoxfieldFetcher from archiTop.scryfall import ScryfallDeckBuilder spin_logger = get_spin_logger(__name__) @@ -28,8 +28,13 @@ def setup_argparse(): def main(): args = setup_argparse() - # fetch raw deck information (card names and count) - deck = ArchidektFetcher(args.deckID).get_deck() + # decide which service to request deck from + if args.deckID.isnumeric(): + spin_logger.debug('Fetching Archidekt deck', extra={'user_waiting': False}) + deck = ArchidektFetcher(args.deckID).get_deck() + else: + spin_logger.debug('Fetching Moxfield deck', extra={'user_waiting': False}) + deck = MoxfieldFetcher(args.deckID).get_deck() # overwrite deckname if optional argument is specified deck.name = args.name if args.name else deck.name diff --git a/archiTop/config.ini b/archiTop/config.ini index af119d7..d1a3a41 100644 --- a/archiTop/config.ini +++ b/archiTop/config.ini @@ -2,7 +2,7 @@ LOG_LEVEL: DEBUG [DECK] -DEFAULT_CARDBACK_URL: https://www.frogtown.me/images/gatherer/CardBack.jpg +DEFAULT_CARDBACK_URL: https://i.imgur.com/LdOBU1I.jpg [EXPORT] # path to Tabletop Save location, root being the home directory diff --git a/archiTop/deck_fetcher/moxfield.py b/archiTop/deck_fetcher/moxfield.py new file mode 100644 index 0000000..6a1de9a --- /dev/null +++ b/archiTop/deck_fetcher/moxfield.py @@ -0,0 +1,88 @@ +"""Sourcefile containing class to interact with moxfield services""" +from typing import List, Set + +import requests + +from archiTop.base_classes import DeckFetcher, DeckFetcherError +from archiTop.data_types import RawCard +from archiTop.scryfall import load_scryfall_id_index + + +class MoxfieldFetcher(DeckFetcher): + """MoxfieldFetcher class, implementing abstract baseclass DeckFetcher""" + base_url = ' https://api.moxfield.com/v2/decks/all/%s' + scryfall_id_index = load_scryfall_id_index() + + @staticmethod + def _parse_card(data: dict, commander: bool = False) -> RawCard: + """Parses single card information from deck service into Card object. + + Args: + card: Card json object to parse information from + + Returns: + Card class containing parsed information from card json object + """ + quantity = data['quantity'] + name = data['card']['name'] + uid = data['card']['scryfall_id'] + + return RawCard(name, quantity, uid, commander) + + @staticmethod + def _handle_response(response: requests.Response): + """Handles response from moxfield api, validating request was successful. + + Raises: + DeckFetcherError: When invalid status code was encountered in response from api + + Args: + response: Response object from archidekt api call + """ + try: + response.raise_for_status() + + except requests.HTTPError as e: + raise DeckFetcherError(f'Failed to fetch moxfield deck with error:\n{e}') + + def _parse_mainboard_cards(self, data: dict) -> List[RawCard]: + """Parses card information from deck data fetched by `_get_raw_deck_data()`. + + Args: + raw_deck_data: Raw server data fetched by deck data request + + Returns: + List of card json objects contained in deck + """ + mainboard_cards = [self._parse_card(card) for card in data['mainboard'].values()] + mainboard_cards += [self._parse_card(card, commander=True) + for card in data['commanders'].values()] + return mainboard_cards + + @staticmethod + def _parse_deck_name(data: dict) -> str: + """Parses deck name from deck data fetched by `_get_raw_deck_data()`. + + Args: + data: Raw server data fetched by deck data request + + Returns: + Name of deck + """ + return data['name'] + + def _get_thumbnail(self, data: dict) -> bytes: + """Parses thumbnail url from deck data fetched by `_get_raw_deck_data()`. + Args: + data: Raw server data fetched by deck data request + + Returns: + Thumbnail url for fetched deck information + """ + if data['commanders'].values(): + card = list(data['commanders'].values())[0] + scryfall_card = self.scryfall_id_index[card['card']['scryfall_id']] + + image_uris = scryfall_card.get('image_uris') or scryfall_card.get('card_faces')[0][ + 'image_uris'] + return requests.get(image_uris['art_crop']).content From bfcf003748d083a89b54699f2d4104b9e091e6f1 Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sat, 30 Jan 2021 14:50:50 +0000 Subject: [PATCH 3/3] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index df7d53d..3bb26b7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='ArchiTop', - version='0.2.5', + version='0.3', author='Julian Brendel', author_email='julian.brendel@t-online.de', packages=find_packages(),