From 4d623f668f396b8724fd2b1f6c8c3bfeb22147b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bukowski?= Date: Mon, 23 Dec 2024 23:10:08 +0100 Subject: [PATCH] Change minimal Python version to 3.12 (#421) * Add minor fixes and bump minimal python version to 3.12 * Update setup.py to version 1.2.0 --------- Authored-by: Wok --- .github/workflows/pythonpr.yml | 2 +- README.md | 2 +- examples/inventory.py | 2 +- setup.py | 10 +++++--- steampy/client.py | 7 +++--- steampy/confirmation.py | 3 +-- steampy/guard.py | 4 +++- steampy/login.py | 7 +++--- steampy/models.py | 1 + steampy/utils.py | 9 +++++--- test/test_client.py | 42 +++++++++++++++++----------------- test/test_guard.py | 10 ++++---- test/test_market.py | 16 ++++++------- test/test_utils.py | 16 ++++++------- 14 files changed, 69 insertions(+), 62 deletions(-) diff --git a/.github/workflows/pythonpr.yml b/.github/workflows/pythonpr.yml index abb6ce3..5b7332c 100644 --- a/.github/workflows/pythonpr.yml +++ b/.github/workflows/pythonpr.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: ["3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index f949e2a..fa88508 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Table of Content Installation ============ -Requires python 3.8 at least +Requires python 3.12 at least ``` pip install steampy ``` diff --git a/examples/inventory.py b/examples/inventory.py index b9ede25..e9761d0 100644 --- a/examples/inventory.py +++ b/examples/inventory.py @@ -57,6 +57,6 @@ # Dump all the info to inventory_(app_id)_(context_id).json file print('Saving information...') -with Path(f'inventory_{app_id}_{context_id}.json').open('w') as file: +with Path(f'inventory_{app_id}_{context_id}.json').open('w', encoding='utf-8') as file: json.dump(item_amounts, file) print(f'Done! Saved to file: inventory_{app_id}_{context_id}.json') diff --git a/setup.py b/setup.py index c083e33..3280789 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,15 @@ +import sys from setuptools import setup -version = '1.1.3' +if sys.version_info < (3, 12): + sys.exit("Python < 3.12 is not supported") + +version = '1.2.0' setup( name='steampy', - packages=['steampy', 'test', 'examples' ], + packages=['steampy', 'test', 'examples'], version=version, description='A Steam lib for trade automation', author='MichaƂ Bukowski', @@ -13,7 +17,7 @@ license='MIT', url='https://github.com/bukson/steampy', download_url='https://github.com/bukson/steampy/tarball/' + version, - keywords=['steam', 'trade' ], + keywords=['steam', 'trade'], classifiers=[], install_requires=[ "requests", diff --git a/steampy/client.py b/steampy/client.py index fdd8bc0..ff1eae6 100644 --- a/steampy/client.py +++ b/steampy/client.py @@ -85,12 +85,11 @@ def get_steam_id(self) -> int: response = self._session.get(url) if steam_id := re.search(r'g_steamID = "(\d+)";', response.text): return int(steam_id.group(1)) - else: - raise ValueError(f'Invalid steam_id: {steam_id}') + raise ValueError(f'Invalid steam_id: {steam_id}') def login(self, username: str | None = None, password: str | None = None, steam_guard: str | None = None) -> None: - invalid_client_credentials_is_present = None in (self.username, self._password, self.steam_guard_string) - invalid_login_credentials_is_present = None in (username, password, steam_guard) + invalid_client_credentials_is_present = None in {self.username, self._password, self.steam_guard_string} + invalid_login_credentials_is_present = None in {username, password, steam_guard} if invalid_client_credentials_is_present and invalid_login_credentials_is_present: raise InvalidCredentials( diff --git a/steampy/confirmation.py b/steampy/confirmation.py index be03a84..4ff9b14 100644 --- a/steampy/confirmation.py +++ b/steampy/confirmation.py @@ -66,8 +66,7 @@ def _get_confirmations(self) -> list[Confirmation]: nonce = conf['nonce'] confirmations.append(Confirmation(data_confid, nonce)) return confirmations - else: - raise ConfirmationExpected + raise ConfirmationExpected def _fetch_confirmations_page(self) -> requests.Response: tag = Tag.CONF.value diff --git a/steampy/guard.py b/steampy/guard.py index 5a80903..e558c47 100644 --- a/steampy/guard.py +++ b/steampy/guard.py @@ -15,11 +15,13 @@ def load_steam_guard(steam_guard: str) -> dict[str, str]: Arguments: steam_guard (str): If this string is a path to a file, then its contents will be parsed as a json data. Otherwise, the string will be parsed as a json data. + Returns: Dict[str, str]: Parsed json data as a dictionary of strings (both key and value). + """ if Path(steam_guard).is_file(): - with Path(steam_guard).open() as f: + with Path(steam_guard).open(encoding='utf-8') as f: return json.loads(f.read(), parse_int=str) else: return json.loads(steam_guard, parse_int=str) diff --git a/steampy/login.py b/steampy/login.py index 9f798a1..d6e5732 100644 --- a/steampy/login.py +++ b/steampy/login.py @@ -30,10 +30,9 @@ def _api_call(self, method: str, service: str, endpoint: str, version: str = 'v1 headers = {'Referer': f'{SteamUrl.COMMUNITY_URL}/', 'Origin': SteamUrl.COMMUNITY_URL} if method.upper() == 'GET': return self.session.get(url, params=params, headers=headers) - elif method.upper() == 'POST': + if method.upper() == 'POST': return self.session.post(url, data=params, headers=headers) - else: - raise ValueError('Method must be either GET or POST') + raise ValueError('Method must be either GET or POST') def login(self) -> Session: login_response = self._send_login_request() @@ -53,7 +52,7 @@ def _send_login_request(self) -> Response: request_data = self._prepare_login_request_data(encrypted_password, rsa_timestamp) return self._api_call('POST', 'IAuthenticationService', 'BeginAuthSessionViaCredentials', params=request_data) - def set_sessionid_cookies(self): + def set_sessionid_cookies(self) -> None: community_domain = SteamUrl.COMMUNITY_URL[8:] store_domain = SteamUrl.STORE_URL[8:] community_cookie_dic = self.session.cookies.get_dict(domain=community_domain) diff --git a/steampy/models.py b/steampy/models.py index ca1fe40..0c37a99 100644 --- a/steampy/models.py +++ b/steampy/models.py @@ -6,6 +6,7 @@ class PredefinedOptions(NamedTuple): app_id: str context_id: str + class GameOptions: STEAM = PredefinedOptions('753', '6') DOTA2 = PredefinedOptions('570', '2') diff --git a/steampy/utils.py b/steampy/utils.py index f828cf7..d1096f8 100644 --- a/steampy/utils.py +++ b/steampy/utils.py @@ -23,8 +23,7 @@ def login_required(func): def func_wrapper(self, *args, **kwargs): if not self.was_login_executed: raise LoginRequired('Use login method first') - else: - return func(self, *args, **kwargs) + return func(self, *args, **kwargs) return func_wrapper @@ -67,8 +66,10 @@ def calculate_gross_price(price_net: Decimal, publisher_fee: Decimal, steam_fee: fraud incidents and cover the cost of development of this and future Steam economy features. The fee is currently `5%` (with a minimum fee of `$0.01`). This fee may be increased or decreased by Steam in the future. + Returns: Decimal: Gross price (including fees) - the amount that the buyer pays during a market transaction + """ price_net *= 100 steam_fee_amount = int(math.floor(max(price_net * steam_fee, 1))) @@ -88,8 +89,10 @@ def calculate_net_price(price_gross: Decimal, publisher_fee: Decimal, steam_fee: fraud incidents and cover the cost of development of this and future Steam economy features. The fee is currently `5%` (with a minimum fee of `$0.01`). This fee may be increased or decreased by Steam in the future. + Returns: Decimal: Net price (without fees) - the amount that the seller receives after a market transaction. + """ price_gross *= 100 estimated_net_price = Decimal(int(price_gross / (steam_fee + publisher_fee + 1))) @@ -244,7 +247,7 @@ def get_key_value_from_url(url: str, key: str, case_sensitive: bool = True) -> s def load_credentials(): dirname = Path(__file__).resolve().parent - with Path(f'{dirname}/../secrets/credentials.pwd').open() as f: + with Path(f'{dirname}/../secrets/credentials.pwd').open(encoding='utf-8') as f: return [Credentials(line.split()[0], line.split()[1], line.split()[2]) for line in f] diff --git a/test/test_client.py b/test/test_client.py index 8717e76..a071b20 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -12,38 +12,38 @@ @unittest.skip('Requires secrets/Steamguard.txt') class TestSteamClient(TestCase): @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.credentials = load_credentials()[0] dirname = Path(__file__).resolve().parent cls.steam_guard_file = f'{dirname}/../secrets/Steamguard.txt' - def test_get_steam_id(self): + def test_get_steam_id(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) assert client.get_steam_id() == int(self.steam_guard_file['Session']['SteamID']) - def test_login(self): + def test_login(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) - def test_is_session_alive(self): + def test_is_session_alive(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) assert client.is_session_alive() - def test_logout(self): + def test_logout(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) assert client.is_session_alive() client.logout() - def test_client_with_statement(self): + def test_client_with_statement(self) -> None: with SteamClient( self.credentials.api_key, self.credentials.login, self.credentials.password, self.steam_guard_file, ) as client: assert client.is_session_alive() - def test_send_offer_without_sessionid_cookie(self): + def test_send_offer_without_sessionid_cookie(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) client._session.cookies.set('sessionid', None, domain='steamcommunity.com') @@ -55,7 +55,7 @@ def test_send_offer_without_sessionid_cookie(self): trade_offer_url = '' self.assertRaises(AttributeError, lambda: client.make_offer_with_url([my_asset], [], trade_offer_url, 'TEST')) - def test_sessionid_cookie(self): + def test_sessionid_cookie(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) community_cookies = client._session.cookies.get_dict('steamcommunity.com') @@ -63,13 +63,13 @@ def test_sessionid_cookie(self): assert 'sessionid' in community_cookies assert 'sessionid' in store_cookies - def test_get_my_inventory(self): + def test_get_my_inventory(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) inventory = client.get_my_inventory(GameOptions.CS) assert inventory is not None - def test_get_partner_inventory(self): + def test_get_partner_inventory(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) partner_id = '' @@ -77,48 +77,48 @@ def test_get_partner_inventory(self): inventory = client.get_partner_inventory(partner_id, game) assert inventory is not None - def test_get_trade_offers_summary(self): + def test_get_trade_offers_summary(self) -> None: client = SteamClient(self.credentials.api_key) summary = client.get_trade_offers_summary() assert summary is not None - def test_get_trade_offers(self): + def test_get_trade_offers(self) -> None: client = SteamClient(self.credentials.api_key) offers = client.get_trade_offers() assert offers is not None - def test_get_trade_offer(self): + def test_get_trade_offer(self) -> None: client = SteamClient(self.credentials.api_key) trade_offer_id = '1442685162' offer = client.get_trade_offer(trade_offer_id) assert offer is not None - def test_accept_trade_offer_without_login(self): + def test_accept_trade_offer_without_login(self) -> None: client = SteamClient(self.credentials.api_key) self.assertRaises(LoginRequired, client.accept_trade_offer, 'id') - def test_accept_trade_offer(self): + def test_accept_trade_offer(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) trade_offer_id = '1451378159' response_dict = client.accept_trade_offer(trade_offer_id) assert response_dict is not None - def test_decline_trade_offer(self): + def test_decline_trade_offer(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) trade_offer_id = '1449530707' response_dict = client.decline_trade_offer(trade_offer_id) assert response_dict['response'] == {} - def test_cancel_trade_offer(self): + def test_cancel_trade_offer(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) trade_offer_id = '1450637835' response_dict = client.cancel_trade_offer(trade_offer_id) assert response_dict['response'] == {} - def test_make_offer(self): + def test_make_offer(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) partner_id = '' @@ -133,7 +133,7 @@ def test_make_offer(self): assert response is not None assert 'tradeofferid' in response - def test_make_offer_url(self): + def test_make_offer_url(self) -> None: partner_account_id = '32384925' partner_token = '7vqRtBpC' sample_trade_url = ( @@ -154,7 +154,7 @@ def test_make_offer_url(self): assert response is not None assert 'tradeofferid' in response - def test_get_escrow_duration(self): + def test_get_escrow_duration(self) -> None: # A sample trade URL with escrow time of 15 days cause mobile auth not added sample_trade_url = 'https://steamcommunity.com/tradeoffer/new/?partner=314218906&token=sgA4FdNm' client = SteamClient(self.credentials.api_key) @@ -162,7 +162,7 @@ def test_get_escrow_duration(self): response = client.get_escrow_duration(sample_trade_url) assert response == 15 - def test_get_wallet_balance(self): + def test_get_wallet_balance(self) -> None: with SteamClient( self.credentials.api_key, self.credentials.login, self.credentials.password, self.steam_guard_file, ) as client: diff --git a/test/test_guard.py b/test/test_guard.py index 83421d0..763e025 100644 --- a/test/test_guard.py +++ b/test/test_guard.py @@ -7,26 +7,26 @@ class TestGuard(TestCase): @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.shared_secret = b64encode(b'1234567890abcdefghij') cls.identity_secret = b64encode(b'abcdefghijklmnoprstu') - def test_one_time_code(self): + def test_one_time_code(self) -> None: timestamp = 1469184207 code = guard.generate_one_time_code(self.shared_secret, timestamp) assert code == 'P2QJN' - def test_confirmation_key(self): + def test_confirmation_key(self) -> None: timestamp = 1470838334 confirmation_key = guard.generate_confirmation_key(self.identity_secret, Tag.CONF.value, timestamp) assert confirmation_key == b'pWqjnkcwqni+t/n+5xXaEa0SGeA=' - def test_generate_device_id(self): + def test_generate_device_id(self) -> None: steam_id = '12341234123412345' device_id = guard.generate_device_id(steam_id) assert device_id == 'android:677cf5aa-3300-7807-d1e2-c408142742e2' - def test_load_steam_guard(self): + def test_load_steam_guard(self) -> None: expected_keys = ('steamid', 'shared_secret', 'identity_secret') guard_json_str = '{"steamid": 12345678, "shared_secret": "SHARED_SECRET", "identity_secret": "IDENTITY_SECRET"}' diff --git a/test/test_market.py b/test/test_market.py index 4670e47..50eae5c 100644 --- a/test/test_market.py +++ b/test/test_market.py @@ -11,19 +11,19 @@ @unittest.skip('Requires secrets/Steamguard.txt') class TestMarket(TestCase): @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.credentials = load_credentials()[0] dirname = Path(__file__).resolve().parent cls.steam_guard_file = f'{dirname}/../secrets/Steamguard.txt' - def test_get_price(self): + def test_get_price(self) -> None: client = SteamClient(self.credentials.api_key) item = 'M4A1-S | Cyrex (Factory New)' prices = client.market.fetch_price(item, GameOptions.CS) assert prices['success'] - def test_get_price_to_many_requests(self): - def request_loop(): + def test_get_price_to_many_requests(self) -> None: + def request_loop() -> None: item = 'M4A1-S | Cyrex (Factory New)' for _ in range(21): client.market.fetch_price(item, GameOptions.CS) @@ -32,7 +32,7 @@ def request_loop(): client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) self.assertRaises(TooManyRequests, request_loop) - def test_get_price_history(self): + def test_get_price_history(self) -> None: with SteamClient( self.credentials.api_key, self.credentials.login, self.credentials.password, self.steam_guard_file, ) as client: @@ -41,7 +41,7 @@ def test_get_price_history(self): assert response['success'] assert 'prices' in response - def test_get_all_listings_from_market(self): + def test_get_all_listings_from_market(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) listings = client.market.get_my_market_listings() @@ -50,7 +50,7 @@ def test_get_all_listings_from_market(self): assert len(listings.get('sell_listings')) == 1 assert isinstance(next(iter(listings.get('sell_listings').values())).get('description'), dict) - def test_create_and_remove_sell_listing(self): + def test_create_and_remove_sell_listing(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) game = GameOptions.DOTA2 @@ -72,7 +72,7 @@ def test_create_and_remove_sell_listing(self): assert listing_to_cancel is not None client.market.cancel_sell_order(listing_to_cancel) - def test_create_and_cancel_buy_order(self): + def test_create_and_cancel_buy_order(self) -> None: client = SteamClient(self.credentials.api_key) client.login(self.credentials.login, self.credentials.password, self.steam_guard_file) # PUT THE REAL CURRENCY OF YOUR STEAM WALLET, OTHER CURRENCIES WON'T WORK diff --git a/test/test_utils.py b/test/test_utils.py index 262daa2..5082e8f 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -5,37 +5,37 @@ class TestUtils(TestCase): - def test_text_between(self): + def test_text_between(self) -> None: text = 'var a = "dupadupa";' text_between = utils.text_between(text, 'var a = "', '";') assert text_between == 'dupadupa' - def test_texts_between(self): + def test_texts_between(self) -> None: text = '
  • element 1
  • \n
  • some random element
  • ' items = list(utils.texts_between(text, '
  • ', '
  • ')) assert items == ['element 1', 'some random element'] - def test_account_id_to_steam_id(self): + def test_account_id_to_steam_id(self) -> None: account_id = '358617487' steam_id = utils.account_id_to_steam_id(account_id) assert steam_id == '76561198318883215' - def test_steam_id_to_account_id(self): + def test_steam_id_to_account_id(self) -> None: steam_id = '76561198318883215' account_id = utils.steam_id_to_account_id(steam_id) assert account_id == '358617487' - def test_get_key_value_from_url(self): + def test_get_key_value_from_url(self) -> None: url = 'https://steamcommunity.com/tradeoffer/new/?partner=aaa&token=bbb' assert utils.get_key_value_from_url(url, 'partner') == 'aaa' assert utils.get_key_value_from_url(url, 'token') == 'bbb' - def test_get_key_value_from_url_case_insensitive(self): + def test_get_key_value_from_url_case_insensitive(self) -> None: url = 'https://steamcommunity.com/tradeoffer/new/?Partner=aaa&Token=bbb' assert utils.get_key_value_from_url(url, 'partner', case_sensitive=False) == 'aaa' assert utils.get_key_value_from_url(url, 'token', case_sensitive=False) == 'bbb' - def test_calculate_gross_price(self): + def test_calculate_gross_price(self) -> None: steam_fee = Decimal('0.05') # 5% publisher_fee = Decimal('0.1') # 10% @@ -43,7 +43,7 @@ def test_calculate_gross_price(self): assert utils.calculate_gross_price(Decimal('0.10'), publisher_fee, steam_fee) == Decimal('0.12') assert utils.calculate_gross_price(Decimal(100), publisher_fee, steam_fee) == Decimal(115) - def test_calculate_net_price(self): + def test_calculate_net_price(self) -> None: steam_fee = Decimal('0.05') # 5% publisher_fee = Decimal('0.1') # 10%