diff --git a/conftest.py b/conftest.py index a981554..f98fddc 100644 --- a/conftest.py +++ b/conftest.py @@ -26,6 +26,7 @@ def get_item_data(file_name: str) -> dict: SPELLED_ITEM = get_item_data("spelled_item") PAINTED_HAT = get_item_data("painted_hat") ELLIS_CAP = get_item_data("ellis_cap") +STRANGE_PART = get_item_data("strange_part") @pytest.fixture @@ -84,6 +85,11 @@ def ellis_cap() -> dict: return ELLIS_CAP +@pytest.fixture +def strange_part() -> dict: + return STRANGE_PART + + @pytest.fixture async def aiohttp_session(): async with ClientSession() as session: diff --git a/docs/usage/inventory.py b/docs/usage/inventory.py index f2cb186..0f5fbe3 100644 --- a/docs/usage/inventory.py +++ b/docs/usage/inventory.py @@ -18,6 +18,10 @@ provider = Inventory() # (the same as above) # using steamcommunity will not require an API key and fetches 5000 items at once +# SteamCommunity uses a hardcoded browser-style User-Agent. +# Referer is set to the target user's profile inventory page for every fetch. +# These headers do not guarantee that Steam's rate limits will be avoided. + # Get the inventory of a user user_inventory = provider.fetch("76561198828172881") diff --git a/src/tf2_utils/__init__.py b/src/tf2_utils/__init__.py index 2f222f4..60bacbd 100644 --- a/src/tf2_utils/__init__.py +++ b/src/tf2_utils/__init__.py @@ -1,7 +1,7 @@ # flake8: noqa: F401, F403 __title__ = "tf2-utils" __author__ = "offish" -__version__ = "2.4.1" +__version__ = "2.4.3" __license__ = "MIT" from .currency import CurrencyExchange diff --git a/src/tf2_utils/inventory.py b/src/tf2_utils/inventory.py index 830ab68..66dd85c 100644 --- a/src/tf2_utils/inventory.py +++ b/src/tf2_utils/inventory.py @@ -62,11 +62,19 @@ def __init__( self.provider = i(api_key) break - def fetch(self, steam_id: str, app_id: int = 440, context_id: int = 2) -> dict: + def fetch( + self, + steam_id: str, + app_id: int = 440, + context_id: int = 2, + start_assetid: str | None = None, + ) -> dict: url, params = self.provider.get_url_and_params(steam_id, app_id, context_id) + if start_assetid is not None: + url = f"{url}?start_assetid={start_assetid}" response = requests.get(url, params=params, headers=self.provider.headers) try: return response.json() - except Exception as e: + except ValueError as e: return {"success": False, "error": str(e)} diff --git a/src/tf2_utils/item.py b/src/tf2_utils/item.py index b517b0b..8b2e35f 100644 --- a/src/tf2_utils/item.py +++ b/src/tf2_utils/item.py @@ -64,7 +64,7 @@ def has_quality(self, quality: str) -> bool: return self.get_quality() == quality def has_strange_in_name(self) -> bool: - return "Strange" in self.name + return "Strange " in self.name and "Strange Part: " not in self.name def has_vintage_in_name(self) -> bool: return "Vintage" in self.name diff --git a/src/tf2_utils/providers/steamcommunity.py b/src/tf2_utils/providers/steamcommunity.py index d459a7c..ed638a3 100644 --- a/src/tf2_utils/providers/steamcommunity.py +++ b/src/tf2_utils/providers/steamcommunity.py @@ -1,10 +1,44 @@ +from requests.structures import CaseInsensitiveDict + from .provider import Provider class SteamCommunity(Provider): + def __init__(self, api_key: str = "") -> None: + super().__init__(api_key) + self.headers = CaseInsensitiveDict( + { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/131.0.0.0 Safari/537.36" + ), + "Accept": ( + "text/html,application/xhtml+xml,application/xml;q=0.9," + "image/avif,image/webp,image/apng,*/*;q=0.8," + "application/signed-exchange;v=b3;q=0.7" + ), + "Accept-Language": "en-US,en;q=0.9,ja;q=0.8,ms;q=0.7", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + "Cookie": "", + "Host": "steamcommunity.com", + "Upgrade-Insecure-Requests": "1", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Referer": "https://steamcommunity.com/", + } + ) + def get_url_and_params( self, steam_id: str, app_id: int, context_id: int ) -> tuple[str, dict]: + self.headers["Referer"] = ( + f"https://steamcommunity.com/profiles/{steam_id}/inventory" + ) return ( f"https://steamcommunity.com/inventory/{steam_id}/{app_id}/{context_id}", {"l": "english", "count": 2500}, diff --git a/tests/test_inventory.py b/tests/test_inventory.py index ea1ea5a..a7221b6 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -1,3 +1,5 @@ +from unittest.mock import Mock, patch + import pytest from src.tf2_utils import InvalidInventory, map_inventory @@ -56,6 +58,33 @@ def test_steamapis_inventory_provider() -> None: assert inventory.provider.api_key == "api_key" +@pytest.mark.parametrize("app_id,context_id", [(440, 2), (730, 3)]) +def test_steamcommunity_inventory_request(app_id, context_id) -> None: + inventory = Inventory() + response = Mock() + response.json.return_value = INVENTORY + + with patch("src.tf2_utils.inventory.requests.get", return_value=response) as get: + for steam_id in ["76561198253325712", "76561198828172881"]: + assert inventory.fetch(steam_id, app_id, context_id) is INVENTORY + get.assert_called_with( + f"https://steamcommunity.com/inventory/" + f"{steam_id}/{app_id}/{context_id}", + params={"l": "english", "count": 2500}, + headers=inventory.provider.headers, + ) + headers = get.call_args.kwargs["headers"] + assert headers["Referer"] == ( + f"https://steamcommunity.com/profiles/{steam_id}/inventory" + ) + assert headers["User-Agent"] == ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/130.0.0.0 Safari/537.36" + ) + assert "Origin" not in headers + + def test_steamcommunity_inventory_fetch(steam_id: str) -> None: provider = Inventory("steamcommunity") inventory = provider.fetch(steam_id) diff --git a/tests/test_providers.py b/tests/test_providers.py index 8d21475..54d7b11 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -1,7 +1,10 @@ +import pytest + from src.tf2_utils.providers.express_load import ExpressLoad from src.tf2_utils.providers.steamcommunity import SteamCommunity +@pytest.mark.parametrize("steam_id", ["76561198253325712"]) def test_steam_community_inventory(steam_id: str) -> None: provider = SteamCommunity() url, params = provider.get_url_and_params(steam_id, 440, 2) @@ -12,7 +15,43 @@ def test_steam_community_inventory(steam_id: str) -> None: "l": "english", "count": 2500, } - assert provider.headers == {} + assert provider.headers["User-Agent"].startswith("Mozilla/5.0 ") + assert "Chrome/" in provider.headers["User-Agent"] + assert provider.headers["Referer"] == ( + f"https://steamcommunity.com/profiles/{steam_id}/inventory" + ) + + +def test_steam_community_api_key() -> None: + provider = SteamCommunity("api_key") + + assert provider.api_key == "api_key" + assert provider.headers["User-Agent"] == ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/130.0.0.0 Safari/537.36" + ) + + +def test_steam_community_headers_are_independent() -> None: + first = SteamCommunity() + second = SteamCommunity() + first.get_url_and_params("76561198253325712", 440, 2) + + assert second.headers["User-Agent"].startswith("Mozilla/5.0 ") + assert "Referer" not in second.headers + + +def test_steam_community_referer_tracks_inventory() -> None: + provider = SteamCommunity() + for steam_id in ["76561198253325712", "76561198828172881"]: + url, params = provider.get_url_and_params(steam_id, 730, 2) + assert url == f"https://steamcommunity.com/inventory/{steam_id}/730/2" + assert params == {"l": "english", "count": 2500} + assert provider.headers["Referer"] == ( + f"https://steamcommunity.com/profiles/{steam_id}/inventory" + ) + assert len(provider.headers) == 2 def test_express_load_inventory(express_load_api_key: str, steam_id: str) -> None: diff --git a/tests/test_sku.py b/tests/test_sku.py index b238ff2..3ce218c 100644 --- a/tests/test_sku.py +++ b/tests/test_sku.py @@ -27,11 +27,25 @@ def test_ellis_cap_sku_properties(ellis_cap: dict) -> None: } +def test_strange_part_sku_properties(strange_part: dict) -> None: + assert get_sku_properties(strange_part) == { + "defindex": 6012, + "quality": 6, + "australium": False, + "craftable": True, + "wear": -1, + "killstreak_tier": -1, + "festivized": False, + "strange": False, + } + + def test_get_sku_items( ellis_cap: dict, crusaders_crossbow: dict, hong_kong_cone: dict, uncraftable_hat: dict, + strange_part: dict, ) -> None: # https://marketplace.tf/items/tf2/263;6 assert get_sku(ellis_cap) == "263;6" @@ -41,6 +55,8 @@ def test_get_sku_items( assert get_sku(hong_kong_cone) == "30177;5;u107;strange" # https://marketplace.tf/items/tf2/734;6;uncraftable assert get_sku(uncraftable_hat) == "734;6;uncraftable" + # https://marketplace.tf/items/tf2/6012;6 + assert get_sku(strange_part) == "6012;6" def test_properties() -> None: