PK!#xpyartifact/.DS_StoreBud1%typesfdscboolsfdscbool  @ @ @ @ E%DSDB` @ @ @PK!Grrpyartifact/__init__.py__version__ = '0.3.1' __all__ = ['Cards', 'CardFilter', 'Deck', 'decode_deck_string', 'encode_deck'] # Wrappers from .api_sync import Cards from .deck import Deck from .filtering import CardFilter # For users that only want to use the encoding and decoding algorithms from .deck_encoding.decode import decode_deck_string from .deck_encoding.encode import encode_deck PK!tVKKpyartifact/_context.pyfrom typing import Any, Dict, List class Context: all_sets = ['00', '01'] def __init__(self, language: str = 'english') -> None: self.language: str = language self.cards_by_id: Dict[int, Any] = {} self.cards_by_name: Dict[str, Any] = {} self.loaded_sets: List[str] = [] ctx = Context() PK!jpyartifact/api_sync.pyfrom typing import List, Optional from ._context import ctx from .filtering import CardFilter from .sets_and_cards import CardSet, CardTypesInstanced class Cards: """ Synchronous API around Artifact API sets of cards """ def __init__( self, limit_sets: Optional[List[str]] = None, localize: Optional[str] = None, ) -> None: """ :param limit_sets: Whether to only fetch some sets, by default all ar used ('00', and '01') :param localize: Which language to fetch strings for, at this time only english is available """ self._set_numbers = limit_sets or ctx.all_sets self.sets: List[CardSet] = [CardSet(set_number) for set_number in self._set_numbers] if localize is not None: ctx.language = localize def load_all_sets(self) -> None: """ Loads all the sets it should load from the api. """ ctx.cards_by_id = {} ctx.cards_by_name = {} for set_ in self.sets: set_.load() ctx.loaded_sets = self._set_numbers @property def filter(self) -> 'CardFilter': """ Creates a new filter instance with all the cards. :return: """ return CardFilter(sets=self.sets) # noinspection PyMethodMayBeStatic def get(self, name: str) -> 'CardTypesInstanced': """ Gets a card instance by name :param name: Name of the card (case insensitive) """ return ctx.cards_by_name[name.lower()] def find(self, name_approx: str) -> 'CardTypesInstanced': raise NotImplementedError('Card lookup with approx. names is not yet implemented.') PK!2&&pyartifact/deck.py""" Deck wrapper with an overview class. """ from collections import defaultdict from typing import List, Union, Dict from ._context import ctx from .deck_encoding.decode import decode_deck_string from .deck_encoding.encode import encode_deck from .exceptions import PyArtifactException from .filtering import CardFilter from .sets_and_cards import Hero, CardTypesInstanced, Item, Spell, Improvement, Creep from .types.deck_types import HeroDeckType, CardDeckType, DeckContents class Deck: """ Class for holding information about a deck. To use this, all sets must be loaded. As this library isn't supposed to be a fully fledged deck constructor, accessing deck_contents directly is recommended. Compared to the out-of-the-box data encoded in the deck string, this object enriches them with an additional key `instance` that holds an instance of the deck from :py:class:`pyartifact.Cards` As of now, the deck object does no validations, the rules to follow are: * Some cards have `includes`, that are automatically added to the deck and they shouldn't be in the cards portion of deck contents. * Abilities and Passive abilities aren't able to be included in a deck, as they come with another cards and are more of a traits than cards. * Heroes have their own part of deck contents, don't put them into the cards section **Deck code versions** Deck codes currently have two versions. We are able to load both, but only dump to version 2. +---------+--------+-------+---------------+ | Version | Heroes | Cards | Deck name | +=========+========+=======+===============+ | 1 | yes | yes | no | +---------+--------+-------+---------------+ | 2 | yes | yes | 63 characters | +---------+--------+-------+---------------+ When loading version 1, this library will still provide a name, which will be an empty string. """ def __init__(self, deck_contents: DeckContents) -> None: """ :param deck_contents: dict of deck contents """ if sorted(ctx.loaded_sets) != sorted(ctx.all_sets): raise PyArtifactException("To instantiate a Deck, all sets musts be loaded.") self.deck_contents = deck_contents self._enrich() def __repr__(self) -> str: return f'' @classmethod def from_code(cls, deck_code: str) -> 'Deck': """ Constructs the deck object from a deck code string. `Deck.from_code(deck_code)` does the exact same thing as `Deck.loads(deck_code)` :param deck_code: Version 1 or 2 deck code """ return cls(deck_contents=decode_deck_string(deck_code)) loads = from_code @classmethod def new(cls, name: str, heroes: List[HeroDeckType], cards: List[CardDeckType]): """ Constructs the deck object from the insides of deck contents, for when you can't be bothered to make a dict. :param name: Name of the deck :param heroes: List of dictionaries holding information about the heroes :param cards: List of dictionaries holding information about the cards """ deck_contents = DeckContents(name=name, heroes=heroes, cards=cards) return cls(deck_contents) @property def deck_code(self) -> str: """Returns the latest version of the deck code.""" return encode_deck(self.deck_contents) __str__ = deck_code def dumps(self) -> str: """ Returns the latest version of the deck code, same as deck_code property. For people who are used to json/yaml api :). """ return self.deck_code @property def name(self) -> str: """Name of the deck or an empty string of there is no name.""" return self.deck_contents.get('name', '') @name.setter def name(self, new_name: str) -> None: """Sets a new name of the deck""" self.deck_contents['name'] = new_name @property def cards(self) -> List[CardDeckType]: """List of dictionaries with all the cards and their information""" return self.deck_contents['cards'] @property def heroes(self) -> List[HeroDeckType]: """List of dictionaries with all the heroes and their information""" return self.deck_contents['heroes'] @property def overview(self) -> 'Overview': """ Returns an overview of the deck, used for quick glances at it's contents. The overview holds an instance of the deck and will change with any changes made to the deck. """ return Overview(self) def expand_cards(self, hero_includes: bool = True) -> List[CardTypesInstanced]: """ Expands all the cards in the deck into a list of their instances, once for each count. :param hero_includes: Whether also expand auto-includes coming with the heroes """ expanded: List[CardTypesInstanced] = [] for card in self.cards: for _ in range(card['count']): expanded.append(card['instance']) if hero_includes: for hero in self.heroes: expanded += hero['instance'].includes return expanded def _enrich(self) -> None: """Adds instance key to all heroes and cards.""" enriched_heroes: List[HeroDeckType] = [] for hero in self.heroes: hero_instance = ctx.cards_by_id[hero['id']] enriched_heroes.append(HeroDeckType(instance=hero_instance, turn=hero['turn'], id=hero['id'])) enriched_cards: List[CardDeckType] = [] for card in self.cards: card_instance = ctx.cards_by_id[card['id']] enriched_cards.append(CardDeckType(instance=card_instance, count=card['count'], id=card['id'])) enriched_deck = DeckContents(name=self.name, heroes=enriched_heroes, cards=enriched_cards) self.deck_contents = enriched_deck @staticmethod def new_hero_dict(hero: Hero, turn: int) -> HeroDeckType: """ Construction of a hero dict compatible with the encoding process and Deck object internals :param hero: Instance of a hero card :param turn: Which turn the hero will be deployed """ return HeroDeckType(instance=hero, turn=turn, id=hero.id) @staticmethod def new_card_dict(card: CardTypesInstanced, count: int) -> CardDeckType: """ Construction of a card dict compatible with the encoding process and Deck object internals :param card: Instance of a card :param count: How many copies are in the deck """ return CardDeckType(instance=card, count=count, id=card.id) class Overview: """A helper object for quick glances over the deck contents.""" def __init__(self, deck: Deck) -> None: """ :param deck: An instantiated deck object """ self.deck = deck def items(self, sub_type: str = None, return_filter: bool = False) -> Union[List[Item], CardFilter]: """ All the items in the deck. :param sub_type: Whether to list only certain sub type of the items :param return_filter: Whether to return a :py:class:`pyartifact.CardFilter` object or just a List of cards """ items = CardFilter(cards=self.deck.expand_cards(hero_includes=False)).type(Item) if sub_type is not None: items = items.sub_type(sub_type) if not return_filter: return items.cards return items def spells(self, return_filter: bool = False) -> Union[List[Spell], CardFilter]: """ All the spells in the deck. :param return_filter: Whether to return a :py:class:`pyartifact.CardFilter` object or just a List of cards """ spells = CardFilter(cards=self.deck.expand_cards()).type(Spell) if not return_filter: return spells.cards return spells def heroes(self, return_filter: bool = False) -> Union[List[Hero], CardFilter]: """ All the heroes in the deck, sorted by their turns of deployment. :param return_filter: Whether to return a :py:class:`pyartifact.CardFilter` object or just a List of cards """ heroes = [h['instance'] for h in sorted(self.deck.heroes, key=lambda x: x['turn'])] if return_filter: return CardFilter(cards=heroes) return heroes def improvements(self, return_filter: bool = False) -> Union[List[Improvement], CardFilter]: """ All the improvements in the deck. :param return_filter: Whether to return a :py:class:`pyartifact.CardFilter` object or just a List of cards """ improvements = CardFilter(cards=self.deck.expand_cards()).type(Improvement) if not return_filter: return improvements.cards return improvements def creeps(self, return_filter: bool = False) -> Union[List[Creep], CardFilter]: """ All the creeps in the deck. :param return_filter: Whether to return a :py:class:`pyartifact.CardFilter` object or just a List of cards """ creeps = CardFilter(cards=self.deck.expand_cards()).type(Creep) if not return_filter: return creeps.cards return creeps def items_per_subtype(self) -> Dict[str, List[Item]]: """ A more detailed overview of items on the deck in a form of defaultdict(list) where each key is a sub type and it's value is a list of those items. """ result: Dict[str, List[Item]] = defaultdict(list) for item in self.items(): result[item.sub_type].append(item) return result PK!$pyartifact/deck_encoding/__init__.pyPK!MHD"pyartifact/deck_encoding/decode.pyimport re from base64 import b64decode from typing import List, Tuple from ..exceptions import InvalidDeckString, DeckDecodeException from ..types.deck_types import HeroDeckType, CardDeckType, DeckContents # Version 1: Heroes and Cards # Version 2: Name, Heroes and Cards SUPPORTED_VERSIONS = [1, 2] def decode_deck_string(deck_code: str) -> DeckContents: """ Takes in deck code, e.g. `ADCJWkTZX05uwGDCRV4XQGy3QGLmqUBg4GQJgGLGgO7AaABR3JlZW4vQmxhY2sgRXhhbXBsZQ__` and decodes it into a dict of name, heroes and cards. :param deck_code: Deck code :return: Deck contents :raises InvalidDeckString: When an invalid deck string is provided, e.g. unknown version, bad checksum etc. :raises DeckDecodeException: When something odd happens while decoding """ return Decoder(deck_code).deck_contents class Decoder: prefix = 'ADC' def __init__(self, deck_code: str) -> None: """ :param deck_code: Deck code """ self.deck_code = deck_code self._binary = bytearray() @property def deck_contents(self) -> DeckContents: self._decode_string() self._get_version_and_heroes() self._get_lengths_and_calc_checksum() self._parse_deck() return self._deck_contents def _decode_string(self) -> None: if not self.deck_code.startswith(self.prefix): raise InvalidDeckString("The provided deck string doesn't start with a known prefix") # Strip the prefix and turn it into a valid base64 from url-safe string deck_code_b64 = self.deck_code.lstrip(self.prefix).replace('-', '/').replace('_', '=') decoded = b64decode(deck_code_b64) self._binary = bytearray(decoded) # If something funky happened, let the user know that it's terrible if not self._binary: raise InvalidDeckString("No binary data could be decoded from the string") def _parse_deck(self) -> None: # Read the rest of heroes count and get the moved 'cursor' self._heroes_count, self._current_index = self._read_var_encoded(self._version_and_heroes, 3, self._card_bytes_start_index, self._total_card_bytes) # Read the list of heroes heroes: List[HeroDeckType] = [] self._previous_card_base: int = 0 for _ in range(self._heroes_count): hero_turn, hero_card_id = self._read_serialized_card(read_until_index=self._total_card_bytes) heroes.append(HeroDeckType(id=hero_card_id, turn=hero_turn)) # Read the list of cards cards: List[CardDeckType] = [] self._previous_card_base = 0 while self._current_index < self._total_card_bytes: card_count, card_id = self._read_serialized_card(read_until_index=len(self._binary)) cards.append(CardDeckType(id=card_id, count=card_count)) # Read the name if it's present if self._name_length: name = self._binary[-self._name_length:].decode() # WIP simple html sanitizer name = re.sub('<[^<]+?>', '', name) else: name = '' self._deck_contents: DeckContents = DeckContents(name=name, heroes=heroes, cards=cards) def _get_version_and_heroes(self) -> None: """Reads the deck code version and first part of number of heroes from the first byte.""" self._version_and_heroes = self._binary[0] self.version = self._version_and_heroes >> 4 if self.version not in SUPPORTED_VERSIONS: raise InvalidDeckString(f"Deck string has incompatible version '{self.version}', " f"supported versions are: {SUPPORTED_VERSIONS}") def _get_lengths_and_calc_checksum(self) -> None: """ Depending on the deck code version reads where the card information starts, how long is the name of the deck and completes the checksum. """ computed_checksum = 0 if self.version == 1: self._name_length = 0 computed_checksum += self._binary[2] self._card_bytes_start_index = 2 else: self._name_length = self._binary[2] self._card_bytes_start_index = 3 self._total_card_bytes = len(self._binary) - self._name_length computed_checksum = sum(b for b in self._binary[self._card_bytes_start_index:self._total_card_bytes]) if self._binary[1] != (computed_checksum & 0xFF): raise InvalidDeckString("Checksum doesn't check out") def _read_var_encoded(self, base_value: int, base_bits: int, index: int, max_index: int) -> Tuple[int, int]: """ Reads an encoded variable :param base_value: When there was another int the byte at index, what that int was, so we can read the next one :param base_bits: How many bits did the base value have :param index: At which index of the binary data to start :param max_index: At which index must the currently read variable end :return: Tuple of the variable and new position of the index 'cursor' in the binary data """ value = 0 delta_shift = 0 value, unknown_thing = _read_bits_chunk(base_value, base_bits, delta_shift, value) if (base_bits == 0) or unknown_thing: delta_shift += base_bits while True: if index > max_index: raise DeckDecodeException("Couldn't read a variable from the string.") next_byte = self._binary[index] index += 1 value, unknown_thing = _read_bits_chunk(next_byte, 7, delta_shift, value) if not unknown_thing: break delta_shift += 7 return value, index def _read_serialized_card(self, read_until_index: int) -> Tuple[int, int]: """ Reads a serialized card. :param read_until_index: At which index must the card end """ # Check if we aren't already off limits if self._current_index > read_until_index: raise DeckDecodeException("Couldn't read a serialized card") # Header byte of the card contains the count (turn) information if it's lower than 3 # and a first part of card id delta information after that header = self._binary[self._current_index] self._current_index += 1 # Read the card id delta card_id_delta, self._current_index = self._read_var_encoded(header, 5, self._current_index, read_until_index) # Add the previous id to the delta we just decoded card_id = self._previous_card_base + card_id_delta # If the header didn't have the real count information, read it from the next bytes if (header >> 6) == 3: count, self._current_index = self._read_var_encoded(0, 0, self._current_index, read_until_index) else: # If the header had it, shift it and subtract 2 from it count = (header >> 6) + 1 self._previous_card_base = card_id return count, card_id def _read_bits_chunk(chunk: int, numb_bits: int, curr_shift: int, out_bits: int) -> Tuple[int, bool]: """Reads a chunk of bits from a byte.""" continue_bit = 1 << numb_bits new_bits = chunk & (continue_bit - 1) out_bits |= (new_bits << curr_shift) return out_bits, (chunk & continue_bit) != 0 PK!hп"pyartifact/deck_encoding/encode.py""" Logic for encoding deck into a deck code string. Encoding is done by writing a few things into a bytearray thanks to the magic of bitwise operations. That is then encoded to base64 and sanitized for url usage. """ import re from base64 import b64encode from ..exceptions import DeckEncodeException from ..types.deck_types import DeckContents # Version 1: Heroes and Cards # Version 2: Name, Heroes and Cards SUPPORTED_VERSIONS = [2] def encode_deck(deck_contents: DeckContents, version: int = SUPPORTED_VERSIONS[-1]) -> str: """ Encodes deck content into a deck code string. :param deck_contents: A dictionary with name, heroes and cards (without those included automatically) :param version: Deck code version, atm only 2 and higher is supported :return: Deck code """ return Encoder(deck_contents, version=version).deck_code class Encoder: """ Main purpose of this class is to hold shared data across the encoding process. There shouldn't be a need to use this part of the library, It offers a more low level access to the encoding process, but doesn't offer anything more practical than :py:func:`pyartifact.encode_deck` does. """ header_size = 3 prefix = 'ADC' def __init__(self, deck_contents: DeckContents, version: int = SUPPORTED_VERSIONS[-1]) -> None: """ :param deck_contents: The deck contents. :param version: Version under which to encode, by default the newest version is used. Must be on of the supported versions for encoding (atm only V2). """ self.version = version self.heroes = sorted(deck_contents['heroes'], key=lambda x: x['id']) self.cards = sorted(deck_contents['cards'], key=lambda x: x['id']) name = deck_contents.get('name', '')[:63] # name has a hard limit of 63 characters (V2) self.name = re.sub('<[^<]+?>', '', name) self._binary = bytearray() # This where all our hard work will end in, before being interpreted as string @property def deck_code(self) -> str: """Returns the deck code for the deck contents provided.""" self._encode_to_binary() encoded = b64encode(bytes(self._binary)).decode() deck_code = f'{self.prefix}{encoded}' # url safe (no, base64.urlsafe_b64encode wouldn't do the trick, different replacements in this case) deck_code = deck_code.replace('/', '-').replace('=', '_') return deck_code def _encode_to_binary(self) -> bytearray: """Heavy lifting for the encoding.""" # First byte is deck code version and number of heroes version = (self.version << 4) | _extract_n_bits(len(self.heroes), 3) self._binary.append(version) # Put placeholder for checksum as the second byte placeholder_checksum = 0 checksum_index = len(self._binary) self._binary.append(placeholder_checksum) # Length of the name string is the 3rd byte self._binary.append(len(self.name)) # Add remaining number of heroes to the next byte if needed (happens with 8+ heroes) self._add_remaining_bits_from_number(len(self.heroes), 3) # Serialize and append heroes previous_card_id = 0 for hero in self.heroes: self._add_card(hero['turn'], hero['id'] - previous_card_id) previous_card_id = hero['id'] # Serialize and append cards previous_card_id = 0 for card in self.cards: self._add_card(card['count'], card['id'] - previous_card_id) previous_card_id = card['id'] # We'll finish of the binary data with the name, # note down at which index we'll be starting to not include it in the checksum name_start_index = len(self._binary) name_bytes = bytearray(self.name.encode()) self._binary += name_bytes # Compute the checksum and replace the placeholder self._binary[checksum_index] = sum(b for b in self._binary[self.header_size:name_start_index]) & 255 return self._binary def _add_remaining_bits_from_number(self, value: int, already_written_bits: int) -> None: """Adds the remaining bits from the number we extracted some bits from previously.""" value >>= already_written_bits while value > 0: next_byte = _extract_n_bits(value, 7) value >>= 7 self._binary.append(next_byte) def _add_card(self, count_or_turn: int, value: int) -> None: """Adds a card to the bytearray""" # Note down the index we start at bytes_start = len(self._binary) # max count in the first byte first_byte_max_count = 3 # Whether the count (or turn) exceeds the max extended_count = ((count_or_turn - 1) >= first_byte_max_count) # If up to number 3 was provided as first argument, we us that -1, otherwise we use the maximum - 3 first_byte_count = first_byte_max_count if extended_count else (count_or_turn - 1) # This ends up being either 64, 128 or 192 depending on the count first_byte = first_byte_count << 6 # We bitwise or the number we got with the first 5 bits of the value (card id difference) first_byte |= _extract_n_bits(value, 5) # And write the first byte into our array self._binary.append(first_byte) # After the first byte we add the value (difference in card_id from the previous card_id) self._add_remaining_bits_from_number(value, 5) # If we couldn't fit the count (or turn) into the first byte and we used 3, we need to add it here if extended_count: self._add_remaining_bits_from_number(count_or_turn, 0) count_bytes_end = len(self._binary) # If we exceeded 11 bytes, we are doomed, probably api version v3 will be needed by then, no instructions now if count_bytes_end - bytes_start > 11: raise DeckEncodeException("Something went horribly wrong") def _extract_n_bits(value: int, num_bits: int) -> int: """Extracts n bits from a number""" limit_bit = 1 << num_bits result = value & (limit_bit - 1) if value >= limit_bit: result |= limit_bit return result PK!f/|yypyartifact/exceptions.pyclass PyArtifactException(Exception): pass class FilterException(PyArtifactException): pass class UnknownFilterArgument(FilterException): pass class InvalidDeck(PyArtifactException): pass class InvalidDeckString(InvalidDeck): pass class DeckDecodeException(PyArtifactException): pass class DeckEncodeException(PyArtifactException): pass PK!)"c$$pyartifact/filtering.pyfrom typing import Optional, Iterable, Union, List, Tuple from .exceptions import UnknownFilterArgument from .sets_and_cards import CardSet, CardTypes, CardTypesInstanced, AVAILABLE_TYPES, STR_TO_CARD_TYPE def convert_card_type(type_: Union[str, CardTypes]) -> CardTypes: if type_ in AVAILABLE_TYPES: return type_ else: try: return STR_TO_CARD_TYPE[type_] except KeyError: raise UnknownFilterArgument(f'Unrecognized card type: {type_}') def convert_card_types(card_types: Iterable[Union[str, CardTypes]]) -> Tuple[CardTypes, ...]: return tuple([convert_card_type(ct) for ct in card_types]) class CardFilter: """ Class that allows you to use predefined filters to get the cards you are looking for. The cards are then available in the `cards` attribute. All filter methods return a new filter instance. That means you can both nest filtering methods and save certain filter to a variables and access them later if you want to go a different path. """ def __init__( self, sets: Optional[Iterable[CardSet]] = None, cards: Optional[Iterable[CardTypesInstanced]] = None ) -> None: self.cards: List[CardTypesInstanced] = list(cards) if cards else list() if sets: for card_set in sets: for card in card_set.data.card_list: # type: ignore self.cards.append(card) self._filtered: List[CardTypesInstanced] = [] def __len__(self) -> int: return len(self.cards) def __str__(self) -> str: return f'' def __getitem__(self, item: int) -> 'CardTypesInstanced': return list(self.cards)[item] def type(self, type_: Union[str, CardTypes], filter_out=False) -> 'CardFilter': """ Filters for a single type (or anything but a single type) :param type_: Type of the card :param filter_out: Whether to filter that type out """ type_ = convert_card_type(type_) for card in self.cards: if filter_out: if not isinstance(card, type_): self._filtered.append(card) else: if isinstance(card, type_): self._filtered.append(card) return CardFilter(cards=self._filtered) def types_in(self, card_types: Iterable[Union[str, CardTypes]]) -> 'CardFilter': """ Filters out cards that were not passed to this filter :param card_types: Either strings of card types, or this library's classes of card types """ card_types = convert_card_types(card_types) for card in self.cards: if isinstance(card, card_types): self._filtered.append(card) return CardFilter(cards=self._filtered) def types_not_in(self, card_types: Iterable[Union[str, CardTypes]]) -> 'CardFilter': """ Filters out cards that were passed into this filter :param card_types: Either strings of card types, or this library's classes of card types """ card_types = convert_card_types(card_types) for card in self.cards: if not isinstance(card, card_types): self._filtered.append(card) return CardFilter(cards=self._filtered) def rarity(self, rarity: str) -> 'CardFilter': """ Filters for a rarity :param rarity: Rarity """ for card in self.cards: if hasattr(card, 'rarity'): if card.rarity == rarity: self._filtered.append(card) return CardFilter(cards=self._filtered) def rarity_in(self, rarities: List[str]) -> 'CardFilter': """ Filters for multiple rarities :param rarities: Rarities """ for card in self.cards: if hasattr(card, 'rarity'): if card.rarity in rarities: self._filtered.append(card) return CardFilter(cards=self._filtered) def rarity_not_in(self, rarities: List[str]) -> 'CardFilter': """ Filters out cards of specified rarities. :param rarities: Rarities """ for card in self.cards: if hasattr(card, 'rarity'): if card.rarity not in rarities: self._filtered.append(card) return CardFilter(cards=self._filtered) def color(self, color: str) -> 'CardFilter': """ Filters for a single color. :param color: Color """ for card in self.cards: if hasattr(card, 'color'): if card.color == color.lower(): self._filtered.append(card) return CardFilter(cards=self._filtered) def color_in(self, colors: Iterable[str]) -> 'CardFilter': """ Filters for multiple colors. :param colors: Colors """ colors = [color.lower() for color in colors] for card in self.cards: if hasattr(card, 'color'): if card.color in colors: self._filtered.append(card) return CardFilter(cards=self._filtered) def color_not_in(self, colors: Iterable[str]) -> 'CardFilter': """ For filtering out colors :param colors: Colors """ colors = [color.lower() for color in colors] for card in self.cards: if hasattr(card, 'color'): if card.color not in colors: self._filtered.append(card) return CardFilter(cards=self._filtered) def mana_cost( self, gt: Optional[int] = None, gte: Optional[int] = None, lt: Optional[int] = None, lte: Optional[int] = None, eq: Optional[int] = None ) -> 'CardFilter': """ Filters out cards by their mana cost, if they have one. This will always filter out cards without mana cost. If multiple arguments are passed, every card that fits at least one will pass the filter. :param gt: Filters out cards that have higher mana cost than the number provided :param gte: Filters out cards that have higher or equal mana cost than the number provided :param lt: Filters out cards that have lower mana cost than the number provided :param lte: Filters out cards that have lower or equal mana cost than the number provided :param eq: Filters out cards that have mana cost equal to the number provided """ for card in self.cards: if hasattr(card, 'mana_cost'): if gt is not None and card.mana_cost > gt: self._filtered.append(card) elif gte is not None and card.mana_cost >= gte: self._filtered.append(card) elif lt is not None and card.mana_cost < lt: self._filtered.append(card) elif lte is not None and card.mana_cost <= lte: self._filtered.append(card) elif eq is not None and card.mana_cost == eq: self._filtered.append(card) return CardFilter(cards=self._filtered) def gold_cost( self, gt: Optional[int] = None, gte: Optional[int] = None, lt: Optional[int] = None, lte: Optional[int] = None, eq: Optional[int] = None ) -> 'CardFilter': """ Filters out cards by their gold cost, if they have one. This will always filter out cards without gold cost. If multiple arguments are passed, every card that fits at least one will pass the filter. :param gt: Filters out cards that have higher gold cost than the number provided :param gte: Filters out cards that have higher or equal gold cost than the number provided :param lt: Filters out cards that have lower gold cost than the number provided :param lte: Filters out cards that have lower or equal gold cost than the number provided :param eq: Filters out cards that have gold cost equal to the number provided """ for card in self.cards: if hasattr(card, 'gold_cost'): if gt is not None and card.gold_cost > gt: self._filtered.append(card) elif gte is not None and card.gold_cost >= gte: self._filtered.append(card) elif lt is not None and card.gold_cost < lt: self._filtered.append(card) elif lte is not None and card.gold_cost <= lte: self._filtered.append(card) elif eq is not None and card.gold_cost == eq: self._filtered.append(card) return CardFilter(cards=self._filtered) def sub_type(self, sub_type: str) -> 'CardFilter': """ Filters out everything but items and leaves just items with a subtype equal to the provided string :param sub_type: Sub type of an item """ for card in self.cards: if getattr(card, 'sub_type') == sub_type: self._filtered.append(card) return CardFilter(cards=self._filtered) PK!Hx#44pyartifact/sets_and_cards.pyfrom typing import Optional, List, Union, Type, Dict import requests from ._context import ctx from .types.json_types import CardSetType, SetInfoType, SetDataType, ReferenceType class CardBase: """ All cards (and abilities) inherit the base. +--------------+------+-----------------------------------------------------------------------+ | Attribute | Type | Contents | +==============+======+=======================================================================+ | id | int | Id of the card | +--------------+------+-----------------------------------------------------------------------+ | base_id | int | Currently same as id | +--------------+------+-----------------------------------------------------------------------+ | name | str | Name of the card | +--------------+------+-----------------------------------------------------------------------+ | type | str | Type of the card, also indicated by the actual class holding the card | +--------------+------+-----------------------------------------------------------------------+ | text | str | Text on the card, includes html | +--------------+------+-----------------------------------------------------------------------+ | mini_image | str | Url to mini image | +--------------+------+-----------------------------------------------------------------------+ | large_image | str | Url to large image | +--------------+------+-----------------------------------------------------------------------+ | ingame_image | str | Url to ingame image | +--------------+------+-----------------------------------------------------------------------+ """ def __init__(self, **kwargs) -> None: self.id: int = kwargs['card_id'] self.base_id: int = kwargs['base_card_id'] self.name: str = kwargs['card_name'][ctx.language] self.type: str = kwargs['card_type'] self.text: str = kwargs['card_text'].get(ctx.language, '') self.mini_image: str = kwargs['mini_image'].get('default') self.large_image: str = kwargs['large_image'].get('default') self.ingame_image: str = kwargs['ingame_image'].get('default') self._references: List[ReferenceType] = kwargs['references'] def __str__(self) -> str: return self.name def __repr__(self) -> str: return f'' @property def references(self) -> List['CardTypesInstanced']: """List of cards that this card references""" references = [] for ref in self._references: if ref['ref_type'] == 'references': reference = ctx.cards_by_id[ref['card_id']] references.append(reference) return references class ColoredCard: """ Cards that belong under a certain color. +--------------+------+-----------------------------------------------------------------------+ | Attribute | Type | Contents | +==============+======+=======================================================================+ | color | str | blue, black, red, green or unknown. There are no multicolor cards yet | +--------------+------+-----------------------------------------------------------------------+ """ def __init__(self, **kwargs) -> None: if kwargs.get('is_blue', False): self.color: str = 'blue' elif kwargs.get('is_black', False): self.color = 'black' elif kwargs.get('is_red', False): self.color = 'red' elif kwargs.get('is_green', False): self.color = 'green' else: # in case future sets introduce new colors, this way it won't break this library self.color = 'unknown' class Unit: """ Cards that can be deployed to a battlefield and fight +--------------+------+---------------------------------+ | Attribute | Type | Contents | +==============+======+=================================+ | attack | int | Attack of the unit | +--------------+------+---------------------------------+ | armor | int | Armor of the unit | +--------------+------+---------------------------------+ | hit_points | int | Hit points (health) of the unit | +--------------+------+---------------------------------+ """ def __init__(self, **kwargs) -> None: self.attack: int = kwargs.get('attack', 0) self.armor: int = kwargs.get('armor', 0) self.hit_points: int = kwargs.get('hit_points', 0) class NotAbility: """ Cards that are not abilities. Card API provides abilities and passive abilities alongside cards, so in the context of this library they are treated as cards. +--------------+----------------+-----------------------------------------------------------------------+ | Attribute | Type | Contents | +==============+================+=======================================================================+ | rarity | Optional[str] | Rarity of the card, if it has one (base set cards don't have a rarity | +--------------+----------------+-----------------------------------------------------------------------+ | item_def | Optional[int] | Unknown integer, only present when rarity is present | +--------------+----------------+-----------------------------------------------------------------------+ | illustrator | str | Name of the illustrator that drew the card art | +--------------+----------------+-----------------------------------------------------------------------+ """ def __init__(self, **kwargs) -> None: self.rarity: Optional[str] = kwargs.get('rarity') self.item_def: Optional[int] = kwargs.get('item_def') self.illustrator: str = kwargs['illustrator'] @property def active_abilities(self) -> List['Ability']: """List of the cards active abilities""" abilities = [] for ref in self._references: if ref['ref_type'] == 'active_ability': ability = ctx.cards_by_id[ref['card_id']] abilities.append(ability) return abilities class Castable: """ Cards that can be casted for mana. +--------------+------+---------------------------------+ | Attribute | Type | Contents | +==============+======+=================================+ | mana_cost | int | Mana cost to cast the card | +--------------+------+---------------------------------+ """ def __init__(self, **kwargs) -> None: self.mana_cost: int = kwargs['mana_cost'] class Hero(CardBase, ColoredCard, Unit, NotAbility): """Inherts from :py:class:`CardBase`, :py:class:`ColoredCard`, :py:class:`Unit` and :py:class:`NotAbility`.""" def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) Unit.__init__(self, **kwargs) NotAbility.__init__(self, **kwargs) @property def includes(self) -> List[Union['Spell', 'Creep', 'Improvement']]: """List of all the cards this card includes automatically in a deck.""" includes = [] for ref in self._references: if ref['ref_type'] == 'includes': included = ctx.cards_by_id[ref['card_id']] for _ in range(ref['count']): includes.append(included) return includes @property def passive_abilities(self) -> List['PassiveAbility']: """List of the cards passive abilities""" passive_abilities = [] for ref in self._references: if ref['ref_type'] == 'passive_ability': ability = ctx.cards_by_id[ref['card_id']] passive_abilities.append(ability) return passive_abilities class PassiveAbility(CardBase): """Inherits from :py:class:`CardBase`.""" def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) class Spell(CardBase, ColoredCard, NotAbility, Castable): """Inherits from :py:class:`CardBase`, :py:class:`ColoredCard`, :py:class:`NotAbility` and :py:class:`Castable`.""" def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) NotAbility.__init__(self, **kwargs) Castable.__init__(self, **kwargs) class Creep(CardBase, ColoredCard, Unit, NotAbility, Castable): """ Inherits from :py:class:`CardBase`, :py:class:`ColoredCard`, :py:class:`Unit`, :py:class:`NotAbility`, :py:class:`Castable`. """ def __init__(self, **kwargs): CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) Unit.__init__(self, **kwargs) NotAbility.__init__(self, **kwargs) Castable.__init__(self, **kwargs) class Ability(CardBase): """Inherits from :py:class:`CardBase`.""" def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) class Item(CardBase, NotAbility): """ Inherits from :py:class:`CardBase`, :py:class:`NotAbility`. Also has two attributes unique to this type. +--------------+--------+-----------------------------------------------------------------------+ | Attribute | Type | Contents | +==============+========+=======================================================================+ | gold_cost | int | How much gold does it take to purchase from the shop. | +--------------+--------+-----------------------------------------------------------------------+ | sub_type | str | Subtype of the item - Weapon, Accessory, Armor, Consumable or Deed | +--------------+--------+-----------------------------------------------------------------------+ """ def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) NotAbility.__init__(self, **kwargs) self.gold_cost: int = kwargs['gold_cost'] self.sub_type: str = kwargs['sub_type'] class Improvement(CardBase, ColoredCard, NotAbility, Castable): """Inherits from CardBase, ColoredCard, NotAbility, Castable.""" def __init__(self, **kwargs) -> None: CardBase.__init__(self, **kwargs) ColoredCard.__init__(self, **kwargs) NotAbility.__init__(self, **kwargs) Castable.__init__(self, **kwargs) class SetInfo: """Information about the set - id, name and pack_item_def""" def __init__(self, set_info: SetInfoType) -> None: self.id: int = set_info['set_id'] self.pack_item_def: int = set_info['pack_item_def'] self.name: str = set_info['name'][ctx.language] CardTypesInstanced = Union[Item, Hero, Ability, PassiveAbility, Improvement, Creep, Spell] CardTypes = Union[ Type[Item], Type[Hero], Type[Ability], Type[PassiveAbility], Type[Improvement], Type[Creep], Type[Spell] ] STR_TO_CARD_TYPE: Dict[str, CardTypes] = { 'Hero': Hero, 'Passive Ability': PassiveAbility, 'Spell': Spell, 'Creep': Creep, 'Ability': Ability, 'Item': Item, 'Improvement': Improvement } AVAILABLE_TYPES = (Item, Hero, Ability, PassiveAbility, Improvement, Creep, Spell) class CardSetData: """Cards and Set Data.""" # Stronghold and Pathing are core game mechanics, there's no need to be indexing them not_indexed = ['Stronghold', 'Pathing'] def __init__(self, data: CardSetType) -> None: self.version: int = data['version'] self.set_info = SetInfo(data['set_info']) self.card_list: List[CardTypesInstanced] = [] for card in data['card_list']: if card['card_type'] not in self.not_indexed: type_of_card = STR_TO_CARD_TYPE[card['card_type']] # type: CardTypes card_instance = type_of_card(**card) self.card_list.append(card_instance) # For fast lookups ctx.cards_by_id[card['base_card_id']] = card_instance ctx.cards_by_name[card['card_name'][ctx.language].lower()] = card_instance class CardSet: """Card set.""" base_url = 'https://playartifact.com/cardset/' def __init__(self, set_number: str) -> None: self.url = f'{self.base_url}{set_number}' self.expire_time = None self.data: Optional[CardSetData] = None def load(self) -> None: """Loads the cards set data""" cdn_info = requests.get(self.url).json() self.expire_time = cdn_info['expire_time'] data: SetDataType = requests.get(f"{cdn_info['cdn_root']}{cdn_info['url']}").json() self.data = CardSetData(data['card_set']) PK!pyartifact/types/.DS_StoreBud1_encod deck_encodingfdscbool  @ @ @ @ EDSDB ` @ @ @PK!pyartifact/types/__init__.pyPK!~44pyartifact/types/deck_types.pyfrom typing import List from mypy_extensions import TypedDict from ..sets_and_cards import Hero, CardTypesInstanced class HeroDecodedType(TypedDict): id: int turn: int class HeroDeckType(HeroDecodedType, total=False): instance: Hero class CardDecodedType(TypedDict): id: int count: int class CardDeckType(CardDecodedType, total=False): instance: CardTypesInstanced class DeckContentsBase(TypedDict): cards: List[CardDeckType] heroes: List[HeroDeckType] class DeckContents(DeckContentsBase, total=False): name: str PK!t))pyartifact/types/json_types.pyfrom typing import List, Dict from mypy_extensions import TypedDict # Key is language (atm only english), value is text, which includes html LocalizedText = Dict[str, str] # Key is so far only default, value is the url ImageUrl = Dict[str, str] class ReferenceTypeBase(TypedDict): card_id: int ref_type: str # includes, passive_ability, active_ability, references class ReferenceType(ReferenceTypeBase, total=False): count: int class SetInfoType(TypedDict): set_id: int pack_item_def: int name: LocalizedText class CardListDataType(TypedDict, total=False): card_id: int base_card_id: int card_type: str # Hero, Passive Ability, Spell, Creep, Ability, Item, Improvement sub_type: str gold_cost: int mana_cost: int card_name: LocalizedText card_text: LocalizedText mini_image: ImageUrl large_image: ImageUrl ingame_image: ImageUrl illustrator: str rarity: str # Common, Uncommon, Rare is_blue: bool is_red: bool is_black: bool is_green: bool item_def: int attack: int armor: int hit_points: int references: List[ReferenceType] class CardSetType(TypedDict): version: int set_info: SetInfoType card_list: List[CardListDataType] class SetDataType(TypedDict): card_set: CardSetType PK!%/88"pyartifact-0.3.1.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2018 David Jetelina Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!HW"TT pyartifact-0.3.1.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!H;0 #pyartifact-0.3.1.dist-info/METADATAVms8_k?3  $$Kri/m^:Lb-$қ[uig.2x_>+0,fo@i.!Ҏe0$Œ),2Σ֧/,cj9$KʜGdXQ"L2 d @&Ή̠]Czy 7iHfב^me=_^9S!9b<&g`@mC[ǕFwvuHƣZ=;i0*ɟdRJ>@l1ZBD,yBYx6k_gk3$ٲXA3MO{:i~6avA`; 1ڇ2p},{HX.ryLY7q~-@~dhm9=3CT0/XL*pR)BYn`4lgԒ6,PϹvw5xPr\ OSV ,3!yOF~}w aZ**U=ZŲm+dqt^EP,oӹ?dY?QaaoؤHWhz+J{8 -lAh jkedkqMK.HW?:P h1>  ݓ<iAV%b} yECg,u,(gӘ`p U;^uv eۅNvXw }< ^@::awnv`o,aggPCضG,e(Y:Mmg H<;TV !wn 0H2Y$z`-(5#2'/c&~kˣkTjǰČz.5W\Zj62zulԹXuCRJ_TCZs9D*"ejeK&g;{,aQ0@e]}G gGkfcrtxi.d u~o;^ߟbNRLwElUOc`*JbG?m;H]-ۮRva|im ojO4FrWݪu-s?U"mgǾ hiVf2JULm" EhRtU̱!8+[Y4*,[FXI M\W RE`h_YUPK!Hst[ !pyartifact-0.3.1.dist-info/RECORDӹF|PXE,I (H<=gƌYU[ݿYn((nu"}a$ :]sB£%.F іP`5caXToSBvFYOtiA0)jaҲ7t ߴt6Ztq )ųչE[8+dV|OCJcPV"*hZÍEeU yzg\q{QLdžz*#oǵVƲLyaRaZ%5,ۿeHТd7wFhz3ؑv