Skip to content

Commit

Permalink
Merge pull request #48 from Julian-Brendel/moxfield-support
Browse files Browse the repository at this point in the history
Moxfield support
  • Loading branch information
Julian-Brendel authored Jan 30, 2021
2 parents d86e881 + bfcf003 commit 85edc79
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 114 deletions.
11 changes: 8 additions & 3 deletions archiTop/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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
Expand Down
82 changes: 10 additions & 72 deletions archiTop/base_classes/deck_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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()`.
Expand All @@ -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
2 changes: 1 addition & 1 deletion archiTop/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions archiTop/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions archiTop/deck_builder/final_deck_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions archiTop/deck_fetcher/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .archidekt import ArchidektFetcher
from .moxfield import MoxfieldFetcher
50 changes: 17 additions & 33 deletions archiTop/deck_fetcher/archidekt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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()`.
Expand All @@ -78,34 +66,30 @@ 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
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:
Expand All @@ -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
88 changes: 88 additions & 0 deletions archiTop/deck_fetcher/moxfield.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit 85edc79

Please sign in to comment.