diff --git a/README.md b/README.md index d11c4ba..736cbfc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,29 @@ Tools and utilities for TF2 trading. Use 3rd party inventory providers, get SKUs - BTC: `bc1qntlxs7v76j0zpgkwm62f6z0spsvyezhcmsp0z2` - [Steam Trade Offer](https://steamcommunity.com/tradeoffer/new/?partner=293059984&token=0-l_idZR) +## Features +* Get SKUs directly from inventories/offers +* Fetch inventories using 3rd party providers (avoid being rate-limited) +* Listen for Backpack.TF websocket events +* Listen for Prices.TF websocket events + +## Setup +### Install +```bash +pip install tf2-utils +# or +python -m pip install tf2-utils +``` + +### Upgrade +```bash +pip install --upgrade tf2-utils +# or +python -m pip install --upgrade tf2-utils +``` + ## Usage +See [examples](/examples/) or [tests](/tests/) for more usage examples. ### Inventory fetching ```python @@ -90,18 +112,8 @@ socket = PricesTFSocket(my_function) socket.listen() ``` -## Setup -### Install +## Testing ```bash -pip install tf2-utils -# or -python -m pip install tf2-utils -``` - -### Upgrade -```bash -pip install upgrade tf2-utils -# or -python -m pip install upgrade tf2-utils -``` - +# tf2-utils/ +python -m unittest +``` \ No newline at end of file diff --git a/src/tf2_utils/__init__.py b/src/tf2_utils/__init__.py index f0b5994..67faec1 100644 --- a/src/tf2_utils/__init__.py +++ b/src/tf2_utils/__init__.py @@ -1,11 +1,13 @@ __title__ = "tf2-utils" __author__ = "offish" -__version__ = "2.0.1" +__version__ = "2.0.2" __license__ = "MIT" from .schema import Schema, IEconItems from .inventory import Inventory, map_inventory from .sku import get_sku, get_sku_properties -from .utils import to_refined, to_scrap, refinedify +from .utils import to_refined, to_scrap, refinedify, account_id_to_steam_id from .sockets import BackpackTFSocket, PricesTFSocket from .prices_tf import PricesTF +from .item import Item +from .offer import Offer diff --git a/src/tf2_utils/item.py b/src/tf2_utils/item.py new file mode 100644 index 0000000..6bf5bbf --- /dev/null +++ b/src/tf2_utils/item.py @@ -0,0 +1,235 @@ +# qualities are static enough to have it here as a constant +# effects are not, use effects.json file instead (valve adds new effects yearly) +QUALITIES = { + "Normal": 0, + "Genuine": 1, + "rarity2": 2, + "Vintage": 3, + "rarity3": 4, + "Unusual": 5, + "Unique": 6, + "Community": 7, + "Valve": 8, + "Self-Made": 9, + "Customized": 10, + "Strange": 11, + "Completed": 12, + "Haunted": 13, + "Collector's": 14, + "Decorated Weapon": 15, +} + +KILLSTREAKS = { + "Basic": 1, + "Specialized": 2, + "Professional": 3, +} + +EXTERIORS = { + "Factory New": 1, + "Minimal Wear": 2, + "Field-Tested": 3, + "Well-Worn": 4, + "Battle Scarred": 5, +} + + +class Item: + def __init__(self, item: dict) -> None: + self.item = item + self.name = item["market_hash_name"] + self.descriptions = item.get("descriptions", []) + self.tags = item.get("tags", []) + + def is_tf2(self) -> bool: + return self.item["appid"] == 440 + + def has_name(self, name: str) -> bool: + return self.name == name + + def has_description(self, description: str) -> bool: + for i in self.descriptions: + if i["value"] == description: + return True + + return False + + def has_tag(self, tag: str, exact: bool = True) -> bool: + for i in self.tags: + item_tag = i["localized_tag_name"] + + if (item_tag == tag) or (tag in item_tag.lower() and not exact): + return True + + return False + + def has_quality(self, quality: str) -> bool: + return self.get_quality() == quality + + def has_strange_in_name(self) -> bool: + return "Strange" in self.name + + def has_vintage_in_name(self) -> bool: + return "Vintage" in self.name + + def has_killstreak(self, killstreak: str) -> bool: + return self.get_killstreak() == killstreak + + def get_killstreak(self) -> str: + if not self.is_killstreak(): + return "" + + parts = self.name.split(" ") + killstreak_index = parts.index("Killstreak") + killstreak = parts[killstreak_index - 1] + + if killstreak not in ["Specialized", "Professional"]: + killstreak = "Basic" + + return killstreak + + def get_quality(self) -> str: + for tag in self.tags: + if tag["localized_category_name"] != "Quality": + continue + + return tag["localized_tag_name"] + + return "" # could not find + + def get_quality_id(self) -> int: + return QUALITIES[self.get_quality()] + + def get_defindex(self) -> int: + for action in self.item["actions"]: + if action["name"] != "Item Wiki Page...": + continue + + wiki_link = action["link"] + start = wiki_link.index("id=") + end = wiki_link.index("lang=") + # defindex = re.findall("\\d+", wiki_link[start:end])[0] + + # extract defindex from wiki link + defindex = wiki_link[start + 3 : end - 1] + return int(defindex) + + return -1 # could not find + + def get_effect(self) -> str: + if not self.is_unusual(): + return "" + + string = "★ Unusual Effect: " + + for i in self.descriptions: + if string in i["value"]: + return i["value"].replace(string, "") + + return "" # could not find + + def get_killstreak_id(self) -> int: + if not self.is_killstreak(): + return -1 + + return KILLSTREAKS[self.get_killstreak()] + + def get_exterior(self) -> str: + for tag in self.tags: + if tag["category"] != "Exterior": + continue + + return tag["localized_tag_name"] + + return "" # could not find + + def get_exterior_id(self) -> int: + exterior = self.get_exterior() + + if not exterior: + return -1 + + return EXTERIORS[exterior] + + def is_genuine(self) -> bool: + return self.has_quality("Genuine") + + def is_vintage(self) -> bool: + return self.has_quality("Vintage") + + def is_unusual(self) -> bool: + return self.has_quality("Unusual") + + def is_unique(self) -> bool: + return self.has_quality("Unique") + + def is_strange(self) -> bool: + return self.has_quality("Strange") + + def is_haunted(self) -> bool: + return self.has_quality("Haunted") + + def is_collectors(self) -> bool: + return self.has_quality("Collector's") + + def is_decorated_weapon(self) -> bool: + return self.has_tag("Decorated Weapon") + + def is_craftable(self) -> bool: + return not self.has_description("( Not Usable in Crafting )") + + def is_uncraftable(self) -> bool: + return not self.is_craftable() + + def is_non_craftable(self) -> bool: + return self.is_uncraftable() + + def is_festivized(self) -> bool: + return "Festivized" in self.name + + def is_halloween(self) -> bool: + return self.has_description("Holiday Restriction: Halloween / Full Moon") + + def is_craft_weapon(self) -> bool: + return ( + self.is_unique() and self.is_craftable() and self.has_tag("weapon", False) + ) + + def is_cosmetic(self) -> bool: + return self.has_tag("Cosmetic") + + def is_craft_hat(self) -> bool: + return ( + self.is_unique() + and self.is_craftable() + and self.is_cosmetic() + and not self.is_halloween() + ) + + def is_unusual_cosmetic(self) -> bool: + return self.is_unusual() and self.is_cosmetic() + + def is_australium(self) -> bool: + return "Australium" in self.name + + def is_key(self) -> bool: + return ( + self.is_craftable() + and self.is_unique() + and self.has_name("Mann Co. Supply Crate Key") + ) + + def is_mann_co_key(self) -> bool: + return self.is_key() + + def is_killstreak(self) -> bool: + return "Killstreak" in self.name + + def is_basic_killstreak(self) -> bool: + return self.has_killstreak("Basic") + + def is_specialized_killstreak(self) -> bool: + return self.has_killstreak("Specialized") + + def is_professional_killstreak(self) -> bool: + return self.has_killstreak("Professional") diff --git a/src/tf2_utils/items.py b/src/tf2_utils/items.py deleted file mode 100644 index ae8a26f..0000000 --- a/src/tf2_utils/items.py +++ /dev/null @@ -1,98 +0,0 @@ -import re - - -class Item: - def __init__(self, item: dict) -> None: - self.item = item - self.name = item["market_hash_name"] - self.descriptions = item.get("descriptions") - - def is_tf2(self) -> bool: - return self.item["appid"] == 440 - - def has_name(self, name: str) -> bool: - return self.name == name - - def has_description(self, description: str) -> bool: - if self.descriptions is None: - return False - - for i in self.descriptions: - if i["value"] == description: - return True - return False - - def has_tag(self, tag: str, exact: bool = True) -> bool: - for i in self.item["tags"]: - item_tag = i["localized_tag_name"] - if (item_tag == tag and exact) or (tag in item_tag.lower() and not exact): - return True - return False - - def has_quality(self, quality: str) -> bool: - return self.get_quality() == quality - - def get_quality(self) -> str: - for tag in self.item["tags"]: - if tag["localized_category_name"] != "Quality": - continue - return tag["localized_tag_name"] - return "" # could not find - - def get_defindex(self) -> int: - for action in self.item["actions"]: - if action["name"] == "Item Wiki Page...": - link = action["link"] - defindex = re.findall("\\d+", link)[0] - return int(defindex) - return -1 # could not find - - def get_effect(self) -> str: - if not self.is_unusual(): - return "" - - string = "★ Unusual Effect: " - - if self.descriptions is None: - return "" # could not find - - for i in self.descriptions: - if string in i["value"]: - return i["value"].replace(string, "") - return "" # could not find - - def is_unique(self) -> bool: - return self.has_quality("Unique") - - def is_unusual(self) -> bool: - return self.has_quality("Unusual") - - def is_craftable(self) -> bool: - return not self.has_description("( Not Usable in Crafting )") - - def is_halloween(self) -> bool: - return self.has_description("Holiday Restriction: Halloween / Full Moon") - - def is_craft_weapon(self) -> bool: - return ( - self.is_unique() and self.is_craftable() and self.has_tag("weapon", False) - ) - - def is_craft_hat(self) -> bool: - return self.is_unique() and self.is_craftable() and self.has_tag("Cosmetic") - - def is_unusual_cosmetic(self) -> bool: - return self.is_unusual() and self.has_tag("Cosmetic") - - def is_australium(self) -> bool: - return "Australium" in self.name - - def is_strange(self) -> bool: - return "Strange" in self.name - - def is_key(self) -> bool: - return ( - self.is_craftable() - and self.is_unique() - and self.has_name("Mann Co. Supply Crate Key") - ) diff --git a/src/tf2_utils/offer.py b/src/tf2_utils/offer.py new file mode 100644 index 0000000..a6c71d5 --- /dev/null +++ b/src/tf2_utils/offer.py @@ -0,0 +1,63 @@ +from .utils import account_id_to_steam_id + +import enum + + +class TradeOfferState(enum.IntEnum): + Invalid = 1 + Active = 2 + Accepted = 3 + Countered = 4 + Expired = 5 + Canceled = 6 + Declined = 7 + InvalidItems = 8 + ConfirmationNeed = 9 + CanceledBySecondaryFactor = 10 + StateInEscrow = 11 + + +class Offer: + def __init__(self, offer: dict) -> None: + self.offer = offer + self.state = offer["trade_offer_state"] + + def get_state(self) -> str: + return TradeOfferState(self.state).name + + def has_state(self, state: int) -> bool: + return self.state == state + + def is_active(self) -> bool: + return self.has_state(2) + + def is_accepted(self) -> bool: + return self.has_state(3) + + def is_declined(self) -> bool: + return self.has_state(7) + + def has_trade_hold(self) -> bool: + return self.offer["escrow_end_date"] != 0 + + def is_our_offer(self) -> bool: + return self.offer["is_our_offer"] + + def is_gift(self) -> bool: + return self.offer.get("items_to_receive") and not self.offer.get( + "items_to_give" + ) + + def is_scam(self) -> bool: + return self.offer.get("items_to_give") and not self.offer.get( + "items_to_receive" + ) + + def is_two_sided(self) -> bool: + if self.offer.get("items_to_receive") and self.offer.get("items_to_give"): + return True + + return False + + def get_partner(self) -> str: + return account_id_to_steam_id(self.offer["accountid_other"]) diff --git a/src/tf2_utils/prices_tf.py b/src/tf2_utils/prices_tf.py index f627e38..42e06a2 100644 --- a/src/tf2_utils/prices_tf.py +++ b/src/tf2_utils/prices_tf.py @@ -19,7 +19,7 @@ class EmptyResponse(PricesTFError): class PricesTF: - URI = "https://api2.prices.tf" + URL = "https://api2.prices.tf" def __init__(self) -> None: self.access_token = "" @@ -41,7 +41,7 @@ def validate_response(response) -> None: raise RateLimited("currently ratelimited") def _get(self, endpoint: str, params: dict = {}) -> dict: - response = requests.get(self.URI + endpoint, headers=self.header, params=params) + response = requests.get(self.URL + endpoint, headers=self.header, params=params) res = response.json() @@ -49,7 +49,7 @@ def _get(self, endpoint: str, params: dict = {}) -> dict: return res def _post(self, endpoint: str) -> tuple[dict, int]: - response = requests.post(self.URI + endpoint, headers=self.header) + response = requests.post(self.URL + endpoint, headers=self.header) res = response.json() diff --git a/src/tf2_utils/schema.py b/src/tf2_utils/schema.py index fd72ce7..90042f6 100644 --- a/src/tf2_utils/schema.py +++ b/src/tf2_utils/schema.py @@ -6,17 +6,17 @@ import requests -def _get_json_path(name: str) -> str: +def get_json_path(name: str) -> str: return os.path.abspath(__file__).replace("schema.py", "") + f"/json/{name}.json" -def _use_local_json(name: str) -> dict | list: - return read_json_file(_get_json_path(name)) +def use_local_json(name: str) -> dict | list: + return read_json_file(get_json_path(name)) -SCHEMA_PATH = _get_json_path("schema") -ITEM_QUALITIES = _use_local_json("qualities") -EFFECTS = _use_local_json("effects") +SCHEMA_PATH = get_json_path("schema") +ITEM_QUALITIES = use_local_json("qualities") +EFFECTS = use_local_json("effects") class Schema: @@ -33,7 +33,7 @@ def __init__(self, schema: dict | str = {}, api_key: str = "") -> None: self.schema = schema def set_effects(self) -> dict: - path = _get_json_path("effects") + path = get_json_path("effects") effects = self.schema["result"]["attribute_controlled_attached_particles"] data = {} @@ -50,7 +50,7 @@ def set_effects(self) -> dict: return data def set_qualities(self) -> dict: - path = _get_json_path("qualities") + path = get_json_path("qualities") qualtiy_ids = self.schema["result"]["qualities"] qualtiy_names = self.schema["result"]["qualityNames"] @@ -69,13 +69,12 @@ def set_qualities(self) -> dict: class IEconItems: - PLAYER_ITEMS = "http://api.steampowered.com/IEconItems_440/GetPlayerItems/v0001" - SCHEMA_ITEMS = "https://api.steampowered.com/IEconItems_440/GetSchemaItems/v1" - SCHEMA_OVERVIEW = ( - "https://api.steampowered.com/IEconItems_440/GetSchemaOverview/v0001" - ) - SCHEMA_URL = "http://api.steampowered.com/IEconItems_440/GetSchemaURL/v1" - STORE_DATA = "http://api.steampowered.com/IEconItems_440/GetStoreMetaData/v1" + API_URL = "https://api.steampowered.com/IEconItems_440/" + SCHEMA_OVERVIEW = API_URL + "GetSchemaOverview/v0001" + PLAYER_ITEMS = API_URL + "GetPlayerItems/v0001" + SCHEMA_ITEMS = API_URL + "GetSchemaItems/v1" + STORE_DATA = API_URL + "GetStoreMetaData/v1" + SCHEMA_URL = API_URL + "GetSchemaURL/v1" def __init__(self, key: str) -> None: self.key = key diff --git a/src/tf2_utils/sku.py b/src/tf2_utils/sku.py index 6587fde..1db0d5c 100644 --- a/src/tf2_utils/sku.py +++ b/src/tf2_utils/sku.py @@ -1,6 +1,6 @@ -from .items import Item +from .item import Item -from .schema import ITEM_QUALITIES, EFFECTS +from .schema import EFFECTS from tf2_sku import to_sku @@ -8,14 +8,32 @@ def get_sku_properties(item_description: dict) -> dict: item = Item(item_description) - quality = ITEM_QUALITIES[item.get_quality()] + quality = item.get_quality_id() effect = item.get_effect() + # TODO: add rest sku_properties = { "defindex": item.get_defindex(), "quality": quality, - "craftable": item.is_craftable(), "australium": item.is_australium(), + "craftable": item.is_craftable(), + "wear": item.get_exterior_id(), + "killstreak_tier": item.get_killstreak_id(), + "festivized": item.is_festivized(), + # + # "effect": "u{}", + # "australium": "australium", + # "craftable": "uncraftable", + # "wear": "w{}", + # "skin": "pk{}", + # "strange": "strange", + # "killstreak_tier": "kt-{}", + # "target_defindex": "td-{}", + # "festivized": "festive", + # "craft_number": "n{}", + # "crate_number": "c{}", + # "output_defindex": "od-{}", + # "output_quality": "oq-{}", } if effect: @@ -23,32 +41,10 @@ def get_sku_properties(item_description: dict) -> dict: # e.g. strange unusual if quality != 11: - sku_properties["strange"] = item.is_strange() - - # TODO: add rest + sku_properties["strange"] = item.has_strange_in_name() return sku_properties - # to_sku( - # { - # "defindex": 199, - # "quality": 5, - # "effect": 702, - # "australium": False, - # "craftable": True, - # "wear": 3, - # "skin": 292, - # "strange": True, - # "killstreak_tier": 3, - # "target_defindex": -1, - # "festivized": False, - # "craft_number": -1, - # "crate_number": -1, - # "output_defindex": -1, - # "output_quality": -1, - # } - # ) - def get_sku(item: dict) -> str: properties = get_sku_properties(item) diff --git a/src/tf2_utils/sockets.py b/src/tf2_utils/sockets.py index db72688..6cfcfa6 100644 --- a/src/tf2_utils/sockets.py +++ b/src/tf2_utils/sockets.py @@ -71,7 +71,6 @@ def __init__( self.settings = settings def process_message(self, message: str) -> None: - print(type(message)) data = json.loads(message) self.callback(data) diff --git a/src/tf2_utils/utils.py b/src/tf2_utils/utils.py index 4dba319..b6dc22e 100644 --- a/src/tf2_utils/utils.py +++ b/src/tf2_utils/utils.py @@ -21,3 +21,7 @@ def to_refined(scrap: int) -> float: def refinedify(value: float) -> float: return math.floor((round(value * 9, 0) * 100) / 9) / 100 + + +def account_id_to_steam_id(account_id: int | str) -> str: + return str(76561197960265728 + int(account_id)) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/json/crusaders_crossbow.json b/tests/json/crusaders_crossbow.json new file mode 100644 index 0000000..6ba3c44 --- /dev/null +++ b/tests/json/crusaders_crossbow.json @@ -0,0 +1,114 @@ +{ + "appid": 440, + "contextid": "2", + "assetid": "13448992539", + "classid": "3085792783", + "instanceid": "45707670", + "amount": "1", + "currency": 0, + "background_color": "3C352E", + "icon_url": "fWFc82js0fmoRAP-qOIPu5THSWqfSmTELLqcUywGkijVjZULUrsm1j-9xgEMaQkUTxr2vTx8mMnvA-aHAfQ_ktk664Ma2glogxRoNeewIiRYcRbNErNcU-IF8g3_HS4k7Yk0BoPn8bhWLwzo5YWUYuQkNopKHMnVU_KEZgn1vh0xiKdUK8CB8nu8w223bRJDdgVU", + "icon_url_large": "fWFc82js0fmoRAP-qOIPu5THSWqfSmTELLqcUywGkijVjZULUrsm1j-9xgEMaQkUTxr2vTx8mMnvA-aHAfQ_ktk664Ma2glogxRoNeewIiRYcRbNErNcU-IF8g3_HS4k7Yk0BoPn8bhWLwzo5YWUYuQkNopKHMnVU_KEZgn1vh0xiKdUK8CB8nu8w223bRJDdgVU", + "descriptions": [ + { + "value": "(Allied Healing Done: 0)", + "color": "756b5e" + }, + { + "value": "Festivized", + "color": "ffd700" + }, + { + "value": "Killstreaker: Tornado", + "color": "7ea9d1" + }, + { + "value": "Sheen: Team Shine", + "color": "7ea9d1" + }, + { + "value": "Killstreaks Active", + "color": "7ea9d1" + }, + { + "value": "No headshots", + "color": "d83636" + }, + { + "value": "-75% max primary ammo on wearer", + "color": "d83636" + }, + { + "value": "Fires special bolts that heal teammates and deals damage\nbased on distance traveled\nThis weapon will reload automatically when not active" + }, + { + "value": " " + }, + { + "value": "The Medieval Medic", + "color": "e1e10f" + }, + { + "value": " " + }, + { + "value": "The Amputator", + "color": "8b8989" + }, + { + "value": "Crusader's Crossbow", + "color": "8b8989" + }, + { + "value": "Berliner's Bucket Helm", + "color": "8b8989" + } + ], + "tradable": 1, + "actions": [ + { + "link": "http://wiki.teamfortress.com/scripts/itemredirect.php?id=305&lang=en_US", + "name": "Item Wiki Page..." + }, + { + "link": "steam://rungame/440/76561202255233023/+tf_econ_item_preview%20S%owner_steamid%A%assetid%D16774368091684022893", + "name": "Inspect in Game..." + } + ], + "name": "Strange Festivized Professional Killstreak Crusader's Crossbow", + "name_color": "CF6A32", + "type": "Strange Crossbow - Kills: 0", + "market_name": "Strange Festivized Professional Killstreak Crusader's Crossbow", + "market_hash_name": "Strange Festivized Professional Killstreak Crusader's Crossbow", + "market_actions": [ + { + "link": "steam://rungame/440/76561202255233023/+tf_econ_item_preview%20M%listingid%A%assetid%D16774368091684022893", + "name": "Inspect in Game..." + } + ], + "commodity": 0, + "market_tradable_restriction": 7, + "market_marketable_restriction": 0, + "marketable": 1, + "tags": [ + { + "category": "Quality", + "internal_name": "strange", + "localized_category_name": "Quality", + "localized_tag_name": "Strange", + "color": "CF6A32" + }, + { + "category": "Type", + "internal_name": "primary", + "localized_category_name": "Type", + "localized_tag_name": "Primary weapon" + }, + { + "category": "Class", + "internal_name": "Medic", + "localized_category_name": "Class", + "localized_tag_name": "Medic" + } + ] +} \ No newline at end of file diff --git a/tests/json/ellis_cap.json b/tests/json/ellis_cap.json new file mode 100644 index 0000000..d52acca --- /dev/null +++ b/tests/json/ellis_cap.json @@ -0,0 +1,110 @@ +{ + "appid": 440, + "classid": "313", + "instanceid": "19173012", + "currency": 0, + "background_color": "3C352E", + "icon_url": "IzMF03bi9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdEH9myp0erksICfSIfPcdFZlnqWSMU5OD2IgKw3YInShXOjLx2Sk5MbUqMcbBnQz4ruyeU3X7ZAjBIy3QD2FkHPEJYHaN-GCg4LvCEGzNSOsrFlsGeKICozZObsmMOEY01dYN-2Trkxd-SEJxPNVIdz4ygn2l", + "icon_url_large": "IzMF03bi9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdEH9myp0erksICfSIfPcdFZlnqWSMU5OD2IgKw3YInShXOjLx2Sk5MbUqMcbBnQz4ruyeU3X7ZAjBIy3QD2FkHPEJYHaN-GCg4LvCEGzNSOsrFlsGeKICozZObsmMOEY01dYN-2Trkxd-SEJxPNVIdz4ygn2l", + "descriptions": [ + { + "value": "Paint Color: Muskelmannbraun", + "color": "756b5e" + } + ], + "tradable": 1, + "actions": [ + { + "link": "http://wiki.teamfortress.com/scripts/itemredirect.php?id=263&lang=en_US", + "name": "Item Wiki Page..." + }, + { + "link": "steam://rungame/440/76561202255233023/+tf_econ_item_preview%20S%owner_steamid%A%assetid%D13855333614522486257", + "name": "Inspect in Game..." + } + ], + "name": "Ellis' Cap", + "name_color": "7D6D00", + "type": "Level 10 Hat", + "market_name": "Ellis' Cap", + "market_hash_name": "Ellis' Cap", + "market_actions": [ + { + "link": "steam://rungame/440/76561202255233023/+tf_econ_item_preview%20M%listingid%A%assetid%D13855333614522486257", + "name": "Inspect in Game..." + } + ], + "commodity": 0, + "market_tradable_restriction": 7, + "market_marketable_restriction": 0, + "marketable": 0, + "tags": [ + { + "category": "Quality", + "internal_name": "Unique", + "localized_category_name": "Quality", + "localized_tag_name": "Unique", + "color": "7D6D00" + }, + { + "category": "Type", + "internal_name": "misc", + "localized_category_name": "Type", + "localized_tag_name": "Cosmetic" + }, + { + "category": "Class", + "internal_name": "Scout", + "localized_category_name": "Class", + "localized_tag_name": "Scout" + }, + { + "category": "Class", + "internal_name": "Sniper", + "localized_category_name": "Class", + "localized_tag_name": "Sniper" + }, + { + "category": "Class", + "internal_name": "Soldier", + "localized_category_name": "Class", + "localized_tag_name": "Soldier" + }, + { + "category": "Class", + "internal_name": "Demoman", + "localized_category_name": "Class", + "localized_tag_name": "Demoman" + }, + { + "category": "Class", + "internal_name": "Medic", + "localized_category_name": "Class", + "localized_tag_name": "Medic" + }, + { + "category": "Class", + "internal_name": "Heavy", + "localized_category_name": "Class", + "localized_tag_name": "Heavy" + }, + { + "category": "Class", + "internal_name": "Pyro", + "localized_category_name": "Class", + "localized_tag_name": "Pyro" + }, + { + "category": "Class", + "internal_name": "Spy", + "localized_category_name": "Class", + "localized_tag_name": "Spy" + }, + { + "category": "Class", + "internal_name": "Engineer", + "localized_category_name": "Class", + "localized_tag_name": "Engineer" + } + ] +} \ No newline at end of file diff --git a/tests/json/hong_kong_cone.json b/tests/json/hong_kong_cone.json new file mode 100644 index 0000000..aeac1f2 --- /dev/null +++ b/tests/json/hong_kong_cone.json @@ -0,0 +1,132 @@ +{ + "appid": 440, + "contextid": "2", + "assetid": "9405151126", + "classid": "3716628051", + "instanceid": "5393432478", + "amount": "1", + "currency": 0, + "background_color": "3C352E", + "icon_url": "IzMF03bi9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdEH9myp0erksICfSMf6UeRJpnqWSMU5OD2IwJkXVZnihXOjLx2Sk5MbUqMcbBnQz4ruyeU3L2ZDuWf3CKI1ZnE-QxbjeHqVz2urPBLWSYA795XQhVevAF8GZBPMyJPRpohYFd-GbswhV-S0B_dJQeKVe6zncXNOt2zHZcNcUFbK_2cpI", + "icon_url_large": "IzMF03bi9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdEH9myp0erksICfSMf6UeRJpnqWSMU5OD2IwJkXVZnihXOjLx2Sk5MbUqMcbBnQz4ruyeU3L2ZDuWf3CKI1ZnE-QxbjeHqVz2urPBLWSYA795XQhVevAF8GZBPMyJPRpohYFd-GbswhV-S0B_dJQeKVe6zncXNOt2zHZcNcUFbK_2cpI", + "descriptions": [ + { + "value": "(Assists: 1027)", + "color": "756b5e" + }, + { + "value": "(Kills: 2567)", + "color": "756b5e" + }, + { + "value": "(Damage Dealt: 762943)", + "color": "756b5e" + }, + { + "value": "Paint Color: An Extraordinary Abundance of Tinge", + "color": "756b5e" + }, + { + "value": "\u2605 Unusual Effect: Neutron Star", + "color": "ffd700" + }, + { + "value": "" + } + ], + "tradable": 1, + "actions": [ + { + "link": "http://wiki.teamfortress.com/scripts/itemredirect.php?id=30177&lang=en_US", + "name": "Item Wiki Page..." + }, + { + "link": "steam://rungame/440/76561202255233023/+tf_econ_item_preview%20S%owner_steamid%A%assetid%D14825046932280863783", + "name": "Inspect in Game..." + } + ], + "name": "Strange Unusual Hong Kong Cone", + "name_color": "8650AC", + "type": "Strange Hat - Points Scored: 5427", + "market_name": "Strange Unusual Hong Kong Cone", + "market_hash_name": "Strange Unusual Hong Kong Cone", + "market_actions": [ + { + "link": "steam://rungame/440/76561202255233023/+tf_econ_item_preview%20M%listingid%A%assetid%D14825046932280863783", + "name": "Inspect in Game..." + } + ], + "commodity": 0, + "market_tradable_restriction": 7, + "market_marketable_restriction": 0, + "marketable": 1, + "tags": [ + { + "category": "Quality", + "internal_name": "rarity4", + "localized_category_name": "Quality", + "localized_tag_name": "Unusual", + "color": "8650AC" + }, + { + "category": "Type", + "internal_name": "misc", + "localized_category_name": "Type", + "localized_tag_name": "Cosmetic" + }, + { + "category": "Class", + "internal_name": "Scout", + "localized_category_name": "Class", + "localized_tag_name": "Scout" + }, + { + "category": "Class", + "internal_name": "Sniper", + "localized_category_name": "Class", + "localized_tag_name": "Sniper" + }, + { + "category": "Class", + "internal_name": "Soldier", + "localized_category_name": "Class", + "localized_tag_name": "Soldier" + }, + { + "category": "Class", + "internal_name": "Demoman", + "localized_category_name": "Class", + "localized_tag_name": "Demoman" + }, + { + "category": "Class", + "internal_name": "Medic", + "localized_category_name": "Class", + "localized_tag_name": "Medic" + }, + { + "category": "Class", + "internal_name": "Heavy", + "localized_category_name": "Class", + "localized_tag_name": "Heavy" + }, + { + "category": "Class", + "internal_name": "Pyro", + "localized_category_name": "Class", + "localized_tag_name": "Pyro" + }, + { + "category": "Class", + "internal_name": "Spy", + "localized_category_name": "Class", + "localized_tag_name": "Spy" + }, + { + "category": "Class", + "internal_name": "Engineer", + "localized_category_name": "Class", + "localized_tag_name": "Engineer" + } + ] +} \ No newline at end of file diff --git a/tests/json/offer.json b/tests/json/offer.json new file mode 100644 index 0000000..ce5be26 --- /dev/null +++ b/tests/json/offer.json @@ -0,0 +1,63 @@ +{ + "tradeofferid": "4289274434", + "accountid_other": 293059984, + "message": "", + "expiration_time": 1605970340, + "trade_offer_state": 3, + "items_to_give": [ + { + "appid": 440, + "contextid": "2", + "assetid": "9422622596", + "classid": "2675", + "instanceid": "11040547", + "amount": "1", + "missing": true + }, + { + "appid": 440, + "contextid": "2", + "assetid": "9422622671", + "classid": "2675", + "instanceid": "11040547", + "amount": "1", + "missing": true + } + ], + "items_to_receive": [ + { + "appid": 440, + "contextid": "2", + "assetid": "9443876106", + "classid": "5564", + "instanceid": "11040547", + "amount": "1", + "missing": true + }, + { + "appid": 440, + "contextid": "2", + "assetid": "9443871431", + "classid": "2675", + "instanceid": "11040547", + "amount": "1", + "missing": true + }, + { + "appid": 440, + "contextid": "2", + "assetid": "9443871404", + "classid": "2675", + "instanceid": "11040547", + "amount": "1", + "missing": true + } + ], + "is_our_offer": false, + "time_created": 1604760740, + "time_updated": 1604760772, + "tradeid": "3622544162294464626", + "from_real_time_trade": false, + "escrow_end_date": 0, + "confirmation_method": 2 +} \ No newline at end of file diff --git a/tests/test_offer.py b/tests/test_offer.py new file mode 100644 index 0000000..da7cea9 --- /dev/null +++ b/tests/test_offer.py @@ -0,0 +1,25 @@ +from src.tf2_utils import Offer +from src.tf2_utils.utils import read_json_file + +from unittest import TestCase + +OFFER = read_json_file("./tests/json/offer.json") + +offer = Offer(OFFER) + + +class TestUtils(TestCase): + def test_offer_state(self): + self.assertEqual(False, offer.is_active()) + self.assertEqual(False, offer.is_declined()) + self.assertEqual(True, offer.is_accepted()) + + def test_offer_sides(self): + self.assertEqual(False, offer.is_our_offer()) + self.assertEqual(False, offer.is_scam()) + self.assertEqual(False, offer.is_gift()) + self.assertEqual(True, offer.is_two_sided()) + + def test_offer_partner(self): + self.assertEqual(False, offer.has_trade_hold()) + self.assertEqual("76561198253325712", offer.get_partner()) diff --git a/tests/test_sku.py b/tests/test_sku.py new file mode 100644 index 0000000..8f40e3e --- /dev/null +++ b/tests/test_sku.py @@ -0,0 +1,53 @@ +from src.tf2_utils import Item, get_sku, get_sku_properties +from src.tf2_utils.utils import read_json_file + +from unittest import TestCase + +file_path = "./tests/json/{}.json" + +CRUSADERS_CROSSBOW = read_json_file(file_path.format("crusaders_crossbow")) +HONG_KONG_CONE = read_json_file(file_path.format("hong_kong_cone")) +ELLIS_CAP = read_json_file(file_path.format("ellis_cap")) + + +class TestUtils(TestCase): + def test_ellis_cap_sku(self): + sku = get_sku(ELLIS_CAP) + + # https://marketplace.tf/items/tf2/263;6 + self.assertEqual("263;6", sku) + + def test_ellis_cap_sku_properties(self): + sku = get_sku_properties(ELLIS_CAP) + + self.assertEqual( + { + "defindex": 263, + "quality": 6, + "australium": False, + "craftable": True, + "wear": -1, + "killstreak_tier": -1, + "festivized": False, + "strange": False, + }, + sku, + ) + + def test_ellis_cap_properties(self): + item = Item(ELLIS_CAP) + is_craft_hat = item.is_craft_hat() + + self.assertEqual(True, is_craft_hat) + + def test_crusaders_crossbow_sku(self): + sku = get_sku(CRUSADERS_CROSSBOW) + + # https://marketplace.tf/items/tf2/305;11;kt-3;festive + self.assertEqual("305;11;kt-3;festive", sku) + + def test_strange_unusual_hong_kong_cone(self): + sku = get_sku(HONG_KONG_CONE) + + # https://marketplace.tf/items/tf2/30177;5;u107;strange + self.assertEqual("30177;5;u107;strange", sku) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..73df832 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,34 @@ +from src.tf2_utils import account_id_to_steam_id, to_scrap, to_refined, refinedify + +from unittest import TestCase + + +class TestUtils(TestCase): + def test_steam_id(self): + steam_id = account_id_to_steam_id("293059984") + + self.assertEqual("76561198253325712", steam_id) + + def test_to_refined(self): + scrap = 43 + refined = to_refined(scrap) + + self.assertEqual(4.77, refined) + + def test_to_scrap(self): + refined = 2.44 + scrap = to_scrap(refined) + + self.assertEqual(22, scrap) + + def test_refinedify_up(self): + wrong_value = 32.53 + refined = refinedify(wrong_value) + + self.assertEqual(32.55, refined) + + def test_refinedify_down(self): + wrong_value = 12.47 + refined = refinedify(wrong_value) + + self.assertEqual(12.44, refined)