From 5936bd0e1bd1fc7f207af4e3ed22955a2f092589 Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Sat, 4 Jan 2025 13:13:53 +0300 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D1=80=20=D0=B2=20=D0=B0=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convert_to_async.py | 85 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 convert_to_async.py diff --git a/convert_to_async.py b/convert_to_async.py new file mode 100644 index 0000000..4827f48 --- /dev/null +++ b/convert_to_async.py @@ -0,0 +1,85 @@ +"""Сгенерировать асинхронную версию клиента.""" + +DISCLAIMER = "# ЭТО АВТОМАТИЧЕСКИ СОЗДАННАЯ КОПИЯ СИНХРОННОГО КЛИЕНТА. НЕ ИЗМЕНЯЙТЕ САМОСТОЯТЕЛЬНО #" +DISCLAIMER = f'{"#" * len(DISCLAIMER)}\n{DISCLAIMER}\n{"#" * len(DISCLAIMER)}\n\n' + +REQUEST_METHODS = ('_request_wrapper', 'get', 'post', 'retrieve', 'download') + + +def gen_client(source: str, output: str) -> None: + """Generate async version of client.py.""" + with open(source, 'r', encoding='UTF-8') as f: + code = f.read() + + code = code.replace('class Client(', 'class ClientAsync(') + code = code.replace('ExtClient(Client)', 'ExtClientAsync(ClientAsync)') + code = code.replace('class WebClient', 'class WebClientAsync') + code = code.replace("""from steam_trader.api import ( + Client,""", """from steam_trader.api import ( + ClientAsync,""") + + code = code.replace('def wrapper', 'async def wrapper') + code = code.replace('result = method(', 'result = await method(') + code = code.replace('@log\n def', '@log\n async def') + code = code.replace('@property\n def', '@property\n async def') + code = code.replace('self._get_request', 'await self._get_request') + code = code.replace('self._post_request', 'await self._post_request') + + code = code.replace('def _get_request(', 'async def _get_request(') + code = code.replace('(self._httpx_client or httpx).get(', 'await self._httpx_client.get(') + + code = code.replace('def _post_request(', 'async def _post_request(') + code = code.replace('result = (self._httpx_client or httpx).post(', 'result = await self._httpx_client.post(') + + code = code.replace('self._httpx_client.close()', 'await self._httpx_client.aclose()') + code = code.replace('self._httpx_client = httpx.Client(', 'self._httpx_client = httpx.AsyncClient(') + + code = code.replace('def __enter__(self) -> Self:', 'async def __aenter__(self) -> Self:') + code = code.replace('def __exit__(self, exc_type, exc_val, exc_tb) -> None:', 'async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:') + + code = code.replace('url: str = self.base_url + method', """if not self._httpx_client: + raise ClientError('Необходимо использовать контекст async with ClientAsync()') + url: str = self.base_url + method""") + + code = code.replace("""result = (self._httpx_client or httpx).get( + url, + headers=headers, + params=params, + cookies=cookies, + **kwargs + ) + )""", """if not self._httpx_client: + raise ClientError('Необходимо использовать контекст async with ClientAsync()') + result = (self._httpx_client or httpx).get( + url, + headers=headers, + params=params, + cookies=cookies, + **kwargs + )""") + code = code.replace("""result = await self._httpx_client.post( + url, + headers=self.headers, + data=data + )""", """if not self._httpx_client: + raise ClientError('Необходимо использовать контекст async with ClientAsync()') + result = (await self._httpx_client.post( + url, + headers=self.headers, + data=data + ))""") + + code = code.replace('self.get_order_book(gid)', 'await self.get_order_book(gid)') + code = code.replace('self.get_item_info(', 'await self.get_item_info(') + + code = code.replace('await self.get_item_info(item.gid).filters', '(await self.get_item_info(item.gid)).filters') + + code = DISCLAIMER + code + with open(output, 'w', encoding='UTF-8') as f: + f.write(code) + + +if __name__ == '__main__': + gen_client('steam_trader/api/_client.py', 'steam_trader/api/_client_async.py') + gen_client('steam_trader/web/_client.py', 'steam_trader/web/_client_async.py') + gen_client('steam_trader/api/ext/_client_ext.py', 'steam_trader/api/ext/_client_async_ext.py') From d7657705e978bce6aa998f1c5f44279c544a5289 Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Sat, 4 Jan 2025 13:14:21 +0300 Subject: [PATCH 2/7] =?UTF-8?q?impr:=20=D0=A3=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- steam_trader/__init__.py | 4 +- steam_trader/__version__.py | 4 +- steam_trader/api/__init__.py | 2 +- steam_trader/api/_account.py | 155 ++---- steam_trader/api/_base.py | 17 +- steam_trader/api/_buy.py | 92 +--- steam_trader/api/_client.py | 604 +++++++++------------ steam_trader/api/_client_async.py | 632 ++++++++++------------ steam_trader/api/_edit_item.py | 88 +-- steam_trader/api/_item_info.py | 126 ++--- steam_trader/api/_misc.py | 343 ++++++------ steam_trader/api/_offers.py | 38 +- steam_trader/api/_p2p.py | 68 +-- steam_trader/api/_sale.py | 38 +- steam_trader/api/_trade.py | 100 ++-- steam_trader/api/ext/__init__.py | 2 +- steam_trader/api/ext/_client_async_ext.py | 146 ++--- steam_trader/api/ext/_client_ext.py | 123 ++--- steam_trader/api/ext/_misc.py | 37 +- steam_trader/constants.py | 406 +++++++------- steam_trader/web/__init__.py | 3 +- steam_trader/web/_base.py | 17 +- steam_trader/web/_client.py | 262 +++++---- steam_trader/web/_client_async.py | 293 +++++----- steam_trader/web/_dataclasses.py | 111 ++-- 25 files changed, 1715 insertions(+), 1996 deletions(-) diff --git a/steam_trader/__init__.py b/steam_trader/__init__.py index 9f1d8a0..19b80f5 100644 --- a/steam_trader/__init__.py +++ b/steam_trader/__init__.py @@ -1,6 +1,4 @@ -from .__version__ import __version__ -from .__version__ import __license__ -from .__version__ import __copyright__ +from .__version__ import __version__, __license__, __copyright__ __all__ = [ '__version__', diff --git a/steam_trader/__version__.py b/steam_trader/__version__.py index 16393b9..1feef43 100644 --- a/steam_trader/__version__.py +++ b/steam_trader/__version__.py @@ -1,3 +1,3 @@ -__version__ = '0.4.0' +__version__ = '1.0.0' __license__ = 'BSD 3-Clause License' -__copyright__ = 'Copyright (c) 2024-present, Lemon4ksan (aka Bananchiki) ' +__copyright__ = 'Copyright (c) 2024-2025, Lemon4ksan (aka Bananchiki) ' diff --git a/steam_trader/api/__init__.py b/steam_trader/api/__init__.py index ce9de0a..2783c9d 100644 --- a/steam_trader/api/__init__.py +++ b/steam_trader/api/__init__.py @@ -4,7 +4,7 @@ A basic wrapper for Steam Trader API -Licensed under the BSD 3-Clause License - Copyright (c) 2024-present, Lemon4ksan (aka Bananchiki) +Licensed under the BSD 3-Clause License - Copyright (c) 2024-2025, Lemon4ksan (aka Bananchiki) See LICENSE """ diff --git a/steam_trader/api/_account.py b/steam_trader/api/_account.py index 9f73b04..1da5db5 100644 --- a/steam_trader/api/_account.py +++ b/steam_trader/api/_account.py @@ -1,39 +1,31 @@ import logging from dataclasses import dataclass from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional, Union +from typing import Optional, Any from steam_trader.exceptions import BadRequestError, Unauthorized, NoBuyOrders, TooManyRequests from ._base import TraderClientObject from ._misc import InventoryItem, BuyOrder, Discount, OperationsHistoryItem, AltWebSocketMessage -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class WebSocketToken(TraderClientObject): """Класс, представляющий WebSocket токен. Attributes: - steam_id: (:obj:`str`): SteamID клиента. - time: (:obj:`int`): Время создание токена. - hash: (:obj:`str`): Хеш токена. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + steam_id: (str): SteamID клиента. + time: (int): Время создание токена. + hash: (str): Хеш токена. """ steam_id: str time: int hash: str - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'WebSocketToken': try: @@ -46,9 +38,9 @@ def de_json( except KeyError: pass - data = super(WebSocketToken, cls).de_json(data) + data = super(WebSocketToken, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) @@ -56,28 +48,25 @@ class Inventory(TraderClientObject): """Класс, представляющий инвентарь клиента. Attributes: - success (:obj:`bool`): Результат запроса. - count (:obj:`int`): Количество всех предметов в инвентаре Steam. - gameid (:obj:`int`): AppID игры к которой принадлежит инвентарь. - last_update (:obj:`int`): Timestamp последнего обновления инвентаря. - items (Sequence[:class:`steam_trader.InventoryItem`]): Последовательность с предметами в инвентаре. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + count (int): Количество всех предметов в инвентаре Steam. + gameid (int): AppID игры к которой принадлежит инвентарь. + last_update (int): Timestamp последнего обновления инвентаря. + items (Sequence[steam_trader.InventoryItem]): Последовательность с предметами в инвентаре. """ success: bool count: int gameid: int last_update: int - items: Sequence['InventoryItem'] - client: Union['Client', 'ClientAsync', None] + items: Sequence[InventoryItem] + @classmethod def de_json( - cls: dataclass, - data: dict, + cls, + data: dict[str, Any], status: Optional[Sequence[int]] = None, - client: Union['Client', 'ClientAsync', None] = None ) -> 'Inventory': if not data['success']: @@ -108,9 +97,9 @@ def de_json( for i, offer in enumerate(data['items']): data['items'][i] = InventoryItem.de_json(offer) - data = super(Inventory, cls).de_json(data) + data = super(Inventory, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) @@ -118,21 +107,18 @@ class BuyOrders(TraderClientObject): """Класс, представляющий ваши запросы на покупку. Attributes: - success (:obj:`bool`): Результат запроса. - data (Sequence[:class:`steam_trader.BuyOrder`]): Последовательность запросов на покупку. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + data (Sequence[steam_trader.BuyOrder]): Последовательность запросов на покупку. """ success: bool - data: Sequence['BuyOrder'] - client: Union['Client', 'ClientAsync', None] + data: Sequence[BuyOrder] + @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'BuyOrders': if not data['success']: @@ -149,9 +135,9 @@ def de_json( for i, offer in enumerate(data['data']): data['data'][i] = BuyOrder.de_json(offer) - data = super(BuyOrders, cls).de_json(data) + data = super(BuyOrders, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) @@ -159,21 +145,18 @@ class Discounts(TraderClientObject): """Класс, представляющий комиссии/скидки на игры, доступные на сайте. Attributes: - success (:obj:`bool`): Результат запроса. - data (dict[:obj:`int`, :class:`steam_trader.Discount`]): Словарь, содержащий комисии/скидки. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + data (dict[int, steam_trader.Discount]): Словарь, содержащий комисии/скидки. """ success: bool - data: dict[int, 'Discount'] - client: Union['Client', 'ClientAsync', None] + data: dict[int, Discount] + @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'Discounts': if not data['success']: @@ -187,9 +170,9 @@ def de_json( # Конвертируем ключ в число для совместимости с константами data['data'] = {int(appid): Discount.de_json(_dict) for appid, _dict in data['data'].items()} - data = super(Discounts, cls).de_json(data) + data = super(Discounts, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) @@ -197,21 +180,18 @@ class OperationsHistory(TraderClientObject): """Класс, представляющий истории операций, произведённых на сайте. Attributes: - success (:obj:`bool`): Результат запроса. - data (Sequence[:class:`steam_trader.OperationsHistoryItem`]): Последовательность историй операций. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + data (Sequence[steam_trader.OperationsHistoryItem]): Последовательность историй операций. """ success: bool - data: Sequence['OperationsHistoryItem'] - client: Union['Client', 'ClientAsync', None] + data: Sequence[OperationsHistoryItem] + @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'OperationsHistory': if not data['success']: @@ -226,9 +206,9 @@ def de_json( for i, item in enumerate(data['data']): data['data'][i] = OperationsHistoryItem.de_json(item) - data = super(OperationsHistory, cls).de_json(data) + data = super(OperationsHistory, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) @@ -236,25 +216,22 @@ class InventoryState(TraderClientObject): """Класс, представляющий текущий статус инвентаря. Attributes: - success (:obj:`bool`): Результат запроса. - updating_now (:obj:`bool`): Инвентарь обновляется в данный момент. - last_update (:obj:`int`): Timestamp, когда последний раз был обновлён инвентарь. - items_in_cache (:obj:`int`): Количество предметов в инвентаре. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + updating_now (bool): Инвентарь обновляется в данный момент. + last_update (int): Timestamp, когда последний раз был обновлён инвентарь. + items_in_cache (int): Количество предметов в инвентаре. """ success: bool updating_now: bool last_update: int items_in_cache: int - client: Union['Client', 'ClientAsync', None] + @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'InventoryState': data.update({ # перенос с camleCase на snake_case @@ -264,18 +241,9 @@ def de_json( }) del data['updatingNow'], data['lastUpdate'], data['itemsInCache'] - if not data['success']: - match data['code']: - case 400: - raise BadRequestError('Неправильный запрос.') - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') - - data = super(InventoryState, cls).de_json(data) + data = super(InventoryState, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) @@ -283,22 +251,19 @@ class AltWebSocket(TraderClientObject): """Класс, представляющий запрос альтернативным WebSocket. Attributes: - success (:obj:`bool`): Результат запроса. Если false, сообщений в поле messages не будет, + success (bool): Результат запроса. Если false, сообщений в поле messages не будет, при этом соединение будет поддержано. - messages (Sequence[:class:`steam_trader.AltWebSocketMessage`]): Последовательность с WebSocket сообщениями. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + messages (Sequence[steam_trader.AltWebSocketMessage]): Последовательность с WebSocket сообщениями. """ success: bool - messages: Sequence['AltWebSocketMessage'] - client: Union['Client', 'ClientAsync', None] + messages: Sequence[AltWebSocketMessage] + @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> Optional['AltWebSocket']: if not data['success']: @@ -308,6 +273,6 @@ def de_json( for i, message in enumerate(data['messages']): data['messages'][i] = AltWebSocketMessage.de_json(message) - data = super(AltWebSocket, cls).de_json(data) + data = super(AltWebSocket, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) diff --git a/steam_trader/api/_base.py b/steam_trader/api/_base.py index 15a5f9a..5505043 100644 --- a/steam_trader/api/_base.py +++ b/steam_trader/api/_base.py @@ -1,12 +1,7 @@ import dataclasses import logging from abc import ABCMeta -from dataclasses import dataclass -from typing import TYPE_CHECKING, Union -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync class TraderClientObject: """Базовый класс для всех api объектов библиотеки. @@ -18,20 +13,20 @@ class TraderClientObject: __metaclass__ = ABCMeta @classmethod - def de_json(cls: dataclass, data: dict, client: Union['Client', 'ClientAsync', None] = None) -> dict: + def _de_json(cls, data: dict) -> dict: """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + data (dict): Поля и значения десериализуемого объекта. Returns: - :obj:`dict`, optional: Словарь с валидными аттрибутами для создания датакласса. + dict, optional: Словарь с валидными аттрибутами для создания датакласса. """ - data = data.copy() + if not dataclasses.is_dataclass(cls): + raise TypeError("Ожидался датакласс.") + data = data.copy() fields = {f.name for f in dataclasses.fields(cls)} cleaned_data = {} diff --git a/steam_trader/api/_buy.py b/steam_trader/api/_buy.py index 0d0e013..02848c3 100644 --- a/steam_trader/api/_buy.py +++ b/steam_trader/api/_buy.py @@ -1,31 +1,25 @@ import logging from dataclasses import dataclass from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional, Union +from typing import Optional, Any from .. import exceptions from ._misc import MultiBuyOrder from ._base import TraderClientObject -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class BuyResult(TraderClientObject): """Класс, представляющий результат покупки. Attributes: - success (:obj:`bool`): Результат запроса. - id (:obj:`int`): Уникальный ID покупки. - gid (:obj:`int`): ID группы предметов. - itemid (:obj:`int`): ID купленного предмета. - price (:obj:`float`): Цена, за которую был куплен предмет с учётом скидки. - new_price (:obj:`float`): Новая цена лучшего предложения о продаже для варианта покупки Commodity, + success (bool): Результат запроса. + id (int): Уникальный ID покупки. + gid (int): ID группы предметов. + itemid (int): ID купленного предмета. + price (float): Цена, за которую был куплен предмет с учётом скидки. + new_price (float): Новая цена лучшего предложения о продаже для варианта покупки Commodity, если у группы предметов ещё имеются предложения о продаже. Для остальных вариантов покупки будет 0 - discount (:obj:`float`): Размер скидки в процентах, за которую был куплен предмет. - client (Union[:class:`steam_trader.api.api.Client`, :class:`steam_trader.api.api.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + discount (float): Размер скидки в процентах, за которую был куплен предмет. """ success: bool @@ -35,23 +29,15 @@ class BuyResult(TraderClientObject): price: float new_price: float discount: float - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'BuyResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При создании запроса произошла неизвестная ошибка.') case 3: @@ -61,44 +47,34 @@ def de_json( case 5: raise exceptions.NotEnoughMoney('Недостаточно средств.') - data = super(BuyResult, cls).de_json(data) + data = super(BuyResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class BuyOrderResult(TraderClientObject): """Класс, представляющий результат запроса на покупку. Attributes: - success (:obj:`bool`): Результат запроса. - executed (:obj:`int`): Количество исполненных заявок. - placed (:obj:`int`): Количество размещённых на маркет заявок. - client (Union[:class:`steam_trader.api.api.Client`, :class:`steam_trader.api.api.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + executed (int): Количество исполненных заявок. + placed (int): Количество размещённых на маркет заявок. """ success: bool executed: int placed: int - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'BuyOrderResult': del data['orders'] # Конфликт с steam_trader.api.api.BuyOrder if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При создании запроса произошла неизвестная ошибка.') case 2: @@ -110,23 +86,21 @@ def de_json( case 5: raise exceptions.NotEnoughMoney('Недостаточно средств.') - data = super(BuyOrderResult, cls).de_json(data) + data = super(BuyOrderResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class MultiBuyResult(TraderClientObject): """Класс, представляющий результат мульти-покупки. Attributes: - success (:obj:`bool`): Результат запроса. - balance (:obj:`float`, optional): Баланс после покупки предметов. Указывается если success = True - spent (:obj:`float`, optional): Сумма потраченных средств на покупку предметов. Указывается если success = True - orders (Sequence[:class:`steam_trader.api.api.MultiBuyOrder`], optional): + success (bool): Результат запроса. + balance (float, optional): Баланс после покупки предметов. Указывается если success = True + spent (float, optional): Сумма потраченных средств на покупку предметов. Указывается если success = True + orders (Sequence[steam_trader.api.api.MultiBuyOrder], optional): Последовательность купленных предметов. Указывается если success = True - left (:obj:`int`): Сколько предметов по этой цене осталось. Если операция прошла успешно, всегда равен 0. - client (Union[:class:`steam_trader.api.api.Client`, :class:`steam_trader.api.api.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + left (int): Сколько предметов по этой цене осталось. Если операция прошла успешно, всегда равен 0. Changes: 0.2.3: Теперь, если во время операции закончиться баланс, вместо ошибки, @@ -134,27 +108,19 @@ class MultiBuyResult(TraderClientObject): """ success: bool - client: Union['Client', 'ClientAsync', None] balance: Optional[float] = None spent: Optional[float] = None - orders: Optional[Sequence['MultiBuyOrder']] = None + orders: Optional[Sequence[MultiBuyOrder]] = None left: int = 0 @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'MultiBuyResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При создании запроса произошла неизвестная ошибка.') case 2: @@ -167,6 +133,6 @@ def de_json( for i, offer in enumerate(data['orders']): data['orders'][i] = MultiBuyOrder.de_json(offer) - data = super(MultiBuyResult, cls).de_json(data) + data = super(MultiBuyResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) diff --git a/steam_trader/api/_client.py b/steam_trader/api/_client.py index 6b7fcf0..86cda8b 100644 --- a/steam_trader/api/_client.py +++ b/steam_trader/api/_client.py @@ -1,11 +1,11 @@ import httpx import logging -import functools +from functools import wraps from collections.abc import Sequence, Callable -from typing import Optional, LiteralString, Union, TypeVar, Any +from typing import Optional, Literal, Any, Self, TypeVar from steam_trader.constants import SUPPORTED_APPIDS -from steam_trader.exceptions import BadRequestError, WrongTradeLink, SaveFail, UnsupportedAppID, Unauthorized, TooManyRequests +from steam_trader.exceptions import * from ._base import TraderClientObject from ._account import WebSocketToken, Inventory, BuyOrders, Discounts, OperationsHistory, InventoryState, AltWebSocket from ._buy import BuyResult, BuyOrderResult, MultiBuyResult @@ -22,8 +22,8 @@ def log(method: F) -> F: logger = logging.getLogger(method.__module__) - @functools.wraps(method) - def wrapper(*args, **kwargs) -> Any: + @wraps(method) + def wrapper(*args, **kwargs): logger.debug(f'Entering: {method.__name__}') result = method(*args, **kwargs) @@ -33,37 +33,57 @@ def wrapper(*args, **kwargs) -> Any: return result - return wrapper + return wrapper # type: ignore class Client(TraderClientObject): """Класс, представляющий клиент Steam Trader. Args: - api_token (:obj:`str`): Уникальный ключ для аутентификации. - proxy (:obj:`str`, optional): Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. - headers (:obj:`dict`, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. **kwargs: Будут переданы httpx клиенту. Например timeout. Attributes: - api_token (:obj:`str`): Уникальный ключ для аутентификации. - proxy (:obj:`str`, optional): Прокси для запросов. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. - headers (:obj:`dict`, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. + + Usage: - Raises: - BadRequestError: Неправильный запрос. - Unauthorized: Неправильный api-токен. - TooManyRequests: Слишком много запросов. + ```python + from steam_trader.api import Client + + client = Client('Ваш токен') + ... + + # или + + with client: + ... + ``` + + ```python + from steam_trader.api import ClientAsync + + client = ClientAsync('Ваш токен') + + async def main(): + async with client: + ... + ``` """ __slots__ = [ - 'sessionid', 'proxy', - 'base_url' + 'api_token', + 'base_url', + 'headers' ] def __init__( @@ -87,40 +107,120 @@ def __init__( 'user-agent': 'python3', 'wrapper': 'SteamTrader-Wrapper', 'manufacturer': 'Lemon4ksan', - "Api-Key": self.api_token + 'Api-Key': self.api_token } self.headers = headers self._httpx_client = None + self._kwargs = kwargs self.proxy = proxy - self.kwargs = kwargs - def __enter__(self) -> 'Client': - self._httpx_client = httpx.Client(proxy=self.proxy, **self.kwargs) + def __enter__(self) -> Self: + self._httpx_client = httpx.Client(proxy=self.proxy, **self._kwargs) return self - def __exit__(self, exc_type, exc_val, exc_tb): - self._httpx_client.close() + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + if self._httpx_client: + self._httpx_client.close() + + def _get_request( + self, + method: str, + *, + headers: Optional[dict[str, str]] = None, + params: Optional[dict[str, Any]] = None, + cookies: Optional[dict[str, str]] = None, + **kwargs + ) -> Any: + """Создать GET запрос и вернуть данные. - @property - def balance(self) -> float: - """Баланс клиента.""" + Args: + method (str): API метод. + headers (dict[str, str], optional): Заголовки запроса. + params (dict[str, Any], optional): Параметры запроса. + cookies (dict[str, str], optional): Куки запроса. + **kwargs: Будут переданы httpx клиенту. - url = self.base_url + 'getbalance/' + Returns: + Any: Ответ сервера. + """ + if headers is None: + headers = self.headers + + url: str = self.base_url + method result = (self._httpx_client or httpx).get( url, - headers=self.headers + headers=headers, + params=params, + cookies=cookies, + **kwargs + ) + result = result.json() + + try: + if not result['success']: + match result['code']: + case 400: + raise BadRequestError('Неправильный запрос.') + case 401: + raise Unauthorized('Неправильный api-токен.') + case 429: + raise TooManyRequests('Вы отправили слишком много запросов.') + except KeyError: + pass + + return result + + def _post_request( + self, + method: str, + *, + data: Optional[dict[str, Any]] = None + ) -> Any: + """Создать POST запрос, обработать базовые исключения и вернуть данные. + + Args: + method (str): API метод. + data (dict[str, Any], optional): Параметры для POST запроса. + + Raises: + BadRequestError: Неправильный запрос. + Unauthorized: Неправильный api-токен. + TooManyRequests: Слишком много запросов. + + Returns: + Any: Ответ сервера. + """ + + url: str = self.base_url + method + result = (self._httpx_client or httpx).post( + url, + headers=self.headers, + data=data ).json() - if not result['success']: - match result['code']: - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') + + try: + if not result['success']: + match result['code']: + case 400: + raise BadRequestError('Неправильный запрос.') + case 401: + raise Unauthorized('Неправильный api-токен.') + case 429: + raise TooManyRequests('Вы отправили слишком много запросов.') + except KeyError: + pass + + return result + + @property + def balance(self) -> float: + """Баланс клиента.""" + result = self._get_request('getbalance/') return result['balance'] @log - def sell(self, itemid: int, assetid: int, price: float) -> 'SellResult': + def sell(self, itemid: int, assetid: int, price: float) -> SellResult: """Создать предложение о продаже определённого предмета. Note: @@ -130,15 +230,15 @@ def sell(self, itemid: int, assetid: int, price: float) -> 'SellResult': (дешевле), то сделка совершится по цене 10 ₽. Args: - itemid (:obj:`int`): Уникальный ID предмета. - assetid (:obj:`int`): AssetID предмета в Steam (найти их можно через get_inventory). - price (:obj:`float`): Цена, за которую хотите продать предмет без учёта комиссии/скидки. + itemid (int): Уникальный ID предмета. + assetid (int): AssetID предмета в Steam (найти их можно через get_inventory). + price (float): Цена, за которую хотите продать предмет без учёта комиссии/скидки. Returns: - :class:`steam_trader.SellResult`: Результат создания предложения о продаже. + SellResult: Результат создания предложения о продаже. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. NoTradeLink: Отсутствует сслыка для обмена. IncorrectPrice: Неправильная цена заявки. @@ -146,17 +246,11 @@ def sell(self, itemid: int, assetid: int, price: float) -> 'SellResult': AuthenticatorError: Мобильный аутентификатор не подключён или с момента его подключения ещё не прошло 7 дней. """ - - url = self.base_url + 'sale/' - result = (self._httpx_client or httpx).post( - url, - data={"itemid": itemid, "assetid": assetid, "price": price}, - headers=self.headers - ).json() - return SellResult.de_json(result, self) + result = self._post_request('sale/', data={"itemid": itemid, "assetid": assetid, "price": price}) + return SellResult.de_json(result) @log - def buy(self, _id: Union[int, str], _type: int, price: float, currency: int = 1) -> 'BuyResult': + def buy(self, itemid: int | str, itemtype: Literal[1, 2, 3], price: float, currency: Literal[1] = 1) -> BuyResult: """Создать предложение о покупке предмета по строго указанной цене. Если в момент покупки цена предложения о продаже изменится, покупка не совершится. @@ -165,38 +259,31 @@ def buy(self, _id: Union[int, str], _type: int, price: float, currency: int = 1) Сайт пока работает только с рублями. Не меняйте значение currency. Args: - _id (:obj:`int | :obj: str`): В качества ID может выступать: + itemid (int | str): В качества ID может выступать: GID для варианта покупки Commodity. Часть ссылки после nc/ (nc/L8RJI7XR96Mmo3Bu) для варианта покупки NoCommission. ID предложения о продаже для варианта покупки Offer (найти их можно в ItemInfo). - _type (:obj:`int`): Вариант покупки (указаны выше) - 1 / 2 / 3. - price (:obj:`float`): Цена предложения о продаже без учёта комиссии/скидки. + itemtype (int): Вариант покупки (указаны выше) - 1 / 2 / 3. + price (float): Цена предложения о продаже без учёта комиссии/скидки. Актуальные цены можно узнать через get_item_info и get_min_prices. - currency (:obj:`int`): Валюта покупки. Значение 1 - рубль. + currency (int): Валюта покупки. Значение 1 - рубль. Returns: - :class:`steam_trader.BuyResult`: Результат создания запроса о покупке. + BuyResult: Результат создания запроса о покупке. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. NoTradeLink: Отсутствует сслыка для обмена. NoLongerExists: Предложение больше недействительно. NotEnoughMoney: Недостаточно средств. """ - - if _type not in range(1, 4): - logging.warning(f'Неправильное значение _type >> {_type}') - - url = self.base_url + 'buy/' - result = (self._httpx_client or httpx).post( - url, - data={"id": _id, "type": _type, "price": price, "currency": currency}, - headers=self.headers - ).json() - return BuyResult.de_json(result, self) + if itemtype not in range(1, 4): + logging.warning(f"Неправильное значение _type '{itemtype}'") + result = self._post_request('buy/', data={"id": itemid, "type": itemtype, "price": price, "currency": currency}) + return BuyResult.de_json(result) @log - def create_buy_order(self, gid: int, price: float, *, count: int = 1) -> 'BuyOrderResult': + def create_buy_order(self, gid: int, price: float, *, count: int = 1) -> BuyOrderResult: """Создать заявку на покупку предмета с определённым GID. Note: @@ -206,34 +293,27 @@ def create_buy_order(self, gid: int, price: float, *, count: int = 1) -> 'BuyOrd (дешевле), то сделка совершится по цене 10 ₽. Args: - gid (:obj:`int`): ID группы предметов. - price (:obj:`float`): Цена предмета, за которую будете его покупать без учёта комиссии/скидки. - count (:obj:`int`): Количество заявок для размещения (не более 500). По умолчанию - 1. + gid (int): ID группы предметов. + price (float): Цена предмета, за которую будете его покупать без учёта комиссии/скидки. + count (int): Количество заявок для размещения (не более 500). По умолчанию - 1. Returns: - :class:`steam_trader.BuyOrderResult`: Результат созданния заявки на покупку. + BuyOrderResult: Результат созданния заявки на покупку. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. NoTradeLink: Отсутствует сслыка для обмена. NoLongerExists: Предложение больше недействительно. NotEnoughMoney: Недостаточно средств. """ - if not 1 <= count <= 500: - logging.warning(f'Количество заявок должно быть от 1 до 500 (не {count})') - - url = self.base_url + 'createbuyorder/' - result = (self._httpx_client or httpx).post( - url, - data={"gid": gid, "price": price, "count": count}, - headers=self.headers - ).json() - return BuyOrderResult.de_json(result, self) + logging.warning(f"Количество заявок должно быть от 1 до 500 (не '{count}')") + result = self._post_request('createbuyorder/', data={"gid": gid, "price": price, "count": count}) + return BuyOrderResult.de_json(result) @log - def multi_buy(self, gid: int, max_price: float, count: int) -> 'MultiBuyResult': + def multi_buy(self, gid: int, max_price: float, count: int) -> MultiBuyResult: """Создать запрос о покупке нескольких предметов с определённым GID. Будут куплены самые лучшие (дешёвые) предложения о продаже. @@ -247,15 +327,15 @@ def multi_buy(self, gid: int, max_price: float, count: int) -> 'MultiBuyResult': success будет равен False и будет указано кол-во оставшихся предметов по данной цене. Args: - gid (:obj:`int`): ID группы предметов. - max_price (:obj:`float`): Максимальная цена одного предмета без учета комиссии/скидки. - count (:obj:`int`): Количество предметов для покупки. + gid (int): ID группы предметов. + max_price (float): Максимальная цена одного предмета без учета комиссии/скидки. + count (int): Количество предметов для покупки. Returns: - :class:`steam_trader.MultiBuyResult`: Результат создания запроса на мульти-покупку. + MultiBuyResult: Результат создания запроса на мульти-покупку. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. NoTradeLink: Отсутствует сслыка для обмена. NotEnoughMoney: Недостаточно средств. @@ -263,28 +343,22 @@ def multi_buy(self, gid: int, max_price: float, count: int) -> 'MultiBuyResult': 0.2.3: Теперь, если во время операции закончиться баланс, вместо ошибки, в датаклассе будет указано кол-во оставшихся предметов по данной цене. """ - - url = self.base_url + 'multibuy/' - result = (self._httpx_client or httpx).post( - url, - data={"gid": gid, "max_price": max_price, "count": count}, - headers=self.headers - ).json() - return MultiBuyResult.de_json(result, self) + result = self._post_request('multibuy/', data={"gid": gid, "max_price": max_price, "count": count}) + return MultiBuyResult.de_json(result) @log - def edit_price(self, _id: int, price: float) -> 'EditPriceResult': + def edit_price(self, itemid: int, price: float) -> 'EditPriceResult': """Редактировать цену предмета/заявки на покупку. При редактировании может произойти моментальная продажа/покупка по аналогии тому, как это сделано в методах sell и create_buy_order. Args: - _id (:obj:`int`): ID предложения о продаже/заявки на покупку. - price (:obj:`float`): Новая цена, за которую хотите продать/купить предмет без учёта комиссии/скидки. + itemid (int): ID предложения о продаже/заявки на покупку. + price (float): Новая цена, за которую хотите продать/купить предмет без учёта комиссии/скидки. Returns: - :class:`steam_trader.EditPriceResult`: Результат запроса на изменение цены. + EditPriceResult: Результат запроса на изменение цены. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -292,51 +366,39 @@ def edit_price(self, _id: int, price: float) -> 'EditPriceResult': IncorrectPrice: Неправильная цена заявки. NotEnoughMoney: Недостаточно средств. """ - - url = self.base_url + 'editprice/' - result = (self._httpx_client or httpx).post( - url, - data={"id": _id, "price": price}, - headers=self.headers - ).json() - return EditPriceResult.de_json(result, self) + result = self._post_request('editprice/', data={"id": itemid, "price": price}) + return EditPriceResult.de_json(result) @log - def delete_item(self, _id: int) -> 'DeleteItemResult': + def delete_item(self, itemid: int) -> DeleteItemResult: """Снять предмет с продажи/заявку на покупку. Args: - _id (:obj:`int`): ID продажи/заявки на покупку. + itemid (int): ID продажи/заявки на покупку. Returns: - :class:`steam_trader.DeleteItemResult`: Результат запроса снятия предмета + DeleteItemResult: Результат запроса снятия предмета с продажи/заявки на покупку. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. """ - - url = self.base_url + 'deleteitem/' - result = (self._httpx_client or httpx).post( - url, - data={"id": _id}, - headers=self.headers - ).json() - return DeleteItemResult.de_json(result, self) + result = self._post_request('deleteitem/', data={"id": itemid}) + return DeleteItemResult.de_json(result) @log - def get_down_orders(self, gameid: int, *, order_type: LiteralString = 'sell') -> 'GetDownOrdersResult': + def get_down_orders(self, gameid: int, *, order_type: Literal['sell', 'buy'] = 'sell') -> GetDownOrdersResult: """Снять все заявки на продажу/покупку предметов. Args: - gameid (:obj:`int`): AppID приложения в Steam. - order_type (:obj:`LiteralString`): Тип заявок для удаления: + gameid (int): AppID приложения в Steam. + order_type (str): Тип заявок для удаления: "sell" - предложения о ПРОДАЖЕ. Значение по умолчанию. "buy" - предложения о ПОКУПКЕ. Returns: - :class:`steam_trader.GetDownOrdersResult`: Результат снятия всех заявок + GetDownOrdersResult: Результат снятия всех заявок на продажу/покупку предметов. Raises: @@ -345,42 +407,30 @@ def get_down_orders(self, gameid: int, *, order_type: LiteralString = 'sell') -> UnsupportedAppID: Указан недействительный gameid. ValueError: Указано недопустимое значение order_type. """ - if gameid not in SUPPORTED_APPIDS: - raise UnsupportedAppID(f'Игра с AppID {gameid} в данный момент не поддерживается.') - + raise UnsupportedAppID(f"Игра с AppID '{gameid}' в данный момент не поддерживается.") if order_type not in ['sell', 'buy']: - logging.warning(f'Неизвестный тип >> {order_type}') + logging.warning(f"Неизвестный тип '{order_type}'") - url = self.base_url + 'getdownorders/' - result = (self._httpx_client or httpx).post( - url, - data={"gameid": gameid, "type": order_type}, - headers=self.headers - ).json() - return GetDownOrdersResult.de_json(result, self) + result = self._post_request('getdownorders/', data={"gameid": gameid, "type": order_type}) + return GetDownOrdersResult.de_json(result) @log - def get_items_for_exchange(self) -> 'ItemsForExchange': + def get_items_for_exchange(self) -> ItemsForExchange: """Получить список предметов для обмена с ботом. Returns: - :class:`steam_trader.ItemsForExchange`: Cписок предметов для обмена с ботом. + ItemsForExchange: Cписок предметов для обмена с ботом. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. NoTradeItems: Нет предметов для обмена. """ - - url = self.base_url + 'itemsforexchange/' - result = (self._httpx_client or httpx).get( - url, - headers=self.headers - ).json() - return ItemsForExchange.de_json(result, self) + result = self._get_request('itemsforexchange/') + return ItemsForExchange.de_json(result) @log - def exchange(self) -> 'ExchangeResult': + def exchange(self) -> ExchangeResult: """Выполнить обмен с ботом. Note: @@ -388,7 +438,7 @@ def exchange(self) -> 'ExchangeResult': В противном случае трейд будет отменён. Returns: - :class:`steam_trader.ExchangeResult`: Результат обмена с ботом. + ExchangeResult: Результат обмена с ботом. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -403,36 +453,25 @@ def exchange(self) -> 'ExchangeResult': AuthenticatorError: Мобильный аутентификатор не подключён, или с момента его подключения ещё не прошло 7 дней. """ - - url = self.base_url + 'exchange/' - result = (self._httpx_client or httpx).get( - url, - headers=self.headers - ).json() - return ExchangeResult.de_json(result, self) + result = self._get_request('exchange/') + return ExchangeResult.de_json(result) @log - def get_items_for_exchange_p2p(self) -> 'ItemsForExchange': + def get_items_for_exchange_p2p(self) -> ItemsForExchange: """Получить список предметов для p2p обмена. Returns: - :class:`steam_trader.ItemsForExchange`: Cписок предметов для p2p обмена. + ItemsForExchange: Cписок предметов для p2p обмена. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. NoTradeItems: Нет предметов для обмена. """ - - url = self.base_url + 'itemsforexchangep2p/' - result = (self._httpx_client or httpx).get( - url, - headers=self.headers - ).json() - - return ItemsForExchange.de_json(result, self) + result = self._get_request('itemsforexchangep2p/') + return ItemsForExchange.de_json(result) @log - def exchange_p2p(self) -> 'ExchangeP2PResult': + def exchange_p2p(self) -> ExchangeP2PResult: """Выполнить p2p обмен. Note: @@ -440,7 +479,7 @@ def exchange_p2p(self) -> 'ExchangeP2PResult': В противном случае, трейд будет отменён. Returns: - :class:`steam_trader.ExchangeP2PResult`: Результат p2p обмена. + ExchangeP2PResult: Результат p2p обмена. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -453,114 +492,86 @@ def exchange_p2p(self) -> 'ExchangeP2PResult': AuthenticatorError: Мобильный аутентификатор не подключён, или с момента его подключения ещё не прошло 7 дней. """ - - url = self.base_url + 'exchange/' - result = (self._httpx_client or httpx).get( - url, - headers=self.headers - ).json() - - return ExchangeP2PResult.de_json(result, self) + result = self._get_request('exchange/') + return ExchangeP2PResult.de_json(result) @log - def get_min_prices(self, gid: int, currency: int = 1) -> 'MinPrices': + def get_min_prices(self, gid: int, currency: Literal[1] = 1) -> MinPrices: """Получить минимальные/максимальные цены предмета. Note: Сайт пока работает только с рублями. Не меняйте значение currency. Args: - gid (:obj:`int`): ID группы предметов. - currency (:obj:`int`): Валюта, значение 1 - рубль. + gid (int): ID группы предметов. + currency (int): Валюта, значение 1 - рубль. Returns: - :class:`steam_trader.MinPrices`: Минимальные/максимальные цены предмета. + steam_trader.MinPrices: Минимальные/максимальные цены предмета. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. """ - - url = self.base_url + "getminprices/" - result = (self._httpx_client or httpx).get( - url, - params={"gid": gid, "currency": currency}, - headers=self.headers - ).json() - return MinPrices.de_json(result, self) + result = self._get_request('getminprices/', params={"gid": gid, "currency": currency}) + return MinPrices.de_json(result) @log - def get_item_info(self, gid: int) -> 'ItemInfo': + def get_item_info(self, gid: int) -> ItemInfo: """Получить информацию о группе предметов. Args: - gid (:obj:`int`): ID группы предметов. + gid (int): ID группы предметов. Returns: - :class:`steam_trader.ItemInfo`: Информация о группе предметов. + ItemInfo: Информация о группе предметов. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. """ - - url = self.base_url + "iteminfo/" - result = (self._httpx_client or httpx).get( - url, - params={"gid": gid}, - headers=self.headers - ).json() - return ItemInfo.de_json(result, self) + result = self._get_request('iteminfo/', params={"gid": gid}) + return ItemInfo.de_json(result) @log - def get_order_book(self, gid: int, *, mode: LiteralString = 'all', limit: Optional[int] = None) -> 'OrderBook': + def get_order_book(self, gid: int, *, mode: Literal['all', 'sell', 'buy'] = 'all', limit: Optional[int] = None) -> OrderBook: """Получить заявки о покупке/продаже предмета. Args: - gid (:obj:`int`): ID группы предметов. - mode (:obj:`LiteralString`): Режим отображения + gid (int): ID группы предметов. + mode (str): Режим отображения 'all' - отображать покупки и продажи. Значение по умолчанию. 'sell' - отображать только заявки на ПРОДАЖУ. 'buy' - отображать только заявки на ПОКУПКУ. - limit (:obj:`int`, optional): Максимальное количество строк в списке. По умолчанию - неограниченно + limit (int, optional): Максимальное количество строк в списке. По умолчанию - неограниченно Returns: - :class:`steam_trader.OrderBook`: Заявки о покупке/продаже предмета. + OrderBook: Заявки о покупке/продаже предмета. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. """ - if mode not in ['all', 'sell', 'buy']: - logging.warning(f'Неизвестный режим >> {mode}') + logging.warning(f"Неизвестный режим '{mode}'") - url = self.base_url + "orderbook/" - result = (self._httpx_client or httpx).get( - url, - params={"gid": gid, "mode": mode, "limit": limit}, - headers=self.headers - ).json() - return OrderBook.de_json(result, self) + result = self._get_request('orderbook/', params={"gid": gid, "mode": mode, "limit": limit}) + return OrderBook.de_json(result) @log - def get_web_socket_token(self) -> 'WebSocketToken': + def get_web_socket_token(self) -> WebSocketToken: """Получить токен для авторизации в WebSocket. Незадокументированно.""" - url = self.base_url + "getwstoken/" - result = (self._httpx_client or httpx).get( - url, - params={'key': self.api_token} - ).json() - return WebSocketToken.de_json(result, self) + result = self._get_request('getwstoken/', params={'key': self.api_token}) + return WebSocketToken.de_json(result) @log - def get_inventory(self, gameid: int, *, status: Optional[Sequence[int]] = None) -> 'Inventory': + def get_inventory(self, gameid: int, *, status: Optional[Sequence[Literal[0, 1, 2, 3, 4]]] = None) -> Inventory: """Получить инвентарь клиента, включая заявки на покупку и купленные предметы. По умолчанию возвращает список предметов из инвентаря Steam, которые НЕ выставлены на продажу. Args: - gameid (:obj:`int`): AppID приложения в Steam. - status (Sequence[:obj:`int`], optional): + gameid (int): AppID приложения в Steam. + status (Sequence[int], optional): Указывается, чтобы получить список предметов с определенным статусом. Возможные статусы: @@ -571,17 +582,14 @@ def get_inventory(self, gameid: int, *, status: Optional[Sequence[int]] = None) 4 - Заявка на покупку Returns: - :class:`steam_trader.Inventory`: Инвентарь клиента, включая заявки на покупку и купленные предметы. + Inventory: Инвентарь клиента, включая заявки на покупку и купленные предметы. Raises: UnsupportedAppID: Указан недействительный gameid. ValueError: Указан недопустимый статус. """ - if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'getinventory/' params = {"gameid": gameid} if status is not None: @@ -590,32 +598,27 @@ def get_inventory(self, gameid: int, *, status: Optional[Sequence[int]] = None) raise ValueError(f'Неизвестный статус {s}') params[f'status[{i}]'] = s - result = (self._httpx_client or httpx).get( - url, - params=params, - headers=self.headers - ).json() - return Inventory.de_json(result, status, self) + result = self._get_request('getinventory/', params=params) + return Inventory.de_json(result, status) @log - def get_buy_orders(self, *, gameid: Optional[int] = None, gid: Optional[int] = None) -> 'BuyOrders': + def get_buy_orders(self, *, gameid: Optional[int] = None, gid: Optional[int] = None) -> BuyOrders: """Получить последовательность заявок на покупку. По умолчанию возвращаются заявки для всех предметов из всех разделов. При указании соответствующих параметров можно получить заявки из определённого раздела и/или предмета. Args: - gameid (:obj:`int`, optional): AppID приложения в Steam. - gid (:obj:`int`, optional): ID группы предметов. + gameid (int, optional): AppID приложения в Steam. + gid (int, optional): ID группы предметов. Returns: - :class:`steam_trader.BuyOrders`: Список заявок на покупку. + BuyOrders: Список заявок на покупку. Raises: UnsupportedAppID: Указан недействительный gameid. NoBuyOrders: Нет запросов на покупку. """ - if gameid is not None and gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') @@ -625,62 +628,39 @@ def get_buy_orders(self, *, gameid: Optional[int] = None, gid: Optional[int] = N if gid is not None: params['gid'] = gid - url = self.base_url + 'getbuyorders/' - result = (self._httpx_client or httpx).get( - url, - params=params, - headers=self.headers - ).json() - return BuyOrders.de_json(result, self) + result = self._get_request('getbuyorders/', params=params) + return BuyOrders.de_json(result) @log - def get_discounts(self) -> 'Discounts': + def get_discounts(self) -> Discounts: """Получить комиссии/скидки и оборот на сайте. Данные хранятся в словаре data, где ключ - это AppID игры в Steam (См. steam_trader.constants). Returns: - :class:`steam_trader.Discounts`: Комиссии/скидки и оборот на сайте. + Discounts: Комиссии/скидки и оборот на сайте. """ - - url = self.base_url + 'getdiscounts/' - result = (self._httpx_client or httpx).get( - url, - headers=self.headers - ).json() - return Discounts.de_json(result, self) + result = self._get_request('getdiscounts/') + return Discounts.de_json(result) @log def set_trade_link(self, trade_link: str) -> None: """Установить ссылку для обмена. Args: - trade_link (:obj:`str`): Ссылка для обмена, + trade_link (str): Ссылка для обмена, Например, https://steamcommunity.com/tradeoffer/new/?partner=453486961&token=ZhXMbDS9 Raises: SaveFail: Не удалось сохранить ссылку обмена. WrongTradeLink: Указана ссылка для обмена от другого Steam аккаунта ИЛИ ссылка для обмена уже указана. """ - - url = self.base_url + 'settradelink/' - result = (self._httpx_client or httpx).post( - url, - data={"trade_link": trade_link}, - headers=self.headers - ).json() - + result = self._post_request('settradelink/', data={"trade_link": trade_link}) if not result['success']: try: match result['code']: - case 400: - raise BadRequestError('Неправильный запрос.') - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') case 1: - raise SaveFail('Не удалось сохранить ссылку обмена.') + raise SaveFail('Не удалось сохранить ссылку для обмена.') except KeyError: raise WrongTradeLink('Указана ссылка для обмена от другого Steam аккаунта ИЛИ ссылка для обмена уже указана.') @@ -691,29 +671,18 @@ def remove_trade_link(self) -> None: Raises: SaveFail: Не удалось удалить ссылку обмена. """ - - url = self.base_url + 'removetradelink/' - result = (self._httpx_client or httpx).post( - url, - data={"trade_link": "1"}, - headers=self.headers - ).json() - + result = self._post_request('removetradelink/', data={"trade_link": "1"}) if not result['success']: match result['code']: - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') case 1: raise SaveFail('Не удалось удалить ссылку обмена.') @log - def get_operations_history(self, *, operation_type: Optional[int] = None, page: int = 0) -> 'OperationsHistory': + def get_operations_history(self, *, operation_type: Optional[Literal[1, 2, 3, 4, 5, 9, 10]] = None, page: int = 0) -> OperationsHistory: """Получить историю операций (По умолчанию все типы). В каждой странице до 100 пунктов. Args: - operation_type (:obj:`int`, optional): Тип операции. Может быть пустым. + operation_type (int, optional): Тип операции. Может быть пустым. 1 - Покупка предмета 2 - Продажа предмета 3 - Возврат за покупку @@ -721,95 +690,60 @@ def get_operations_history(self, *, operation_type: Optional[int] = None, page: 5 - Вывести средства 9 - Ожидание покупки 10 - Штрафной балл - page (:obj:`int`): Страница операций. Отсчёт начинается с 0. + page (int): Страница операций. Отсчёт начинается с 0. Returns: - :class:`steam_trader.OperationsHistory`: История операций. + OperationsHistory: История операций. Changes: 0.3.0: Добавлен аргумент page. """ - - if operation_type is not None and operation_type not in range(1, 11): - logging.warning(f'Неизвестный тип {operation_type}') - - url = self.base_url + 'operationshistory/' - result = (self._httpx_client or httpx).get( - url, - params={"type": operation_type, "page": page}, - headers=self.headers - ).json() - return OperationsHistory.de_json(result, self) + if operation_type is not None and operation_type not in (1, 2, 3, 4, 5, 9, 10): + logging.warning(f"Неизвестный тип '{operation_type}'") + result = self._get_request('operationshistory/', params={"type": operation_type, "page": page}) + return OperationsHistory.de_json(result) @log def update_inventory(self, gameid: int) -> None: """Обновить инвентарь игры на сайте. Args: - gameid (:obj:`int`): AppID приложения в Steam. + gameid (int): AppID приложения в Steam. Raises: UnsupportedAppID: Указан недействительный gameid. """ - if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'updateinventory/' - result = (self._httpx_client or httpx).get( - url, - params={"gameid": gameid}, - headers=self.headers - ).json() - - if not result['success']: - match result['code']: - case 400: - raise BadRequestError('Неправильный запрос.') - case 401: - raise Unauthorized('Неправильный api-токен') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') + self._get_request('updateinventory/', params={"gameid": gameid}) @log - def get_inventory_state(self, gameid: int) -> 'InventoryState': + def get_inventory_state(self, gameid: int) -> InventoryState: """Получить текущий статус обновления инвентаря. Args: - gameid (:obj:`int`): AppID приложения в Steam. + gameid (int): AppID приложения в Steam. Returns: - :class:`steam_trader.InventoryState`: Текущий статус обновления инвентаря. + InventoryState: Текущий статус обновления инвентаря. Raises: UnsupportedAppID: Указан недействительный gameid. """ - if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'inventorystate/' - result = (self._httpx_client or httpx).get( - url, - params={"gameid": gameid}, - headers=self.headers - ).json() - return InventoryState.de_json(result, self) + result = self._get_request('inventorystate/', params={"gameid": gameid}) + return InventoryState.de_json(result) @log - def trigger_alt_web_socket(self) -> Optional['AltWebSocket']: + def trigger_alt_web_socket(self) -> Optional[AltWebSocket]: """Создать запрос альтернативным WebSocket. Для поддержания активного соединения нужно делать этот запрос каждые 2 минуты. Возвращает None если новых сообщений нет. При этом соединение будет поддрежано. Returns: - :class:`steam_trader.AltWebSocket`, optional: Запрос альтернативным WebSocket. + AltWebSocket, optional: Запрос альтернативным WebSocket. """ - - url = self.base_url + 'altws/' - result = (self._httpx_client or httpx).get( - url, - headers=self.headers - ).json() - return AltWebSocket.de_json(result, self) + result = self._get_request('altws/') + return AltWebSocket.de_json(result) diff --git a/steam_trader/api/_client_async.py b/steam_trader/api/_client_async.py index 85714bb..0e83e5f 100644 --- a/steam_trader/api/_client_async.py +++ b/steam_trader/api/_client_async.py @@ -1,11 +1,15 @@ +###################################################################################### +# ЭТО АВТОМАТИЧЕСКИ СОЗДАННАЯ КОПИЯ СИНХРОННОГО КЛИЕНТА. НЕ ИЗМЕНЯЙТЕ САМОСТОЯТЕЛЬНО # +###################################################################################### + import httpx import logging -import functools +from functools import wraps from collections.abc import Sequence, Callable -from typing import Optional, LiteralString, Union, TypeVar, Any +from typing import Optional, Literal, Any, Self, TypeVar from steam_trader.constants import SUPPORTED_APPIDS -from steam_trader.exceptions import BadRequestError, WrongTradeLink, SaveFail, UnsupportedAppID, Unauthorized, TooManyRequests +from steam_trader.exceptions import * from ._base import TraderClientObject from ._account import WebSocketToken, Inventory, BuyOrders, Discounts, OperationsHistory, InventoryState, AltWebSocket from ._buy import BuyResult, BuyOrderResult, MultiBuyResult @@ -22,8 +26,8 @@ def log(method: F) -> F: logger = logging.getLogger(method.__module__) - @functools.wraps(method) - async def wrapper(*args, **kwargs) -> Any: + @wraps(method) + async def wrapper(*args, **kwargs): logger.debug(f'Entering: {method.__name__}') result = await method(*args, **kwargs) @@ -33,37 +37,57 @@ async def wrapper(*args, **kwargs) -> Any: return result - return wrapper + return wrapper # type: ignore class ClientAsync(TraderClientObject): """Класс, представляющий клиент Steam Trader. Args: - api_token (:obj:`str`): Уникальный ключ для аутентификации. - proxy (:obj:`str`, optional): Прокси для запросов. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. - headers (:obj:`dict`, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. **kwargs: Будут переданы httpx клиенту. Например timeout. Attributes: - api_token (:obj:`str`): Уникальный ключ для аутентификации. - proxy (:obj:`str`, optional): Прокси для запросов. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. - headers (:obj:`dict`, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. + + Usage: + + ```python + from steam_trader.api import Client + + client = Client('Ваш токен') + ... + + # или + + with client: + ... + ``` - Raises: - BadRequestError: Неправильный запрос. - Unauthorized: Неправильный api-токен. - TooManyRequests: Слишком много запросов. + ```python + from steam_trader.api import ClientAsync + + client = ClientAsync('Ваш токен') + + async def main(): + async with client: + ... + ``` """ __slots__ = [ - 'sessionid', 'proxy', - 'base_url' + 'api_token', + 'base_url', + 'headers' ] def __init__( @@ -87,40 +111,126 @@ def __init__( 'user-agent': 'python3', 'wrapper': 'SteamTrader-Wrapper', 'manufacturer': 'Lemon4ksan', - "Api-Key": self.api_token + 'Api-Key': self.api_token } self.headers = headers + self._httpx_client = None + self._kwargs = kwargs self.proxy = proxy - self.kwargs = kwargs - async def __aenter__(self) -> 'ClientAsync': - self._async_client = httpx.AsyncClient(proxy=self.proxy, **self.kwargs) + async def __aenter__(self) -> Self: + self._httpx_client = httpx.AsyncClient(proxy=self.proxy, **self._kwargs) return self - async def __aexit__(self, exc_type, exc_val, exc_tb): - await self._async_client.aclose() + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + if self._httpx_client: + await self._httpx_client.aclose() + + async def _get_request( + self, + method: str, + *, + headers: Optional[dict[str, str]] = None, + params: Optional[dict[str, Any]] = None, + cookies: Optional[dict[str, str]] = None, + **kwargs + ) -> Any: + """Создать GET запрос и вернуть данные. - @property - async def balance(self) -> float: - """Баланс клиента.""" + Args: + method (str): API метод. + headers (dict[str, str], optional): Заголовки запроса. + params (dict[str, Any], optional): Параметры запроса. + cookies (dict[str, str], optional): Куки запроса. + **kwargs: Будут переданы httpx клиенту. - url = self.base_url + 'getbalance/' - result = await self._async_client.get( + Returns: + Any: Ответ сервера. + """ + if headers is None: + headers = self.headers + + if not self._httpx_client: + raise ClientError('Необходимо использовать контекст async with ClientAsync()') + url: str = self.base_url + method + result = await self._httpx_client.get( url, - headers=self.headers + headers=headers, + params=params, + cookies=cookies, + **kwargs ) result = result.json() - if not result['success']: - match result['code']: - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') + + try: + if not result['success']: + match result['code']: + case 400: + raise BadRequestError('Неправильный запрос.') + case 401: + raise Unauthorized('Неправильный api-токен.') + case 429: + raise TooManyRequests('Вы отправили слишком много запросов.') + except KeyError: + pass + + return result + + async def _post_request( + self, + method: str, + *, + data: Optional[dict[str, Any]] = None + ) -> Any: + """Создать POST запрос, обработать базовые исключения и вернуть данные. + + Args: + method (str): API метод. + data (dict[str, Any], optional): Параметры для POST запроса. + + Raises: + BadRequestError: Неправильный запрос. + Unauthorized: Неправильный api-токен. + TooManyRequests: Слишком много запросов. + + Returns: + Any: Ответ сервера. + """ + + if not self._httpx_client: + raise ClientError('Необходимо использовать контекст async with ClientAsync()') + url: str = self.base_url + method + if not self._httpx_client: + raise ClientError('Необходимо использовать контекст async with ClientAsync()') + result = (await self._httpx_client.post( + url, + headers=self.headers, + data=data + )).json() + + try: + if not result['success']: + match result['code']: + case 400: + raise BadRequestError('Неправильный запрос.') + case 401: + raise Unauthorized('Неправильный api-токен.') + case 429: + raise TooManyRequests('Вы отправили слишком много запросов.') + except KeyError: + pass + + return result + + @property + async def balance(self) -> float: + """Баланс клиента.""" + result = await self._get_request('getbalance/') return result['balance'] @log - async def sell(self, itemid: int, assetid: int, price: float) -> 'SellResult': + async def sell(self, itemid: int, assetid: int, price: float) -> SellResult: """Создать предложение о продаже определённого предмета. Note: @@ -130,15 +240,15 @@ async def sell(self, itemid: int, assetid: int, price: float) -> 'SellResult': (дешевле), то сделка совершится по цене 10 ₽. Args: - itemid (:obj:`int`): Уникальный ID предмета. - assetid (:obj:`int`): AssetID предмета в Steam (найти их можно через get_inventory). - price (:obj:`float`): Цена, за которую хотите продать предмет без учёта комиссии/скидки. + itemid (int): Уникальный ID предмета. + assetid (int): AssetID предмета в Steam (найти их можно через get_inventory). + price (float): Цена, за которую хотите продать предмет без учёта комиссии/скидки. Returns: - :class:`steam_trader.SellResult`: Результат создания предложения о продаже. + SellResult: Результат создания предложения о продаже. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. NoTradeLink: Отсутствует сслыка для обмена. IncorrectPrice: Неправильная цена заявки. @@ -146,17 +256,11 @@ async def sell(self, itemid: int, assetid: int, price: float) -> 'SellResult': AuthenticatorError: Мобильный аутентификатор не подключён или с момента его подключения ещё не прошло 7 дней. """ - - url = self.base_url + 'sale/' - result = await self._async_client.post( - url, - data={"itemid": itemid, "assetid": assetid, "price": price}, - headers=self.headers - ) - return SellResult.de_json(result.json(), self) + result = await self._post_request('sale/', data={"itemid": itemid, "assetid": assetid, "price": price}) + return SellResult.de_json(result) @log - async def buy(self, _id: Union[int, str], _type: int, price: float, currency: int = 1) -> 'BuyResult': + async def buy(self, itemid: int | str, itemtype: Literal[1, 2, 3], price: float, currency: Literal[1] = 1) -> BuyResult: """Создать предложение о покупке предмета по строго указанной цене. Если в момент покупки цена предложения о продаже изменится, покупка не совершится. @@ -165,38 +269,31 @@ async def buy(self, _id: Union[int, str], _type: int, price: float, currency: in Сайт пока работает только с рублями. Не меняйте значение currency. Args: - _id (:obj:`int | :obj: str`): В качества ID может выступать: + itemid (int | str): В качества ID может выступать: GID для варианта покупки Commodity. Часть ссылки после nc/ (nc/L8RJI7XR96Mmo3Bu) для варианта покупки NoCommission. ID предложения о продаже для варианта покупки Offer (найти их можно в ItemInfo). - _type (:obj:`int`): Вариант покупки (указаны выше) - 1 / 2 / 3. - price (:obj:`float`): Цена предложения о продаже без учёта комиссии/скидки. + itemtype (int): Вариант покупки (указаны выше) - 1 / 2 / 3. + price (float): Цена предложения о продаже без учёта комиссии/скидки. Актуальные цены можно узнать через get_item_info и get_min_prices. - currency (:obj:`int`): Валюта покупки. Значение 1 - рубль. + currency (int): Валюта покупки. Значение 1 - рубль. Returns: - :class:`steam_trader.BuyResult`: Результат создания запроса о покупке. + BuyResult: Результат создания запроса о покупке. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. NoTradeLink: Отсутствует сслыка для обмена. NoLongerExists: Предложение больше недействительно. NotEnoughMoney: Недостаточно средств. """ - - if _type not in range(1, 4): - logging.warning(f'Неправильное значение _type >> {_type}') - - url = self.base_url + 'buy/' - result = await self._async_client.post( - url, - data={"id": _id, "type": _type, "price": price, "currency": currency}, - headers=self.headers - ) - return BuyResult.de_json(result.json(), self) + if itemtype not in range(1, 4): + logging.warning(f"Неправильное значение _type '{itemtype}'") + result = await self._post_request('buy/', data={"id": itemid, "type": itemtype, "price": price, "currency": currency}) + return BuyResult.de_json(result) @log - async def create_buy_order(self, gid: int, price: float, *, count: int = 1) -> 'BuyOrderResult': + async def create_buy_order(self, gid: int, price: float, *, count: int = 1) -> BuyOrderResult: """Создать заявку на покупку предмета с определённым GID. Note: @@ -206,34 +303,27 @@ async def create_buy_order(self, gid: int, price: float, *, count: int = 1) -> ' (дешевле), то сделка совершится по цене 10 ₽. Args: - gid (:obj:`int`): ID группы предметов. - price (:obj:`float`): Цена предмета, за которую будете его покупать без учёта комиссии/скидки. - count (:obj:`int`): Количество заявок для размещения (не более 500). По умолчанию - 1. + gid (int): ID группы предметов. + price (float): Цена предмета, за которую будете его покупать без учёта комиссии/скидки. + count (int): Количество заявок для размещения (не более 500). По умолчанию - 1. Returns: - :class:`steam_trader.BuyOrderResult`: Результат созданния заявки на покупку. + BuyOrderResult: Результат созданния заявки на покупку. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. NoTradeLink: Отсутствует сслыка для обмена. NoLongerExists: Предложение больше недействительно. NotEnoughMoney: Недостаточно средств. """ - if not 1 <= count <= 500: - logging.warning(f'Количество заявок должно быть от 1 до 500 (не {count})') - - url = self.base_url + 'createbuyorder/' - result = await self._async_client.post( - url, - data={"gid": gid, "price": price, "count": count}, - headers=self.headers - ) - return BuyOrderResult.de_json(result.json(), self) + logging.warning(f"Количество заявок должно быть от 1 до 500 (не '{count}')") + result = await self._post_request('createbuyorder/', data={"gid": gid, "price": price, "count": count}) + return BuyOrderResult.de_json(result) @log - async def multi_buy(self, gid: int, max_price: float, count: int) -> 'MultiBuyResult': + async def multi_buy(self, gid: int, max_price: float, count: int) -> MultiBuyResult: """Создать запрос о покупке нескольких предметов с определённым GID. Будут куплены самые лучшие (дешёвые) предложения о продаже. @@ -243,19 +333,19 @@ async def multi_buy(self, gid: int, max_price: float, count: int) -> 'MultiBuyRe если при покупке указать максмальную цену 25 ₽, то сделки совершатся по цене 10 и 11 ₽, а общая сумма потраченных средств - 21 ₽. - Если по указанной максимальной цене не окажется достаточно предложений о продаже, + Если по указанной максимальной цене не окажется достаточно предложений о продаже, success будет равен False и будет указано кол-во оставшихся предметов по данной цене. Args: - gid (:obj:`int`): ID группы предметов. - max_price (:obj:`float`): Максимальная цена одного предмета без учета комиссии/скидки. - count (:obj:`int`): Количество предметов для покупки. + gid (int): ID группы предметов. + max_price (float): Максимальная цена одного предмета без учета комиссии/скидки. + count (int): Количество предметов для покупки. Returns: - :class:`steam_trader.MultiBuyResult`: Результат создания запроса на мульти-покупку. + MultiBuyResult: Результат создания запроса на мульти-покупку. Raises: - OfferCreationFail: При создании заявки произошла неизвестная ошибка. + InternalError: При создании заявки произошла неизвестная ошибка. NoTradeLink: Отсутствует сслыка для обмена. NotEnoughMoney: Недостаточно средств. @@ -263,28 +353,22 @@ async def multi_buy(self, gid: int, max_price: float, count: int) -> 'MultiBuyRe 0.2.3: Теперь, если во время операции закончиться баланс, вместо ошибки, в датаклассе будет указано кол-во оставшихся предметов по данной цене. """ - - url = self.base_url + 'multibuy/' - result = await self._async_client.post( - url, - data={"gid": gid, "max_price": max_price, "count": count}, - headers=self.headers - ) - return MultiBuyResult.de_json(result.json(), self) + result = await self._post_request('multibuy/', data={"gid": gid, "max_price": max_price, "count": count}) + return MultiBuyResult.de_json(result) @log - async def edit_price(self, _id: int, price: float) -> 'EditPriceResult': + async def edit_price(self, itemid: int, price: float) -> 'EditPriceResult': """Редактировать цену предмета/заявки на покупку. При редактировании может произойти моментальная продажа/покупка по аналогии тому, как это сделано в методах sell и create_buy_order. Args: - _id (:obj:`int`): ID предложения о продаже/заявки на покупку. - price (:obj:`float`): Новая цена, за которую хотите продать/купить предмет без учёта комиссии/скидки. + itemid (int): ID предложения о продаже/заявки на покупку. + price (float): Новая цена, за которую хотите продать/купить предмет без учёта комиссии/скидки. Returns: - :class:`steam_trader.EditPriceResult`: Результат запроса на изменение цены. + EditPriceResult: Результат запроса на изменение цены. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -292,51 +376,39 @@ async def edit_price(self, _id: int, price: float) -> 'EditPriceResult': IncorrectPrice: Неправильная цена заявки. NotEnoughMoney: Недостаточно средств. """ - - url = self.base_url + 'editprice/' - result = await self._async_client.post( - url, - data={"id": _id, "price": price}, - headers=self.headers - ) - return EditPriceResult.de_json(result.json(), self) + result = await self._post_request('editprice/', data={"id": itemid, "price": price}) + return EditPriceResult.de_json(result) @log - async def delete_item(self, _id: int) -> 'DeleteItemResult': + async def delete_item(self, itemid: int) -> DeleteItemResult: """Снять предмет с продажи/заявку на покупку. Args: - _id (:obj:`int`): ID продажи/заявки на покупку. + itemid (int): ID продажи/заявки на покупку. Returns: - :class:`steam_trader.DeleteItemResult`: Результат запроса снятия предмета + DeleteItemResult: Результат запроса снятия предмета с продажи/заявки на покупку. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. """ - - url = self.base_url + 'deleteitem/' - result = await self._async_client.post( - url, - data={"id": _id}, - headers=self.headers - ) - return DeleteItemResult.de_json(result.json(), self) + result = await self._post_request('deleteitem/', data={"id": itemid}) + return DeleteItemResult.de_json(result) @log - async def get_down_orders(self, gameid: int, *, order_type: LiteralString = 'sell') -> 'GetDownOrdersResult': + async def get_down_orders(self, gameid: int, *, order_type: Literal['sell', 'buy'] = 'sell') -> GetDownOrdersResult: """Снять все заявки на продажу/покупку предметов. Args: - gameid (:obj:`int`): AppID приложения в Steam. - order_type (:obj:`LiteralString`): Тип заявок для удаления: + gameid (int): AppID приложения в Steam. + order_type (str): Тип заявок для удаления: "sell" - предложения о ПРОДАЖЕ. Значение по умолчанию. "buy" - предложения о ПОКУПКЕ. Returns: - :class:`steam_trader.GetDownOrdersResult`: Результат снятия всех заявок + GetDownOrdersResult: Результат снятия всех заявок на продажу/покупку предметов. Raises: @@ -345,42 +417,30 @@ async def get_down_orders(self, gameid: int, *, order_type: LiteralString = 'sel UnsupportedAppID: Указан недействительный gameid. ValueError: Указано недопустимое значение order_type. """ - if gameid not in SUPPORTED_APPIDS: - raise UnsupportedAppID(f'Игра с AppID {gameid} в данный момент не поддерживается.') - + raise UnsupportedAppID(f"Игра с AppID '{gameid}' в данный момент не поддерживается.") if order_type not in ['sell', 'buy']: - logging.warning(f'Неизвестный тип >> {order_type}') + logging.warning(f"Неизвестный тип '{order_type}'") - url = self.base_url + 'getdownorders/' - result = await self._async_client.post( - url, - data={"gameid": gameid, "type": order_type}, - headers=self.headers - ) - return GetDownOrdersResult.de_json(result.json(), self) + result = await self._post_request('getdownorders/', data={"gameid": gameid, "type": order_type}) + return GetDownOrdersResult.de_json(result) @log - async def get_items_for_exchange(self) -> 'ItemsForExchange': + async def get_items_for_exchange(self) -> ItemsForExchange: """Получить список предметов для обмена с ботом. Returns: - :class:`steam_trader.ItemsForExchange`: Cписок предметов для обмена с ботом. + ItemsForExchange: Cписок предметов для обмена с ботом. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. NoTradeItems: Нет предметов для обмена. """ - - url = self.base_url + 'itemsforexchange/' - result = await self._async_client.get( - url, - headers=self.headers - ) - return ItemsForExchange.de_json(result.json(), self) + result = await self._get_request('itemsforexchange/') + return ItemsForExchange.de_json(result) @log - async def exchange(self) -> 'ExchangeResult': + async def exchange(self) -> ExchangeResult: """Выполнить обмен с ботом. Note: @@ -388,7 +448,7 @@ async def exchange(self) -> 'ExchangeResult': В противном случае трейд будет отменён. Returns: - :class:`steam_trader.ExchangeResult`: Результат обмена с ботом. + ExchangeResult: Результат обмена с ботом. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -403,35 +463,25 @@ async def exchange(self) -> 'ExchangeResult': AuthenticatorError: Мобильный аутентификатор не подключён, или с момента его подключения ещё не прошло 7 дней. """ - - url = self.base_url + 'exchange/' - result = await self._async_client.get( - url, - headers=self.headers - ) - return ExchangeResult.de_json(result.json(), self) + result = await self._get_request('exchange/') + return ExchangeResult.de_json(result) @log - async def get_items_for_exchange_p2p(self) -> 'ItemsForExchange': + async def get_items_for_exchange_p2p(self) -> ItemsForExchange: """Получить список предметов для p2p обмена. Returns: - :class:`steam_trader.ItemsForExchange`: Cписок предметов для p2p обмена. + ItemsForExchange: Cписок предметов для p2p обмена. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. NoTradeItems: Нет предметов для обмена. """ - - url = self.base_url + 'itemsforexchangep2p/' - result = await self._async_client.get( - url, - headers=self.headers - ) - return ItemsForExchange.de_json(result.json(), self) + result = await self._get_request('itemsforexchangep2p/') + return ItemsForExchange.de_json(result) @log - async def exchange_p2p(self) -> 'ExchangeP2PResult': + async def exchange_p2p(self) -> ExchangeP2PResult: """Выполнить p2p обмен. Note: @@ -439,7 +489,7 @@ async def exchange_p2p(self) -> 'ExchangeP2PResult': В противном случае, трейд будет отменён. Returns: - :class:`steam_trader.ExchangeP2PResult`: Результат p2p обмена. + ExchangeP2PResult: Результат p2p обмена. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -452,113 +502,86 @@ async def exchange_p2p(self) -> 'ExchangeP2PResult': AuthenticatorError: Мобильный аутентификатор не подключён, или с момента его подключения ещё не прошло 7 дней. """ - - url = self.base_url + 'exchange/' - result = await self._async_client.get( - url, - headers=self.headers - ) - return ExchangeP2PResult.de_json(result.json(), self) + result = await self._get_request('exchange/') + return ExchangeP2PResult.de_json(result) @log - async def get_min_prices(self, gid: int, currency: int = 1) -> 'MinPrices': + async def get_min_prices(self, gid: int, currency: Literal[1] = 1) -> MinPrices: """Получить минимальные/максимальные цены предмета. Note: Сайт пока работает только с рублями. Не меняйте значение currency. Args: - gid (:obj:`int`): ID группы предметов. - currency (:obj:`int`): Валюта, значение 1 - рубль. + gid (int): ID группы предметов. + currency (int): Валюта, значение 1 - рубль. Returns: - :class:`steam_trader.MinPrices`: Минимальные/максимальные цены предмета. + steam_trader.MinPrices: Минимальные/максимальные цены предмета. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. """ - - url = self.base_url + "getminprices/" - result = await self._async_client.get( - url, - params={"gid": gid, "currency": currency}, - headers=self.headers - ) - return MinPrices.de_json(result.json(), self) + result = await self._get_request('getminprices/', params={"gid": gid, "currency": currency}) + return MinPrices.de_json(result) @log - async def get_item_info(self, gid: int) -> 'ItemInfo': + async def get_item_info(self, gid: int) -> ItemInfo: """Получить информацию о группе предметов. Args: - gid (:obj:`int`): ID группы предметов. + gid (int): ID группы предметов. Returns: - :class:`steam_trader.ItemInfo`: Информация о группе предметов. + ItemInfo: Информация о группе предметов. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. UnknownItem: Неизвестный предмет. """ - - url = self.base_url + "iteminfo/" - result = await self._async_client.get( - url, - params={"gid": gid}, - headers=self.headers - ) - return ItemInfo.de_json(result.json(), self) + result = await self._get_request('iteminfo/', params={"gid": gid}) + return ItemInfo.de_json(result) @log - async def get_order_book(self, gid: int, *, mode: LiteralString = 'all', limit: Optional[int] = None) -> 'OrderBook': + async def get_order_book(self, gid: int, *, mode: Literal['all', 'sell', 'buy'] = 'all', limit: Optional[int] = None) -> OrderBook: """Получить заявки о покупке/продаже предмета. Args: - gid (:obj:`int`): ID группы предметов. - mode (:obj:`LiteralString`): Режим отображения + gid (int): ID группы предметов. + mode (str): Режим отображения 'all' - отображать покупки и продажи. Значение по умолчанию. 'sell' - отображать только заявки на ПРОДАЖУ. 'buy' - отображать только заявки на ПОКУПКУ. - limit (:obj:`int`, optional): Максимальное количество строк в списке. По умолчанию - неограниченно + limit (int, optional): Максимальное количество строк в списке. По умолчанию - неограниченно Returns: - :class:`steam_trader.OrderBook`: Заявки о покупке/продаже предмета. + OrderBook: Заявки о покупке/продаже предмета. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. """ - if mode not in ['all', 'sell', 'buy']: - logging.warning(f'Неизвестный режим >> {mode}') + logging.warning(f"Неизвестный режим '{mode}'") - url = self.base_url + "orderbook/" - result = await self._async_client.get( - url, - params={"gid": gid, "mode": mode, "limit": limit}, - headers=self.headers - ) - return OrderBook.de_json(result.json(), self) + result = await self._get_request('orderbook/', params={"gid": gid, "mode": mode, "limit": limit}) + return OrderBook.de_json(result) @log - async def get_web_socket_token(self) -> 'WebSocketToken': + async def get_web_socket_token(self) -> WebSocketToken: """Получить токен для авторизации в WebSocket. Незадокументированно.""" - url = self.base_url + "getwstoken/" - result = await self._async_client.get( - url, - params={'key': self.api_token} - ) - return WebSocketToken.de_json(result.json(), self) + result = await self._get_request('getwstoken/', params={'key': self.api_token}) + return WebSocketToken.de_json(result) @log - async def get_inventory(self, gameid: int, *, status: Optional[Sequence[int]] = None) -> 'Inventory': + async def get_inventory(self, gameid: int, *, status: Optional[Sequence[Literal[0, 1, 2, 3, 4]]] = None) -> Inventory: """Получить инвентарь клиента, включая заявки на покупку и купленные предметы. По умолчанию возвращает список предметов из инвентаря Steam, которые НЕ выставлены на продажу. Args: - gameid (:obj:`int`): AppID приложения в Steam. - status (Sequence[:obj:`int`], optional): + gameid (int): AppID приложения в Steam. + status (Sequence[int], optional): Указывается, чтобы получить список предметов с определенным статусом. Возможные статусы: @@ -569,17 +592,14 @@ async def get_inventory(self, gameid: int, *, status: Optional[Sequence[int]] = 4 - Заявка на покупку Returns: - :class:`steam_trader.Inventory`: Инвентарь клиента, включая заявки на покупку и купленные предметы. + Inventory: Инвентарь клиента, включая заявки на покупку и купленные предметы. Raises: UnsupportedAppID: Указан недействительный gameid. ValueError: Указан недопустимый статус. """ - if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'getinventory/' params = {"gameid": gameid} if status is not None: @@ -588,36 +608,27 @@ async def get_inventory(self, gameid: int, *, status: Optional[Sequence[int]] = raise ValueError(f'Неизвестный статус {s}') params[f'status[{i}]'] = s - result = await self._async_client.get( - url, - params=params, - headers=self.headers - ) - return Inventory.de_json(result.json(), status, self) + result = await self._get_request('getinventory/', params=params) + return Inventory.de_json(result, status) @log - async def get_buy_orders(self, *, gameid: Optional[int] = None, gid: Optional[int] = None) -> 'BuyOrders': + async def get_buy_orders(self, *, gameid: Optional[int] = None, gid: Optional[int] = None) -> BuyOrders: """Получить последовательность заявок на покупку. По умолчанию возвращаются заявки для всех предметов из всех разделов. При указании соответствующих параметров можно получить заявки из определённого раздела и/или предмета. - Note: - Во время тестирования мои запросы на покупку отображались только тогда, - когда я указал конкретный appid игры и gid предмета. - Args: - gameid (:obj:`int`, optional): AppID приложения в Steam. - gid (:obj:`int`, optional): ID группы предметов. + gameid (int, optional): AppID приложения в Steam. + gid (int, optional): ID группы предметов. Returns: - :class:`steam_trader.BuyOrders`: Список заявок на покупку. + BuyOrders: Список заявок на покупку. Raises: UnsupportedAppID: Указан недействительный gameid. NoBuyOrders: Нет запросов на покупку. """ - if gameid is not None and gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') @@ -627,63 +638,39 @@ async def get_buy_orders(self, *, gameid: Optional[int] = None, gid: Optional[in if gid is not None: params['gid'] = gid - url = self.base_url + 'getbuyorders/' - result = await self._async_client.get( - url, - params=params, - headers=self.headers - ) - return BuyOrders.de_json(result.json(), self) + result = await self._get_request('getbuyorders/', params=params) + return BuyOrders.de_json(result) @log - async def get_discounts(self) -> 'Discounts': + async def get_discounts(self) -> Discounts: """Получить комиссии/скидки и оборот на сайте. Данные хранятся в словаре data, где ключ - это AppID игры в Steam (См. steam_trader.constants). Returns: - :class:`steam_trader.Discounts`: Комиссии/скидки и оборот на сайте. + Discounts: Комиссии/скидки и оборот на сайте. """ - - url = self.base_url + 'getdiscounts/' - result = await self._async_client.get( - url, - headers=self.headers - ) - return Discounts.de_json(result.json(), self) + result = await self._get_request('getdiscounts/') + return Discounts.de_json(result) @log async def set_trade_link(self, trade_link: str) -> None: """Установить ссылку для обмена. Args: - trade_link (:obj:`str`): Ссылка для обмена, + trade_link (str): Ссылка для обмена, Например, https://steamcommunity.com/tradeoffer/new/?partner=453486961&token=ZhXMbDS9 Raises: SaveFail: Не удалось сохранить ссылку обмена. WrongTradeLink: Указана ссылка для обмена от другого Steam аккаунта ИЛИ ссылка для обмена уже указана. """ - - url = self.base_url + 'settradelink/' - result = await self._async_client.post( - url, - data={"trade_link": trade_link}, - headers=self.headers - ) - result = result.json() - + result = await self._post_request('settradelink/', data={"trade_link": trade_link}) if not result['success']: try: match result['code']: - case 400: - raise BadRequestError('Неправильный запрос') - case 401: - raise Unauthorized('Неправильный api-токен') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') case 1: - raise SaveFail('Не удалось сохранить ссылку обмена') + raise SaveFail('Не удалось сохранить ссылку для обмена.') except KeyError: raise WrongTradeLink('Указана ссылка для обмена от другого Steam аккаунта ИЛИ ссылка для обмена уже указана.') @@ -694,32 +681,18 @@ async def remove_trade_link(self) -> None: Raises: SaveFail: Не удалось удалить ссылку обмена. """ - - url = self.base_url + 'removetradelink/' - result = await self._async_client.post( - url, - data={"trade_link": "1"}, - headers=self.headers - ) - result = result.json() - + result = await self._post_request('removetradelink/', data={"trade_link": "1"}) if not result['success']: match result['code']: - case 400: - raise BadRequestError('Неправильный запрос') - case 401: - raise Unauthorized('Неправильный api-токен') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') case 1: - raise SaveFail('Не удалось удалить ссылку обмена') + raise SaveFail('Не удалось удалить ссылку обмена.') @log - async def get_operations_history(self, *, operation_type: Optional[int] = None, page: int = 0) -> 'OperationsHistory': - """Получить историю операций (По умолчанию все типы). + async def get_operations_history(self, *, operation_type: Optional[Literal[1, 2, 3, 4, 5, 9, 10]] = None, page: int = 0) -> OperationsHistory: + """Получить историю операций (По умолчанию все типы). В каждой странице до 100 пунктов. Args: - operation_type (:obj:`int`, optional): Тип операции. Может быть пустым. + operation_type (int, optional): Тип операции. Может быть пустым. 1 - Покупка предмета 2 - Продажа предмета 3 - Возврат за покупку @@ -727,99 +700,60 @@ async def get_operations_history(self, *, operation_type: Optional[int] = None, 5 - Вывести средства 9 - Ожидание покупки 10 - Штрафной балл - page (:obj:`int`): Страница операций. Отсчёт начинается с 0. + page (int): Страница операций. Отсчёт начинается с 0. Returns: - :class:`steam_trader.OperationsHistory`: История операций. - - Raises: - ValueError: Указано недопустимое значение operation_type. + OperationsHistory: История операций. Changes: 0.3.0: Добавлен аргумент page. """ - - if operation_type is not None and operation_type not in range(1, 11): - logging.warning(f'Неизвестный тип {operation_type}') - - url = self.base_url + 'operationshistory/' - result = await self._async_client.get( - url, - params={"type": operation_type, "page": page}, - headers=self.headers - ) - return OperationsHistory.de_json(result.json(), self) + if operation_type is not None and operation_type not in (1, 2, 3, 4, 5, 9, 10): + logging.warning(f"Неизвестный тип '{operation_type}'") + result = await self._get_request('operationshistory/', params={"type": operation_type, "page": page}) + return OperationsHistory.de_json(result) @log async def update_inventory(self, gameid: int) -> None: """Обновить инвентарь игры на сайте. Args: - gameid (:obj:`int`): AppID приложения в Steam. + gameid (int): AppID приложения в Steam. Raises: UnsupportedAppID: Указан недействительный gameid. """ - if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'updateinventory/' - result = await self._async_client.get( - url, - params={"gameid": gameid}, - headers=self.headers - ) - result = result.json() - - if not result['success']: - match result['code']: - case 400: - raise BadRequestError('Неправильный запрос.') - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') + await self._get_request('updateinventory/', params={"gameid": gameid}) @log - async def get_inventory_state(self, gameid: int) -> 'InventoryState': + async def get_inventory_state(self, gameid: int) -> InventoryState: """Получить текущий статус обновления инвентаря. Args: - gameid (:obj:`int`): AppID приложения в Steam. + gameid (int): AppID приложения в Steam. Returns: - :class:`steam_trader.InventoryState`: Текущий статус обновления инвентаря. + InventoryState: Текущий статус обновления инвентаря. Raises: UnsupportedAppID: Указан недействительный gameid. """ - if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'inventorystate/' - result = await self._async_client.get( - url, - params={"gameid": gameid}, - headers=self.headers - ) - return InventoryState.de_json(result.json(), self) + result = await self._get_request('inventorystate/', params={"gameid": gameid}) + return InventoryState.de_json(result) @log - async def trigger_alt_web_socket(self) -> Optional['AltWebSocket']: + async def trigger_alt_web_socket(self) -> Optional[AltWebSocket]: """Создать запрос альтернативным WebSocket. Для поддержания активного соединения нужно делать этот запрос каждые 2 минуты. Возвращает None если новых сообщений нет. При этом соединение будет поддрежано. Returns: - :class:`steam_trader.AltWebSocket`, optional: Запрос альтернативным WebSocket. + AltWebSocket, optional: Запрос альтернативным WebSocket. """ - - url = self.base_url + 'altws/' - result = await self._async_client.get( - url, - headers=self.headers - ) - return AltWebSocket.de_json(result.json(), self) + result = await self._get_request('altws/') + return AltWebSocket.de_json(result) diff --git a/steam_trader/api/_edit_item.py b/steam_trader/api/_edit_item.py index 72a2c92..7ab77fd 100644 --- a/steam_trader/api/_edit_item.py +++ b/steam_trader/api/_edit_item.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional, Any from steam_trader import exceptions from ._base import TraderClientObject @@ -14,44 +14,34 @@ class EditPriceResult(TraderClientObject): """Класс, представляющий результат запроса на изменение цены. Attributes: - success (:obj:`bool`): Результат запроса. - type (:obj:`int`): Тип заявки. 0 - продажа, 1 - покупка. - position (:obj:`int`): Позиция предмета в очереди. - fast_execute (:obj:`bool`): Был ли предмет продан/куплен моментально. - new_id (:obj:`int`, optional): Новый ID заявки. Указывается, если 'fast_execute' = true. + success (bool): Результат запроса. + type (int): Тип заявки. 0 - продажа, 1 - покупка. + position (int): Позиция предмета в очереди. + fast_execute (bool): Был ли предмет продан/куплен моментально. + new_id (int, optional): Новый ID заявки. Указывается, если 'fast_execute' = true. Новый ID присваивается только заявкам на ПОКУПКУ и только в случае редактирования уже имеющейся заявки. - price (:obj:`float`, optional): Цена, за которую был продан/куплен предмет с учётом комиссии/скидки. + price (float, optional): Цена, за которую был продан/куплен предмет с учётом комиссии/скидки. Указывается, если 'fast_execute' = true. - percent (:obj:`float`, optional): Размер комиссии/скидки в процентах, за которую был продан/куплен предмет. + percent (float, optional): Размер комиссии/скидки в процентах, за которую был продан/куплен предмет. Указывается, если 'fast_execute' = true. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. """ success: bool type: int position: int fast_execute: bool - client: Union['Client', 'ClientAsync', None] new_id: Optional[int] = None price: Optional[float] = None percent: Optional[float] = None @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'EditPriceResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При выполнении запроса произошла неизвестная ошибка.') case 2: @@ -61,23 +51,21 @@ def de_json( case 5: raise exceptions.NotEnoughMoney('Для покупки не достаточно средств.') - data = super(EditPriceResult, cls).de_json(data) + data = super(EditPriceResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class DeleteItemResult(TraderClientObject): """Класс, представляющий результат запроса снятия предмета с продажи/заявки на покупку. Attributes: - success (:obj:`bool`): Результат запроса. - has_ex (:obj:`bool`): Есть ли доступный обмен на сайте. - has_bot_ex (:obj:`bool`): Есть ли доступный обмен с ботом. - has_p2p_ex (:obj:`bool`): Есть ли доступный P2P обмен. - total_fines (:obj:`int`): Общее количество штрафных баллов. - fine_date (:obj:`int`, optional): Дата снятия штрафных баллов. Если None - штрафных баллов нет. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + has_ex (bool): Есть ли доступный обмен на сайте. + has_bot_ex (bool): Есть ли доступный обмен с ботом. + has_p2p_ex (bool): Есть ли доступный P2P обмен. + total_fines (int): Общее количество штрафных баллов. + fine_date (int, optional): Дата снятия штрафных баллов. Если None - штрафных баллов нет. """ success: bool @@ -86,69 +74,51 @@ class DeleteItemResult(TraderClientObject): has_p2p_ex: bool total_fines: int fine_date: Optional[int] - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'DeleteItemResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При выполнении запроса произошла неизвестная ошибка.') case 2: raise exceptions.UnknownItem('Неизвестный предмет.') - data = super(DeleteItemResult, cls).de_json(data) + data = super(DeleteItemResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class GetDownOrdersResult(TraderClientObject): """Класс, представляющий результат снятия всех заявок на продажу/покупку. Attributes: - success (:obj:`bool`): Результат запроса. - count (:obj:`int`): Количество удалённых предложений. - ids (Sequence[:obj:`int`]): Список из ID удалённых предложений. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + count (int): Количество удалённых предложений. + ids (Sequence[int]): Список из ID удалённых предложений. """ success: bool count: int ids: Sequence[int] - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'GetDownOrdersResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При выполнении запроса произошла неизвестная ошибка.') case 2: raise exceptions.NoTradeItems('Нет заявок на продажу/покупку.') - data = super(GetDownOrdersResult, cls).de_json(data) + data = super(GetDownOrdersResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) diff --git a/steam_trader/api/_item_info.py b/steam_trader/api/_item_info.py index 051b532..d5a7031 100644 --- a/steam_trader/api/_item_info.py +++ b/steam_trader/api/_item_info.py @@ -1,29 +1,23 @@ from dataclasses import dataclass from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional, Union +from typing import Optional, Any from steam_trader.exceptions import BadRequestError, Unauthorized, InternalError, UnknownItem, TooManyRequests from ._base import TraderClientObject from ._offers import SellOffer, BuyOffer from ._misc import SellHistoryItem, Filters -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class MinPrices(TraderClientObject): """Класс, представляющий минимальную/максимальную цену на предмет. Attributes: - success (:obj:`bool`): Результат запроса. - market_price (:obj:`float`, optional): Минимальная цена продажи. Может быть пустым. - buy_price (:obj:`float`, optional): Максимальная цена покупки. Может быть пустым. - steam_price (:obj:`float`, optional): Минимальная цена в Steam. Может быть пустым. - count_sell_offers (:obj:`int`): Количество предложений о продаже. - count_buy_offers (:obj:`int`): Количество предложений о покупке. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + market_price (float, optional): Минимальная цена продажи. Может быть пустым. + buy_price (float, optional): Максимальная цена покупки. Может быть пустым. + steam_price (float, optional): Минимальная цена в Steam. Может быть пустым. + count_sell_offers (int): Количество предложений о продаже. + count_buy_offers (int): Количество предложений о покупке. """ success: bool @@ -32,60 +26,50 @@ class MinPrices(TraderClientObject): steam_price: Optional[float] count_sell_offers: int count_buy_offers: int - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'MinPrices': if not data['success']: match data['code']: - case 400: - raise BadRequestError('Неправильный запрос.') - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') case 1: raise InternalError('При выполнении запроса произошла неизвестная ошибка.') case 2: raise UnknownItem('Неизвестный предмет.') - data = super(MinPrices, cls).de_json(data) + data = super(MinPrices, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class ItemInfo(TraderClientObject): """Класс, представляющий информацию о группе предметов на сайте. Attributes: - success (:obj:`bool`): Результат запроса. - name (:obj:`str`): Локализованное (переведённое) название предмета. - hash_name (:obj:`str`): Параметр 'market_hash_name' в Steam. - type (:obj:`str`): Тип предмета (из Steam). - gameid (:obj:`int`): AppID приложения в Steam. - contextid (:obj:`int`): ContextID приложения в Steam. - color (:obj:`str`): Hex код цвета предмета (из Steam). - small_image (:obj:`str`): Абсолютная ссылка на маленькое изображение предмета. - large_image (:obj:`str`): Абсолютная ссылка на большое изображение предмета. - marketable (:obj:`bool`): Параметр 'marketable' в Steam. - tradable (:obj:`bool`): Параметр 'tradable' в Steam. - description (:obj:`str`): Локализованное (переведённое) описание предмета. - market_price (:obj:`float`, optional): Минимальная цена продажи. Может быть пустым. - buy_price (:obj:`float`, optional): Максимальная цена покупки. Может быть пустым. - steam_price (:obj:`float`, optional): Минимальная цена в Steam. Может быть пустым. - filters (:class:`steam_trader.Filters`): Фильтры, используемые для поиска на сайте. - sell_offers (Sequnce[`steam_trader.SellOffer`]): Последовательность с предложениями о продаже. + success (bool): Результат запроса. + name (str): Локализованное (переведённое) название предмета. + hash_name (str): Параметр 'market_hash_name' в Steam. + type (str): Тип предмета (из Steam). + gameid (int): AppID приложения в Steam. + contextid (int): ContextID приложения в Steam. + color (str): Hex код цвета предмета (из Steam). + small_image (str): Абсолютная ссылка на маленькое изображение предмета. + large_image (str): Абсолютная ссылка на большое изображение предмета. + marketable (bool): Параметр 'marketable' в Steam. + tradable (bool): Параметр 'tradable' в Steam. + description (str): Локализованное (переведённое) описание предмета. + market_price (float, optional): Минимальная цена продажи. Может быть пустым. + buy_price (float, optional): Максимальная цена покупки. Может быть пустым. + steam_price (float, optional): Минимальная цена в Steam. Может быть пустым. + filters (steam_trader.Filters): Фильтры, используемые для поиска на сайте. + sell_offers (Sequnce[steam_trader.SellOffer]): Последовательность с предложениями о продаже. От большего к меньшему. - buy_offers (Sequnce[`steam_trader.BuyOffer`]): Последовательность с предложениями о покупке. + buy_offers (Sequnce[steam_trader.BuyOffer]): Последовательность с предложениями о покупке. От большего к меньшему. - sell_history (Sequence[`steam_trader.SellHistoryItem`]): Последовательность истории продаж. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + sell_history (Sequence[steam_trader.SellHistoryItem]): Последовательность истории продаж. """ success: bool @@ -103,17 +87,15 @@ class ItemInfo(TraderClientObject): market_price: Optional[float] buy_price: Optional[float] steam_price: Optional[float] - filters: Optional['Filters'] - sell_offers: Sequence['SellOffer'] - buy_offers: Sequence['BuyOffer'] - sell_history: Sequence['SellHistoryItem'] - client: Union['Client', 'ClientAsync', None] + filters: Optional[Filters] + sell_offers: Sequence[SellOffer] + buy_offers: Sequence[BuyOffer] + sell_history: Sequence[SellHistoryItem] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'ItemInfo': if not data['success']: @@ -140,51 +122,41 @@ def de_json( for i, item in enumerate(data['sell_history']): data['sell_history'][i] = SellHistoryItem.de_json(item) - data = super(ItemInfo, cls).de_json(data) + data = super(ItemInfo, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class OrderBook(TraderClientObject): """Класс, представляющий заявоки о покупке/продаже предмета. Attributes: - success (:obj:`bool`): Результат запроса. - sell (Sequence[Sequence[:obj:`int`, :obj:`int`]]): Сгруппированный по цене список заявок на продажу. + success (bool): Результат запроса. + sell (Sequence[Sequence[int]]): Сгруппированный по цене список заявок на продажу. Каждый элемент в списке является массивом, где первый элемент - это цена, а второй - количество заявок. - buy (Sequence[Sequence[:obj:`int`, :obj:`int`]]): Сгруппированный по цене список заявок на покупку. + buy (Sequence[Sequence[int]]): Сгруппированный по цене список заявок на покупку. Каждый элемент в списке является массивом, где первый элемент - это цена, а второй - количество заявок. - total_sell (:obj:`int`): Количество всех заявок на продажу. - total_buy (:obj:`int`): Количество всех заявок на покупку. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + total_sell (int): Количество всех заявок на продажу. + total_buy (int): Количество всех заявок на покупку. """ success: bool - sell: Sequence[Sequence[int, int]] - buy: Sequence[Sequence[int, int]] + sell: Sequence[Sequence[int]] + buy: Sequence[Sequence[int]] total_sell: int total_buy: int - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'OrderBook': if not data['success']: match data['code']: - case 400: - raise BadRequestError('Неправильный запрос.') - case 401: - raise Unauthorized('Неправильный api-токен.') - case 429: - raise TooManyRequests('Вы отправили слишком много запросов.') case 1: raise InternalError('При выполнении запроса произошла ошибка.') - data = super(OrderBook, cls).de_json(data) + data = super(OrderBook, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) diff --git a/steam_trader/api/_misc.py b/steam_trader/api/_misc.py index 1994307..0988570 100644 --- a/steam_trader/api/_misc.py +++ b/steam_trader/api/_misc.py @@ -1,70 +1,70 @@ from dataclasses import dataclass from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional, Union +from typing import Optional, Any from ._base import TraderClientObject -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class SellHistoryItem(TraderClientObject): """Класс, представляющий информацию о предмете в истории продаж. Attributes: - date (:obj:`int`): Timestamp времени продажи. - price (:obj:`float`): Цена предложения о покупке/продаже. + date (int): Timestamp времени продажи. + price (float): Цена предложения о покупке/продаже. """ date: int price: float @classmethod - def de_json(cls: dataclass, data: list, client: Union['Client', 'ClientAsync', None] = None) -> 'SellHistoryItem': + def de_json( # type: ignore + cls, + data: list + ) -> 'SellHistoryItem': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.SellHistoryItem`: Информация о предмете в истории продаж. + steam_trader.SellHistoryItem: Информация о предмете в истории продаж. """ - data = {'date': data[0], 'price': float(data[1])} # Принудительно конвертируем для совместимости. - - return cls(**data) + return cls( + date=data[0], + price=float(data[1]) + ) @dataclass(slots=True) class InventoryItem(TraderClientObject): """Класс, представляющий предмет в инвентаре. Attributes: - id (:obj:`int`, optional): ID заявки на покупку/продажу. Может быть пустым. - assetid (:obj:`int`, optional): AssetID предмета в Steam. Может быть пустым. - gid (:obj:`int`): ID группы предметов. - itemid (:obj:`int`): Уникальный ID предмета. - price (:obj:`float`, optional): Цена, за которую предмет был выставлен/куплен/продан предмет без учёта + id (int, optional): ID заявки на покупку/продажу. Может быть пустым. + assetid (int, optional): AssetID предмета в Steam. Может быть пустым. + gid (int): ID группы предметов. + itemid (int): Уникальный ID предмета. + price (float, optional): Цена, за которую предмет был выставлен/куплен/продан предмет без учёта скидки/комиссии. Может быть пустым. - currency (:obj:`int`, optional): Валюта, за которую предмет был выставлен/куплен/продан. Значение 1 - рубль. + currency (int, optional): Валюта, за которую предмет был выставлен/куплен/продан. Значение 1 - рубль. Может быть пустым. - timer (:obj:`int`, optional): Время, которое доступно для приема/передачи этого предмета. Может быть пустым. - type (:obj:`int`, optional): Тип предмета. 0 - продажа, 1 - покупка. Может быть пустым. - status (:obj:`int`): Статус предмета. - -2 - Предмет в инвентаре Steam не выставлен на продажу. + timer (int, optional): Время, которое доступно для приема/передачи этого предмета. Может быть пустым. + type (int, optional): Тип предмета. 0 - продажа, 1 - покупка. Может быть пустым. + status (int): Статус предмета. + -2 - Предмет в инвентаре Steam не выставлен на продажу. 0 - Предмет выставлен на продажу или выставлена заявка на покупку. Для различия используется поле type. 1 - Предмет был куплен/продан и ожидает передачи боту или P2P способом. Для различия используется поле type. 2 - Предмет был передан боту или P2P способом и ожидает приёма покупателем. 6 - Предмет находится в режиме резервного времени. На сайте отображается как "Проверяется" - после истечения времени на передачу боту или P2P способом. - position (:obj:`int`, optional): Позиция предмета в списке заявок на покупку/продажу. Может быть пустым. - nc (:obj:`int`, optional): ID заявки на продажу для бескомиссионной ссылки. Может быть пустым. - percent (:obj:`float`, optional): Размер скидки/комиссии в процентах, с которой был куплен/продан предмет. + после истечения времени на передачу боту или P2P способом. + position (int, optional): Позиция предмета в списке заявок на покупку/продажу. Может быть пустым. + nc (int, optional): ID заявки на продажу для бескомиссионной ссылки. Может быть пустым. + percent (float, optional): Размер скидки/комиссии в процентах, с которой был куплен/продан предмет. Может быть пустым. - steam_item (:obj:`bool`): Флаг, определяющий, имеется ли этот предмет в инвентаре в Steam (для продавца). - nm (:obj:`bool`): Незадокументированно. + steam_item (bool): Флаг, определяющий, имеется ли этот предмет в инвентаре в Steam (для продавца). + nm (bool): Незадокументированно. """ id: Optional[int] @@ -83,19 +83,22 @@ class InventoryItem(TraderClientObject): nm: bool @classmethod - def de_json(cls: dataclass, data: dict, client: Union['Client', 'ClientAsync', None] = None) -> 'InventoryItem': + def de_json( + cls, + data: dict[str, Any] + ) -> 'InventoryItem': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.InventoryItem`: Предмет в инвентаре. + steam_trader.InventoryItem: Предмет в инвентаре. """ - data = super(InventoryItem, cls).de_json(data) + data = super(InventoryItem, cls)._de_json(data) return cls(**data) @@ -104,10 +107,10 @@ class Filter(TraderClientObject): """Класс, представляющий фильтр. Attributes: - id (:obj:`int`, optional): ID фильтра, может быть пустым. Если вы создаёте класс вручную, + id (int, optional): ID фильтра, может быть пустым. Если вы создаёте класс вручную, то обязательно укажите этот параметр. - title (:obj:`str`, optional): Тайтл фильтра, может быть пустым. - color (:obj:`str`, optionl): Hex цвет фильтра, может быть пустым. + title (str, optional): Тайтл фильтра, может быть пустым. + color (str, optional): Hex цвет фильтра, может быть пустым. """ id: Optional[int] @@ -116,22 +119,21 @@ class Filter(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'Filter': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.Filter`, optional: Фильтр. + steam_trader.Filter, optional: Фильтр. """ - data = super(Filter, cls).de_json(data) + data = super(Filter, cls)._de_json(data) return cls(**data) @@ -140,25 +142,25 @@ class Filters(TraderClientObject): """Класс, представляющий фильтры, используемые для поиска на сайте. Attributes: - quality (Sequence[:class:`steam_trader.Filter`], optional): + quality (Sequence[steam_trader.Filter], optional): Качество предмета (TF2, DOTA2). - type (Sequence[:class:`steam_trader.Filter`], optional): + type (Sequence[steam_trader.Filter], optional): Тип предмета (TF2, DOTA2). - used_by (Sequence[:class:`steam_trader.Filter`], optional): + used_by (Sequence[steam_trader.Filter], optional): Класс, который использует предмет (TF2). - craft (Sequence[:class:`steam_trader.Filter`], optional): + craft (Sequence[steam_trader.Filter], optional): Информация о карфте (TF2). - region (Sequence[:class:`steam_trader.Filter`], optional): + region (Sequence[steam_trader.Filter], optional): Регион игры (SteamGift). - genre (Sequence[:class:`steam_trader.Filter`], optional): + genre (Sequence[steam_trader.Filter], optional): Жанр игры (SteamGift). - mode (Sequence[:class:`steam_trader.Filter`], optional): + mode (Sequence[steam_trader.Filter], optional): Тип игры, взаимодействие с Steam (SteamGift). - trade (Sequence[:class:`steam_trader.Filter`], optional): + trade (Sequence[steam_trader.Filter], optional): Информация об обмене (SteamGift). - rarity (Sequence[:class:`steam_trader.Filter`], optional): + rarity (Sequence[steam_trader.Filter], optional): Редкость предмета (DOTA2). - hero (Sequence[:class:`steam_trader.Filter`], optional): + hero (Sequence[steam_trader.Filter], optional): Герой, который использует предмет (DOTA2). """ @@ -175,19 +177,18 @@ class Filters(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'Filters': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.Filters`: Фильтры. + steam_trader.Filters: Фильтры. """ try: @@ -199,47 +200,47 @@ def de_json( del data['class'] for i, _filter in enumerate(data['quality']): - data['quality'][i] = Filter.de_json(data['quality'][i]) + data['quality'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['type']): - data['type'][i] = Filter.de_json(data['type'][i]) + data['type'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['used_by']): - data['used_by'][i] = Filter.de_json(data['used_by'][i]) + data['used_by'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['craft']): - data['craft'][i] = Filter.de_json(data['craft'][i]) + data['craft'][i] = Filter.de_json(_filter) except KeyError: try: # SteamGift for i, _filter in enumerate(data['region']): - data['region'][i] = Filter.de_json(data['region'][i]) + data['region'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['genre']): - data['genre'][i] = Filter.de_json(data['genre'][i]) + data['genre'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['mode']): - data['mode'][i] = Filter.de_json(data['mode'][i]) + data['mode'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['trade']): - data['trade'][i] = Filter.de_json(data['trade'][i]) + data['trade'][i] = Filter.de_json(_filter) except KeyError: # DOTA2 for i, _filter in enumerate(data['rarity']): - data['rarity'][i] = Filter.de_json(data['rarity'][i]) + data['rarity'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['quality']): - data['quality'][i] = Filter.de_json(data['quality'][i]) + data['quality'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['type']): - data['type'][i] = Filter.de_json(data['type'][i]) + data['type'][i] = Filter.de_json(_filter) for i, _filter in enumerate(data['hero']): - data['hero'][i] = Filter.de_json(data['hero'][i]) + data['hero'][i] = Filter.de_json(_filter) - data = super(Filters, cls).de_json(data) + data = super(Filters, cls)._de_json(data) return cls(**data) @@ -248,14 +249,14 @@ class BuyOrder(TraderClientObject): """Класс, представляющий информацию о запросе на покупку. Attributes: - id (:obj:`int`): ID заявки на покупку. - gid (:obj:`int`): ID группы предметов. - gameid (:obj:`int`): AppID приложения в Steam. - hash_name (:obj:`str`): Параметр market_hash_name в Steam. - date (:obj:`int`): Timestamp подачи заявки. - price (:obj:`float`): Предлагаемая цена покупки без учёта скидки. - currency (:obj:`int`): Валюта, значение 1 - рубль. - position (:obj:`int`): Позиция заявки в очереди. + id (int): ID заявки на покупку. + gid (int): ID группы предметов. + gameid (int): AppID приложения в Steam. + hash_name (str): Параметр market_hash_name в Steam. + date (int): Timestamp подачи заявки. + price (float): Предлагаемая цена покупки без учёта скидки. + currency (int): Валюта, значение 1 - рубль. + position (int): Позиция заявки в очереди. """ id: int @@ -269,22 +270,21 @@ class BuyOrder(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'BuyOrder': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.BuyOrder`: Информация о запрос на покупку. + steam_trader.BuyOrder: Информация о запрос на покупку. """ - data = super(BuyOrder, cls).de_json(data) + data = super(BuyOrder, cls)._de_json(data) return cls(**data) @@ -293,10 +293,10 @@ class Discount(TraderClientObject): """Класс, представляющий информацию о комиссии/скидке в определённой игре. Attributes: - total_buy (:obj:`float`): Cколько денег потрачено на покупки. - total_sell (:obj:`float`): Cколько денег получено с продажи предметов. - discount (:obj:`float`): Cкидка на покупку. Величина в %. - commission (:obj:`float`): Комиссия на продажу. Величина в %. + total_buy (float): Cколько денег потрачено на покупки. + total_sell (float): Cколько денег получено с продажи предметов. + discount (float): Cкидка на покупку. Величина в %. + commission (float): Комиссия на продажу. Величина в %. """ total_buy: float @@ -305,19 +305,22 @@ class Discount(TraderClientObject): commission: float @classmethod - def de_json(cls: dataclass, data: dict, client: Union['Client', 'ClientAsync', None] = None) -> 'Discount': + def de_json( + cls, + data: dict[str, Any] + ) -> 'Discount': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.Discount`: Информацию о комиссии/скидке в определённой игре. + steam_trader.Discount: Информацию о комиссии/скидке в определённой игре. """ - data = super(Discount, cls).de_json(data) + data = super(Discount, cls)._de_json(data) return cls(**data) @@ -326,12 +329,12 @@ class OperationsHistoryItem(TraderClientObject): """Класс, представляющий информацию о предмете в истории операций. Attributes: - id (:obj:`int`): ID Операции. - name (:obj:`str`): Название операции. - type (:obj:`int`): Тип операции. 0 - продажа, 1 - покупка. - amount (:obj:`float`): Сумма операции. - currency (:obj:`int`): Валюта, значение 1 - рубль. - date (:obj:`int`): Timestamp операции. + id (int): ID Операции. + name (str): Название операции. + type (int): Тип операции. 0 - продажа, 1 - покупка. + amount (float): Сумма операции. + currency (int): Валюта, значение 1 - рубль. + date (int): Timestamp операции. """ id: int @@ -342,19 +345,22 @@ class OperationsHistoryItem(TraderClientObject): date: int @classmethod - def de_json(cls: dataclass, data: dict, client: Union['Client', 'ClientAsync', None] = None) -> 'OperationsHistoryItem': + def de_json( + cls, + data: dict[str, Any] + ) -> 'OperationsHistoryItem': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.OperationsHistoryItem`: Информацию о предмете в истории операций. + steam_trader.OperationsHistoryItem: Информацию о предмете в истории операций. """ - data = super(OperationsHistoryItem, cls).de_json(data) + data = super(OperationsHistoryItem, cls)._de_json(data) return cls(**data) @@ -363,27 +369,30 @@ class AltWebSocketMessage(TraderClientObject): """Класс, представляющий AltWebSocket сообщение. Attributes: - type (:obj:`int`): Код WebSocket сообщения. - data (:obj:`str`): WebSocket сообщение. + type (int): Код WebSocket сообщения. + data (str): WebSocket сообщение. """ type: int data: str @classmethod - def de_json(cls: dataclass, data: dict, client: Union['Client', 'ClientAsync', None] = None) -> 'AltWebSocketMessage': + def de_json( + cls, + data: dict[str, Any] + ) -> 'AltWebSocketMessage': """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + data (dict): Поля и значения десериализуемого объекта. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. Returns: - :class:`steam_trader.AltWebSocketMessage`: AltWebSocket сообщение. + steam_trader.AltWebSocketMessage: AltWebSocket сообщение. """ - data = super(AltWebSocketMessage, cls).de_json(data) + data = super(AltWebSocketMessage, cls)._de_json(data) return cls(**data) @@ -392,9 +401,9 @@ class MultiBuyOrder(TraderClientObject): """Класс, представляющий предмет из запроса на мульти-покупку. Args: - id (:obj:`int`): Уникальный ID заявки. - itemid (:obj:`int`): ID предмета. - price (:obj:`float`): Цена, за которую был куплен предмет с учётом скидки. + id (int): Уникальный ID заявки. + itemid (int): ID предмета. + price (float): Цена, за которую был куплен предмет с учётом скидки. """ id: int @@ -403,12 +412,11 @@ class MultiBuyOrder(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'MultiBuyOrder': - data = super(MultiBuyOrder, cls).de_json(data) + data = super(MultiBuyOrder, cls)._de_json(data) return cls(**data) @@ -417,20 +425,20 @@ class ItemForExchange(TraderClientObject): """Класс, представляющий информацию о предмете для передачи/получения боту. Attributes: - id (:obj:`int`): ID покупки/продажи. - assetid (:obj:`int`): AssetID предмета в Steam. - gameid (:obj:`int`): AppID приложения в Steam. - contextid (:obj:`int`): ContextID приложения в Steam. - classid (:obj:`int`): Параметр ClassID в Steam. - instanceid (:obj:`int`): Параметр InstanceID в Steam. - gid (:obj:`int`): ID группы предметов. - itemid (:obj:`int`): Уникальный ID предмета. - price (:obj:`float`): Цена предмета, за которую купили/продали, без учета комиссии/скидки. - currency (:obj:`int`): Валюта покупки/продажи. - timer (:obj:`int`): Cколько времени осталось до передачи боту/окончания гарантии. - asset_type (:obj:`int`): Значение 0 - этот предмет для передачи боту. Значение 1 - для приёма предмета от бота. - percent (:obj:`float`): Размер комиссии/скидки в процентах, за которую был продан/куплен предмет. - steam_item (:obj:`bool`): Присутствует ли предмет в вашем инвентаре Steam. + id (int): ID покупки/продажи. + assetid (int): AssetID предмета в Steam. + gameid (int): AppID приложения в Steam. + contextid (int): ContextID приложения в Steam. + classid (int): Параметр ClassID в Steam. + instanceid (int): Параметр InstanceID в Steam. + gid (int): ID группы предметов. + itemid (int): Уникальный ID предмета. + price (float): Цена предмета, за которую купили/продали, без учета комиссии/скидки. + currency (int): Валюта покупки/продажи. + timer (int): Cколько времени осталось до передачи боту/окончания гарантии. + asset_type (int): Значение 0 - этот предмет для передачи боту. Значение 1 - для приёма предмета от бота. + percent (float): Размер комиссии/скидки в процентах, за которую был продан/куплен предмет. + steam_item (bool): Присутствует ли предмет в вашем инвентаре Steam. """ id: int @@ -450,12 +458,11 @@ class ItemForExchange(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'ItemForExchange': - data = super(ItemForExchange, cls).de_json(data) + data = super(ItemForExchange, cls)._de_json(data) return cls(**data) @@ -464,14 +471,14 @@ class TradeDescription(TraderClientObject): """Класс, предстваляющий описание предмета для передачи/получения боту. Attributes: - type (:obj:`str`): Тип предмета. - description (:obj:`str`): Описание предмета. - hash_name (:obj:`str`): Параметр market_hash_name в Steam. - name (:obj:`str`): Локализованное (переведённое) название предмета. - image_small (:obj:`str`): Маленькое изображение предмета. - color (:obj:`str`): Цвет предмета (из Steam). - outline (:obj:`str`): Цвет фильтра предмета (из Steam). - gameid (:obj:`int`): AppID приложения в Steam + type (str): Тип предмета. + description (str): Описание предмета. + hash_name (str): Параметр market_hash_name в Steam. + name (str): Локализованное (переведённое) название предмета. + image_small (str): Маленькое изображение предмета. + color (str): Цвет предмета (из Steam). + outline (str): Цвет фильтра предмета (из Steam). + gameid (int): AppID приложения в Steam """ type: str @@ -485,12 +492,11 @@ class TradeDescription(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'TradeDescription': - data = super(TradeDescription, cls).de_json(data) + data = super(TradeDescription, cls)._de_json(data) return cls(**data) @@ -499,18 +505,18 @@ class ExchangeItem(TraderClientObject): """Класс, представляющий предмет, на который был отправлен обмен. Attributes: - id (:obj:`int`): Уникальный ID заявки. - assetid (:obj:`int`): AssetID предмета в Steam. - gameid (:obj:`int`): AppID приложения в Steam. - contextid (:obj:`int`): ContextID приложения в Steam. - classid (:obj:`int`): ClassID предмета в Steam. - instanceid (:obj:`int`): InstanceID предмета в Steam. - type (:obj:`int`): Значение 0 - предмет для передачи боту, значение 1 - предмет для приема от бота. - itemid (:obj:`int`): ID предмета. - gid (:obj:`int`): Идентификатор группы предметов в нашей базе. - price (:obj:`int`): Цена, за которую предмет был куплен/продан с учётом скидки/комиссии. - currency (:obj:`int`): Валюта покупки/продажи. - percent (:obj:`float`): Размер скидки/комиссии в процентах, с которой был куплен/продан предмет. + id (int): Уникальный ID заявки. + assetid (int): AssetID предмета в Steam. + gameid (int): AppID приложения в Steam. + contextid (int): ContextID приложения в Steam. + classid (int): ClassID предмета в Steam. + instanceid (int): InstanceID предмета в Steam. + type (int): Значение 0 - предмет для передачи боту, значение 1 - предмет для приема от бота. + itemid (int): ID предмета. + gid (int): Идентификатор группы предметов в нашей базе. + price (int): Цена, за которую предмет был куплен/продан с учётом скидки/комиссии. + currency (int): Валюта покупки/продажи. + percent (float): Размер скидки/комиссии в процентах, с которой был куплен/продан предмет. """ id: int @@ -528,11 +534,10 @@ class ExchangeItem(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'ExchangeItem': - data = super(ExchangeItem, cls).de_json(data) + data = super(ExchangeItem, cls)._de_json(data) return cls(**data) diff --git a/steam_trader/api/_offers.py b/steam_trader/api/_offers.py index 0110277..8de452d 100644 --- a/steam_trader/api/_offers.py +++ b/steam_trader/api/_offers.py @@ -1,23 +1,19 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Union +from typing import Any from ._base import TraderClientObject -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class SellOffer(TraderClientObject): """Класс, представляющий информацию о предложении продажи. Attributes: - id (:obj:`int`): Уникальный ID заявки. - classid (:obj:`int`): ClassID предмета в Steam. - instanceid (:obj:`int`): InstanceID предмета в Steam. - itemid (:obj:`int`): ID предмета. - price (:obj:`float`): Цена предложения о покупке/продаже. - currency (:obj:`int`): Валюта покупки/продажи. + id (int): Уникальный ID заявки. + classid (int): ClassID предмета в Steam. + instanceid (int): InstanceID предмета в Steam. + itemid (int): ID предмета. + price (float): Цена предложения о покупке/продаже. + currency (int): Валюта покупки/продажи. """ id: int @@ -29,12 +25,11 @@ class SellOffer(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'SellOffer': - data = super(SellOffer, cls).de_json(data) + data = super(SellOffer, cls)._de_json(data) return cls(**data) @@ -43,9 +38,9 @@ class BuyOffer(TraderClientObject): """Класс, представляющий информацию о запросе на покупку. Attributes: - id (:obj:`int`): ID заявки. - price (:obj:`float`): Цена предложения о покупке/продаже. - currency (:obj:`int`): Валюта покупки/продажи. + id (int): ID заявки. + price (float): Цена предложения о покупке/продаже. + currency (int): Валюта покупки/продажи. """ id: int @@ -54,11 +49,10 @@ class BuyOffer(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'BuyOffer': - data = super(BuyOffer, cls).de_json(data) + data = super(BuyOffer, cls)._de_json(data) return cls(**data) diff --git a/steam_trader/api/_p2p.py b/steam_trader/api/_p2p.py index 31be32d..e085d70 100644 --- a/steam_trader/api/_p2p.py +++ b/steam_trader/api/_p2p.py @@ -1,26 +1,22 @@ from dataclasses import dataclass from collections.abc import Sequence -from typing import TYPE_CHECKING, Union +from typing import Any from ._base import TraderClientObject from ._misc import ExchangeItem -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class P2PTradeOffer(TraderClientObject): """Класс, представляющий данные для совершения p2p трейда. Незадокументированно. Attributes: - sessionid (:obj:`str`): - serverid (:obj:`int`): - partner (:obj:`str`): - tradeoffermessage (:obj:`str`): - json_tradeoffer (:obj:`str`): - captcha (:obj:`str`): - trade_offer_create_params (:obj:`str`): + sessionid (`str`): + serverid (`int`): + partner (`str`): + tradeoffermessage (`str`): + json_tradeoffer (`str`): + captcha (`str`): + trade_offer_create_params (`str`): """ sessionid: str @@ -33,12 +29,11 @@ class P2PTradeOffer(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'P2PTradeOffer': - data = super(P2PTradeOffer, cls).de_json(data) + data = super(P2PTradeOffer, cls)._de_json(data) return cls(**data) @@ -47,20 +42,19 @@ class P2PSendObject(TraderClientObject): """Класс, представляющий ссылку на p2p обмен и сам обмен. Attributes: - trade_link (:obj:`str`): Ссылка для p2p обмена. - trade_offer (:class:`steam_trader.P2PTradeOffer`): Параметры для POST запроса + trade_link (`str`): Ссылка для p2p обмена. + trade_offer (`steam_trader.P2PTradeOffer`): Параметры для POST запроса (https://steamcommunity.com/tradeoffer/new/send) при создании обмена в Steam. Вместо {sessionid} нужно указывать ID своей сессии в Steam. """ trade_link: str - trade_offer: 'P2PTradeOffer' + trade_offer: P2PTradeOffer @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'P2PSendObject': data.update({ # перенос с camleCase на snake_case @@ -71,7 +65,7 @@ def de_json( data['trade_offer'] = P2PTradeOffer.de_json(data['trade_offer']) - data = super(P2PSendObject, cls).de_json(data) + data = super(P2PSendObject, cls)._de_json(data) return cls(**data) @@ -80,22 +74,21 @@ class P2PReceiveObject(TraderClientObject): """Класс, представляющий массив с данными для принятия обмена. Attributes: - offerid (:obj:`int`): ID обмена в Steam. - code (:obj:`str`): Код проверки обмена. + offerid (`int`): ID обмена в Steam. + code (`str`): Код проверки обмена. items (Sequence[`steam_trader.ExchangeItem`]): Список предметов в обмене. - partner_steamid (:obj:`int`): SteamID покупателя. + partner_steamid (`int`): SteamID покупателя. """ offerid: int code: str - items: Sequence['ExchangeItem'] + items: Sequence[ExchangeItem] partner_steamid: int @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'P2PReceiveObject': data.update({ # перенос с camleCase на snake_case @@ -107,7 +100,7 @@ def de_json( for i, item in enumerate(data['items']): data['items'][i] = ExchangeItem.de_json(item) - data = super(P2PReceiveObject, cls).de_json(data) + data = super(P2PReceiveObject, cls)._de_json(data) return cls(**data) @@ -116,9 +109,9 @@ class P2PConfirmObject(TraderClientObject): """Класс, представляющий массив с данными для подтверждения обмена в мобильном аутентификаторе. Attributes: - offerid (:obj:`int`): ID обмена в Steam - code (:obj:`str`): Код проверки обмена - partner_steamid (:obj:`int`) SteamID покупателя + offerid (`int`): ID обмена в Steam + code (`str`): Код проверки обмена + partner_steamid (`int`) SteamID покупателя """ offerid: int @@ -127,9 +120,8 @@ class P2PConfirmObject(TraderClientObject): @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'P2PConfirmObject': data.update({ # перенос с camleCase на snake_case @@ -138,6 +130,6 @@ def de_json( }) del data['offerId'], data['partnerSteamId'] - data = super(P2PConfirmObject, cls).de_json(data) + data = super(P2PConfirmObject, cls)._de_json(data) return cls(**data) diff --git a/steam_trader/api/_sale.py b/steam_trader/api/_sale.py index 05f2dfb..fb9266b 100644 --- a/steam_trader/api/_sale.py +++ b/steam_trader/api/_sale.py @@ -1,29 +1,23 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional, Union +from typing import Optional, Any from steam_trader import exceptions from ._base import TraderClientObject -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class SellResult(TraderClientObject): """Класс, представляющий информацию о выставленном на продажу предмете. Attributes: - success (:obj:`bool`): Результат запроса. - id: (:obj:`int`): ID продажи. - position (:obj:`int`): Позиция предмета в очереди. - fast_execute (:obj:`bool`): Был ли предмет продан моментально. - nc (:obj:`str`): Идентификатор для бескомиссионной продажи предмета. - price (:obj:`float`, optional): Цена, за которую был продан предмет с учетом комиссии. + success (bool): Результат запроса. + id: (int): ID продажи. + position (int): Позиция предмета в очереди. + fast_execute (bool): Был ли предмет продан моментально. + nc (str): Идентификатор для бескомиссионной продажи предмета. + price (float, optional): Цена, за которую был продан предмет с учетом комиссии. Указывается, если 'fast_execute' = True - commission (:obj:`float`, optional): Размер комиссии в процентах, за которую был продан предмет. + commission (float, optional): Размер комиссии в процентах, за которую был продан предмет. Указывается, если 'fast_execute' = True - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. """ success: bool @@ -31,25 +25,17 @@ class SellResult(TraderClientObject): position: int fast_execute: bool nc: str - client: Union['Client', 'ClientAsync', None] price: Optional[float] = None commission: Optional[float] = None @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'SellResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При создании запроса произошла неизвестная ошибка.') case 2: @@ -63,6 +49,6 @@ def de_json( case 6: raise exceptions.AuthenticatorError('Мобильный аутентификатор не подключён или с момента его подключения ещё не прошло 7 дней.') - data = super(SellResult, cls).de_json(data) + data = super(SellResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) diff --git a/steam_trader/api/_trade.py b/steam_trader/api/_trade.py index bc1c2bf..b1ff5d8 100644 --- a/steam_trader/api/_trade.py +++ b/steam_trader/api/_trade.py @@ -1,49 +1,35 @@ from dataclasses import dataclass from collections.abc import Sequence -from typing import TYPE_CHECKING, Union +from typing import Any from .. import exceptions from ._base import TraderClientObject from ._misc import TradeDescription, ItemForExchange, ExchangeItem from ._p2p import P2PConfirmObject, P2PReceiveObject, P2PSendObject -if TYPE_CHECKING: - from ._client import Client - from ._client_async import ClientAsync - @dataclass(slots=True) class ItemsForExchange(TraderClientObject): """Класс, представляющий предметы для обмена с ботом. Attributes: - success (:obj:`bool`): Результат запроса. - items (Sequence[`steam_trader.ItemForExchange`]): Последовательность предметов для обмена с ботом. - descriptions (dict[:obj:`int`, :class:`steam_trader.TradeDescription`]): Описания предметов + success (bool): Результат запроса. + items (Sequence[steam_trader.ItemForExchange]): Последовательность предметов для обмена с ботом. + descriptions (dict[int, steam_trader.TradeDescription]): Описания предметов для обмена с ботом. Ключ - itemid предмета. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. """ success: bool - items: Sequence['ItemForExchange'] - descriptions: dict[int, 'TradeDescription'] - client: Union['Client', 'ClientAsync', None] + items: Sequence[ItemForExchange] + descriptions: dict[int, TradeDescription] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'ItemsForExchange': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При выполнении запроса произошла неизвестная ошибка.') case 2: @@ -56,23 +42,21 @@ def de_json( data['descriptions'] = { int(_id): TradeDescription.de_json(_dict) for _id, _dict in data['descriptions'].items() } - data = super(ItemsForExchange, cls).de_json(data) + data = super(ItemsForExchange, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class ExchangeResult(TraderClientObject): """Класс, представляющий результат инициализации обмена с ботом. Attributes: - success: (:obj:`bool`): Результат запроса. - offer_id (:obj:`int`): ID обмена в Steam. - code (:obj:`str`): Код проверки обмена. - bot_steamid (:obj:`int`): SteamID бота, который отправил обмен. - bot_nick (:obj:`str`): Ник бота. - items (Sequence[:class:`steam_trader.ExchangeItem`]): Cписок предметов для обмена с ботом. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success: (bool): Результат запроса. + offer_id (int): ID обмена в Steam. + code (str): Код проверки обмена. + bot_steamid (int): SteamID бота, который отправил обмен. + bot_nick (str): Ник бота. + items (Sequence[steam_trader.ExchangeItem]): Cписок предметов для обмена с ботом. """ success: bool @@ -80,24 +64,16 @@ class ExchangeResult(TraderClientObject): code: str bot_steamid: int bot_nick: str - items: Sequence['ExchangeItem'] - client: Union['Client', 'ClientAsync', None] + items: Sequence[ExchangeItem] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'ExchangeResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При выполнении запроса произошла неизвестная ошибка.') case 2: @@ -131,48 +107,40 @@ def de_json( for i, item in enumerate(data['items']): data['items'][i] = ExchangeItem.de_json(item) - data = super(ExchangeResult, cls).de_json(data) + data = super(ExchangeResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) @dataclass(slots=True) class ExchangeP2PResult(TraderClientObject): """Класс, представляющий результат инициализации p2p обмена. Attributes: - success (:obj:`bool`): Результат запроса. - send (Sequence[:class:`steam_trader.P2PSendObject`]): Массив с данными для создания + success (bool): Результат запроса. + send (Sequence[steam_trader.P2PSendObject]): Массив с данными для создания нового обмена в Steam. - receive (Sequence[:class:`steam_trader.RecieveObject`]): Массив с данными для принятия обмена. - confirm (Sequence[:class:`steam_trader.ConfirmObject`]): Массив с данными для подтверждения + receive (Sequence[steam_trader.RecieveObject]): Массив с данными для принятия обмена. + confirm (Sequence[steam_trader.ConfirmObject]): Массив с данными для подтверждения обмена в мобильном аутентификаторе. - cancel (Sequence[:obj:`str`]): Массив из ID обменов, которые нужно отменить. - client (Union[:class:`steam_trader.Client`, :class:`steam_trader.ClientAsync`, :obj:`None`]): + cancel (Sequence[str]): Массив из ID обменов, которые нужно отменить. + client (Union[steam_trader.Client, steam_trader.ClientAsync, None]): Клиент Steam Trader. """ success: bool - send: Sequence['P2PSendObject'] - receive: Sequence['P2PReceiveObject'] - confirm: Sequence['P2PConfirmObject'] + send: Sequence[P2PSendObject] + receive: Sequence[P2PReceiveObject] + confirm: Sequence[P2PConfirmObject] cancel: Sequence[str] - client: Union['Client', 'ClientAsync', None] @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['Client', 'ClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'ExchangeP2PResult': if not data['success']: match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') case 1: raise exceptions.InternalError('При выполнении запроса произошла неизвестная ошибка.') case 2: @@ -197,6 +165,6 @@ def de_json( for i, item in enumerate(data['confirm']): data['confirm'][i] = P2PConfirmObject.de_json(item) - data = super(ExchangeP2PResult, cls).de_json(data) + data = super(ExchangeP2PResult, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) diff --git a/steam_trader/api/ext/__init__.py b/steam_trader/api/ext/__init__.py index c05257c..72a7929 100644 --- a/steam_trader/api/ext/__init__.py +++ b/steam_trader/api/ext/__init__.py @@ -1,6 +1,6 @@ """Extended version of api client. -Licensed under the BSD 3-Clause License - Copyright (c) 2024-present, Lemon4ksan (aka Bananchiki) +Licensed under the BSD 3-Clause License - Copyright (c) 2024-2025, Lemon4ksan (aka Bananchiki) See LICENSE """ diff --git a/steam_trader/api/ext/_client_async_ext.py b/steam_trader/api/ext/_client_async_ext.py index 5848b87..c53b9d1 100644 --- a/steam_trader/api/ext/_client_async_ext.py +++ b/steam_trader/api/ext/_client_async_ext.py @@ -1,7 +1,10 @@ -import asyncio +###################################################################################### +# ЭТО АВТОМАТИЧЕСКИ СОЗДАННАЯ КОПИЯ СИНХРОННОГО КЛИЕНТА. НЕ ИЗМЕНЯЙТЕ САМОСТОЯТЕЛЬНО # +###################################################################################### + import logging -import functools -from typing import Optional, Sequence, TypeVar, Callable, Any, LiteralString +from functools import wraps +from typing import Optional, Sequence, Callable, Any, Literal, cast from ._misc import TradeMode, PriceRange @@ -17,12 +20,10 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) -F = TypeVar('F', bound=Callable[..., Any]) - -def log(method: F) -> F: +def log(method: Callable[..., Any]) -> Any: logger = logging.getLogger(method.__module__) - @functools.wraps(method) + @wraps(method) async def wrapper(*args, **kwargs) -> Any: logger.debug(f'Entering: {method.__name__}') @@ -36,16 +37,31 @@ async def wrapper(*args, **kwargs) -> Any: return wrapper + class ExtClientAsync(ClientAsync): """Данный класс представляет расширенную версию обычного клиента. + Args: + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + Используется при каждом запросе на сайт. + + Attributes: + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + Используется при каждом запросе на сайт. + Изменённые методы: - get_inventory - Добавлена возможность указывать фильтр для отсеивания предметов. + get_inventory - Добавлена возможность указывать фильтр для отсеивания предметов + (очень медленно на синхронном клиенте). Новые методы: - multi_sell - Аналог multi_buy. В отличие от него, возвращает последовательность из результатов продаж, а не один объект. + multi_sell - Аналог multi_buy. В отличие от него, возвращает последовательноасть из результатов продаж, а не один объект. set_trade_mode - Позволяет задать режим торговли. Данного метода нет в документации. - get_price_range - Получить размах цен в истории покупок. Проверяет только последние 100 покупок. Raises: BadRequestError: Неправильный запрос. @@ -69,9 +85,9 @@ async def get_inventory( self, gameid: int, *, - filters: Optional['Filters'] = None, - status: Optional[Sequence[int]] = None - ) -> 'Inventory': + filters: Optional[Filters] = None, + status: Optional[Sequence[Literal[0, 1, 2, 3, 4]]] = None + ) -> Inventory: """Получить инвентарь клиента, включая заявки на покупку и купленные предметы. EXT: @@ -80,9 +96,9 @@ async def get_inventory( По умолчанию возвращает список предметов из инвентаря Steam, которые НЕ выставлены на продажу. Args: - gameid (:obj:`int`): AppID приложения в Steam. - filters (:class:`steam_trader.Filters`, optional): Фильтр для отсеивания предметов. - status (Sequence[:obj:`int`], optional): + gameid (int): AppID приложения в Steam. + filters (steam_trader.Filters, optional): Фильтр для отсеивания предметов. + status (Sequence[int], optional): Указывается, чтобы получить список предметов с определенным статусом. Возможные статусы: @@ -95,7 +111,7 @@ async def get_inventory( Если не указавать, вернётся список предметов из инвентаря Steam, которые НЕ выставлены на продажу. Returns: - :class:`steam_trader.Inventory`: Инвентарь клиента, включая заявки на покупку и купленные предметы. + Inventory: Инвентарь клиента, включая заявки на покупку и купленные предметы. Raises: UnsupportedAppID: Указан недействительный gameid. @@ -104,8 +120,6 @@ async def get_inventory( if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'getinventory/' params = {"gameid": gameid} if status is not None: @@ -114,67 +128,61 @@ async def get_inventory( raise ValueError(f'Неизвестный статус {s}') params[f'status[{i}]'] = s - result = await self._async_client.get( - url, - params=params, - headers=self.headers - ) - inventory = Inventory.de_json(result.json(), status, self) + result = await self._get_request('getinventory/', params=params) + inventory = Inventory.de_json(result, status) if filters is not None: - tasks = [self.get_item_info(item.gid) for item in inventory.items] - responses = await asyncio.gather(*tasks) - + logging.warning('Вы используете синхронный клиент. Запрос с фильтрами может занять до 2 минут. Если хотите ускорить время, используйте асинхронную версию.') new_items = [] - for i, item in enumerate(inventory.items): - item_filters = responses[i].filters - if filters.quality is not None: + for item in inventory.items: + item_filters = cast('Filters', (await self.get_item_info(item.gid)).filters) + if filters.quality is not None and item_filters.quality is not None: required_filters_list = [_filter.id for _filter in filters.quality] item_filters_list = [_filter.id for _filter in item_filters.quality] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.type is not None: + if filters.type is not None and item_filters.type is not None: required_filters_list = [_filter.id for _filter in filters.type] item_filters_list = [_filter.id for _filter in item_filters.type] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.used_by is not None: + if filters.used_by is not None and item_filters.used_by is not None: required_filters_list = [_filter.id for _filter in filters.used_by] item_filters_list = [_filter.id for _filter in item_filters.used_by] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.craft is not None: + if filters.craft is not None and item_filters.craft is not None: required_filters_list = [_filter.id for _filter in filters.craft] item_filters_list = [_filter.id for _filter in item_filters.craft] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.region is not None: + if filters.region is not None and item_filters.region is not None: required_filters_list = [_filter.id for _filter in filters.region] item_filters_list = [_filter.id for _filter in item_filters.region] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.genre is not None: + if filters.genre is not None and item_filters.genre is not None: required_filters_list = [_filter.id for _filter in filters.genre] item_filters_list = [_filter.id for _filter in item_filters.genre] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.mode is not None: + if filters.mode is not None and item_filters.mode is not None: required_filters_list = [_filter.id for _filter in filters.mode] item_filters_list = [_filter.id for _filter in item_filters.mode] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.trade is not None: + if filters.trade is not None and item_filters.trade is not None: required_filters_list = [_filter.id for _filter in filters.trade] item_filters_list = [_filter.id for _filter in item_filters.trade] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.rarity is not None: + if filters.rarity is not None and item_filters.rarity is not None: required_filters_list = [_filter.id for _filter in filters.rarity] item_filters_list = [_filter.id for _filter in item_filters.rarity] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.hero is not None: + if filters.hero is not None and item_filters.hero is not None: required_filters_list = [_filter.id for _filter in filters.hero] item_filters_list = [_filter.id for _filter in item_filters.hero] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): @@ -187,18 +195,18 @@ async def get_inventory( return inventory @log - async def multi_sell(self, gameid: int, gid: int, price: float, count: int) -> Sequence['SellResult']: + async def multi_sell(self, gameid: int, gid: int, price: float, count: int) -> Sequence[SellResult]: """Продать множество вещей из инвенторя с одним gid. Args: - gameid (:obj:`int`): ID инвентаря из которого будет произходить продажа. - gid (:obj:`int`): ID группы предметов. - price (:obj:`int`): Цена для выставления на продажу. - count (:obj:`int`): Количество предметов для продажи. Если число больше чем предметов в инвенторе, + gameid (int): AppID приложения в Steam. + gid (int): ID группы предметов. + price (float): Цена для выставления на продажу. + count (int): Количество предметов для продажи. Если число больше чем предметов в инвенторе, будут проданы те, что имеются. Returns: - Sequence[:class:`steam_trader.SellResult`]: Последовательноасть с результатами продаж. + Sequence[SellResult]: Последовательноасть с результатами продаж. Raises: OfferCreationFail: При создании заявки произошла неизвестная ошибка. @@ -210,31 +218,29 @@ async def multi_sell(self, gameid: int, gid: int, price: float, count: int) -> S или с момента его подключения ещё не прошло 7 дней. """ - inventory = await self.get_inventory(gameid) - tasks = [] + inventory = self.get_inventory(gameid) + results = [] for item in inventory.items: if count == 0: break if item.gid == gid: - tasks.append(self.sell(item.itemid, item.assetid, price)) + results.append(self.sell(item.itemid, item.assetid, price)) count -= 1 - results = await asyncio.gather(*tasks) - return results @log - async def set_trade_mode(self, state: int) -> 'TradeMode': + async def set_trade_mode(self, state: Literal[0, 1]) -> TradeMode: """Задать режим торговли. Args: - state (:obj:`int`): Режим торговли. + state (int): Режим торговли. 0 - Торговля отключена. 1 - Торговля включена. Returns: - :class:`steam_trader.ext.TradeMode`: Режим торговли. + TradeMode: Режим торговли. Raises: ValueError: Недопустимое значение state. @@ -243,27 +249,22 @@ async def set_trade_mode(self, state: int) -> 'TradeMode': if state not in range(2): raise ValueError(f'Недопустимое значение state :: {state}') - url = self.base_url + 'startstoptrading/' - result = await self._async_client.get( - url, - params={"state": state}, - headers=self.headers - ) - return TradeMode.de_json(result.json(), self) + result = await self._get_request('startstoptrading/', params={"state": state}) + return TradeMode.de_json(result) @log - async def get_price_range(self, gid: int, *, mode: LiteralString = 'sell') -> 'PriceRange': - """Получить размах цен в истории покупок. Проверяет только последние 100 покупок. + async def get_price_range(self, gid: int, *, mode: Literal['sell', 'buy', 'history'] = 'sell') -> PriceRange: + """Получить размах цен. Args: - gid (:obj:`int`): ID группы предметов. - mode (:obj:`LiteralString`): Режим получения: + gid (int): ID группы предметов. + mode (str): Режим получения: 'sell' - Цены запросов на продажу. Значение по умолчанию. 'buy' - Цены запросов на покупку. 'history' - Цены из истории продаж. Максимум 100 пунктов. Returns: - :NamedTuple:`PriceRange(lowest: float, highest: float)`: Размах цен в истории покупок. + PriceRange(lowest: float, highest: float): Размах цен в истории покупок. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -272,24 +273,25 @@ async def get_price_range(self, gid: int, *, mode: LiteralString = 'sell') -> 'P """ lowest = highest = None + match mode: case 'sell': - sell_offers = (await self.get_order_book(gid)).sell - for item in sell_offers: + sell_offers = await self.get_order_book(gid) + for item in sell_offers.sell: if lowest is None or item[0] < lowest: lowest = item[0] if highest is None or item[0] > highest: highest = item[0] case 'buy': - buy_offers = (await self.get_order_book(gid)).buy - for item in buy_offers: + buy_offers = await self.get_order_book(gid) + for item in buy_offers.buy: if lowest is None or item[0] < lowest: lowest = item[0] if highest is None or item[0] > highest: highest = item[0] case 'history': - sell_history = (await self.get_item_info(gid)).sell_history - for item in sell_history: + sell_history = await self.get_item_info(gid) + for item in sell_history.sell_history: if lowest is None or item.price < lowest: lowest = item.price if highest is None or item.price > highest: diff --git a/steam_trader/api/ext/_client_ext.py b/steam_trader/api/ext/_client_ext.py index 5f0a782..6550a88 100644 --- a/steam_trader/api/ext/_client_ext.py +++ b/steam_trader/api/ext/_client_ext.py @@ -1,7 +1,6 @@ -import httpx import logging -import functools -from typing import Optional, Sequence, TypeVar, Callable, Any, LiteralString +from functools import wraps +from typing import Optional, Sequence, Callable, Any, Literal, cast from ._misc import TradeMode, PriceRange @@ -17,13 +16,10 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) -F = TypeVar('F', bound=Callable[..., Any]) - - -def log(method: F) -> F: +def log(method: Callable[..., Any]) -> Any: logger = logging.getLogger(method.__module__) - @functools.wraps(method) + @wraps(method) def wrapper(*args, **kwargs) -> Any: logger.debug(f'Entering: {method.__name__}') @@ -37,22 +33,22 @@ def wrapper(*args, **kwargs) -> Any: return wrapper + class ExtClient(Client): """Данный класс представляет расширенную версию обычного клиента. Args: - api_token (:obj:`str`): Уникальный ключ для аутентификации. - steam_api_token (:obj:`str`): Уникальный ключ для аутентификации в SteamWebAPI. - proxy (:obj:`str`, optional): Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. - headers (:obj:`dict`, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. Attributes: - api_token (:obj:`str`): Уникальный ключ для аутентификации. - proxy (:obj:`str`, optional): Прокси для запросов. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. - headers (:obj:`dict`, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. + api_token (str): Уникальный ключ для аутентификации. + proxy (str, optional): Прокси для запросов. + base_url (str, optional): Ссылка на API Steam Trader. + headers (dict, optional): Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. Изменённые методы: @@ -85,9 +81,9 @@ def get_inventory( self, gameid: int, *, - filters: Optional['Filters'] = None, - status: Optional[Sequence[int]] = None - ) -> 'Inventory': + filters: Optional[Filters] = None, + status: Optional[Sequence[Literal[0, 1, 2, 3, 4]]] = None + ) -> Inventory: """Получить инвентарь клиента, включая заявки на покупку и купленные предметы. EXT: @@ -96,9 +92,9 @@ def get_inventory( По умолчанию возвращает список предметов из инвентаря Steam, которые НЕ выставлены на продажу. Args: - gameid (:obj:`int`): AppID приложения в Steam. - filters (:class:`steam_trader.Filters`, optional): Фильтр для отсеивания предметов. - status (Sequence[:obj:`int`], optional): + gameid (int): AppID приложения в Steam. + filters (steam_trader.Filters, optional): Фильтр для отсеивания предметов. + status (Sequence[int], optional): Указывается, чтобы получить список предметов с определенным статусом. Возможные статусы: @@ -111,7 +107,7 @@ def get_inventory( Если не указавать, вернётся список предметов из инвентаря Steam, которые НЕ выставлены на продажу. Returns: - :class:`steam_trader.Inventory`: Инвентарь клиента, включая заявки на покупку и купленные предметы. + Inventory: Инвентарь клиента, включая заявки на покупку и купленные предметы. Raises: UnsupportedAppID: Указан недействительный gameid. @@ -120,8 +116,6 @@ def get_inventory( if gameid not in SUPPORTED_APPIDS: raise UnsupportedAppID(f'Игра с AppID {gameid}, в данный момент не поддерживается.') - - url = self.base_url + 'getinventory/' params = {"gameid": gameid} if status is not None: @@ -130,65 +124,61 @@ def get_inventory( raise ValueError(f'Неизвестный статус {s}') params[f'status[{i}]'] = s - result = (self._httpx_client or httpx).get( - url, - params=params, - headers=self.headers - ).json() - inventory = Inventory.de_json(result, status, self) + result = self._get_request('getinventory/', params=params) + inventory = Inventory.de_json(result, status) if filters is not None: logging.warning('Вы используете синхронный клиент. Запрос с фильтрами может занять до 2 минут. Если хотите ускорить время, используйте асинхронную версию.') new_items = [] for item in inventory.items: - item_filters = self.get_item_info(item.gid).filters - if filters.quality is not None: + item_filters = cast('Filters', self.get_item_info(item.gid).filters) + if filters.quality is not None and item_filters.quality is not None: required_filters_list = [_filter.id for _filter in filters.quality] item_filters_list = [_filter.id for _filter in item_filters.quality] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.type is not None: + if filters.type is not None and item_filters.type is not None: required_filters_list = [_filter.id for _filter in filters.type] item_filters_list = [_filter.id for _filter in item_filters.type] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.used_by is not None: + if filters.used_by is not None and item_filters.used_by is not None: required_filters_list = [_filter.id for _filter in filters.used_by] item_filters_list = [_filter.id for _filter in item_filters.used_by] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.craft is not None: + if filters.craft is not None and item_filters.craft is not None: required_filters_list = [_filter.id for _filter in filters.craft] item_filters_list = [_filter.id for _filter in item_filters.craft] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.region is not None: + if filters.region is not None and item_filters.region is not None: required_filters_list = [_filter.id for _filter in filters.region] item_filters_list = [_filter.id for _filter in item_filters.region] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.genre is not None: + if filters.genre is not None and item_filters.genre is not None: required_filters_list = [_filter.id for _filter in filters.genre] item_filters_list = [_filter.id for _filter in item_filters.genre] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.mode is not None: + if filters.mode is not None and item_filters.mode is not None: required_filters_list = [_filter.id for _filter in filters.mode] item_filters_list = [_filter.id for _filter in item_filters.mode] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.trade is not None: + if filters.trade is not None and item_filters.trade is not None: required_filters_list = [_filter.id for _filter in filters.trade] item_filters_list = [_filter.id for _filter in item_filters.trade] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.rarity is not None: + if filters.rarity is not None and item_filters.rarity is not None: required_filters_list = [_filter.id for _filter in filters.rarity] item_filters_list = [_filter.id for _filter in item_filters.rarity] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): continue - if filters.hero is not None: + if filters.hero is not None and item_filters.hero is not None: required_filters_list = [_filter.id for _filter in filters.hero] item_filters_list = [_filter.id for _filter in item_filters.hero] if not any([required_filter in item_filters_list for required_filter in required_filters_list]): @@ -201,18 +191,18 @@ def get_inventory( return inventory @log - def multi_sell(self, gameid: int, gid: int, price: float, count: int) -> Sequence['SellResult']: + def multi_sell(self, gameid: int, gid: int, price: float, count: int) -> Sequence[SellResult]: """Продать множество вещей из инвенторя с одним gid. Args: - gameid (:obj:`int`): AppID приложения в Steam. - gid (:obj:`int`): ID группы предметов. - price (:obj:`float`): Цена для выставления на продажу. - count (:obj:`int`): Количество предметов для продажи. Если число больше чем предметов в инвенторе, + gameid (int): AppID приложения в Steam. + gid (int): ID группы предметов. + price (float): Цена для выставления на продажу. + count (int): Количество предметов для продажи. Если число больше чем предметов в инвенторе, будут проданы те, что имеются. Returns: - Sequence[:class:`steam_trader.SellResult`]: Последовательноасть с результатами продаж. + Sequence[SellResult]: Последовательноасть с результатами продаж. Raises: OfferCreationFail: При создании заявки произошла неизвестная ошибка. @@ -237,16 +227,16 @@ def multi_sell(self, gameid: int, gid: int, price: float, count: int) -> Sequenc return results @log - def set_trade_mode(self, state: int) -> 'TradeMode': + def set_trade_mode(self, state: Literal[0, 1]) -> TradeMode: """Задать режим торговли. Args: - state (:obj:`int`): Режим торговли. + state (int): Режим торговли. 0 - Торговля отключена. 1 - Торговля включена. Returns: - :class:`steam_trader.ext.TradeMode`: Режим торговли. + TradeMode: Режим торговли. Raises: ValueError: Недопустимое значение state. @@ -255,27 +245,22 @@ def set_trade_mode(self, state: int) -> 'TradeMode': if state not in range(2): raise ValueError(f'Недопустимое значение state :: {state}') - url = self.base_url + 'startstoptrading/' - result = (self._httpx_client or httpx).get( - url, - params={"state": state}, - headers=self.headers - ).json() - return TradeMode.de_json(result, self) + result = self._get_request('startstoptrading/', params={"state": state}) + return TradeMode.de_json(result) @log - def get_price_range(self, gid: int, *, mode: LiteralString = 'sell') -> 'PriceRange': + def get_price_range(self, gid: int, *, mode: Literal['sell', 'buy', 'history'] = 'sell') -> PriceRange: """Получить размах цен. Args: - gid (:obj:`int`): ID группы предметов. - mode (:obj:`LiteralString`): Режим получения: + gid (int): ID группы предметов. + mode (str): Режим получения: 'sell' - Цены запросов на продажу. Значение по умолчанию. 'buy' - Цены запросов на покупку. 'history' - Цены из истории продаж. Максимум 100 пунктов. Returns: - :NamedTuple:`PriceRange(lowest: float, highest: float)`: Размах цен в истории покупок. + PriceRange(lowest: float, highest: float): Размах цен в истории покупок. Raises: InternalError: При выполнении запроса произошла неизвестная ошибка. @@ -287,22 +272,22 @@ def get_price_range(self, gid: int, *, mode: LiteralString = 'sell') -> 'PriceRa match mode: case 'sell': - sell_offers = self.get_order_book(gid).sell - for item in sell_offers: + sell_offers = self.get_order_book(gid) + for item in sell_offers.sell: if lowest is None or item[0] < lowest: lowest = item[0] if highest is None or item[0] > highest: highest = item[0] case 'buy': - buy_offers = self.get_order_book(gid).buy - for item in buy_offers: + buy_offers = self.get_order_book(gid) + for item in buy_offers.buy: if lowest is None or item[0] < lowest: lowest = item[0] if highest is None or item[0] > highest: highest = item[0] case 'history': - sell_history = self.get_item_info(gid).sell_history - for item in sell_history: + sell_history = self.get_item_info(gid) + for item in sell_history.sell_history: if lowest is None or item.price < lowest: lowest = item.price if highest is None or item.price > highest: diff --git a/steam_trader/api/ext/_misc.py b/steam_trader/api/ext/_misc.py index a93f209..f3d479e 100644 --- a/steam_trader/api/ext/_misc.py +++ b/steam_trader/api/ext/_misc.py @@ -1,13 +1,9 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Union +from typing import Any, Optional from steam_trader.api import TraderClientObject from steam_trader import exceptions -if TYPE_CHECKING: - from ._client_ext import ExtClient - from ._client_async_ext import ExtClientAsync - from collections import namedtuple PriceRange = namedtuple('PriceRange', ['lowest', 'highest']) @@ -17,35 +13,22 @@ class TradeMode(TraderClientObject): """Класс, представляющий режим торговли. Attributes: - success (:obj:`bool`): Результат запроса. - state (:obj:`bool`): Режим обычной торговли. - p2p (:obj:`bool`): Режим p2p торговли. - client (Union[:class:`steam_trader.ExtClient`, :class:`steam_trader.ExtClientAsync`, :obj:`None`]): - Клиент Steam Trader. + success (bool): Результат запроса. + state (bool): Режим обычной торговли. + p2p (bool): Режим p2p торговли. """ success: bool - state: bool - p2p: bool - client: Union['ExtClient', 'ExtClientAsync', None] + state: Optional[bool] = None + p2p: Optional[bool] = None @classmethod def de_json( - cls: dataclass, - data: dict, - client: Union['ExtClient', 'ExtClientAsync', None] = None + cls, + data: dict[str, Any] ) -> 'TradeMode': - if not data['success']: - match data['code']: - case 400: - raise exceptions.BadRequestError('Неправильный запрос.') - case 401: - raise exceptions.Unauthorized('Неправильный api-токен.') - case 429: - raise exceptions.TooManyRequests('Вы отправили слишком много запросов.') - - data = super(TradeMode, cls).de_json(data) + data = super(TradeMode, cls)._de_json(data) - return cls(client=client, **data) + return cls(**data) diff --git a/steam_trader/constants.py b/steam_trader/constants.py index 95c20c4..560c2a0 100644 --- a/steam_trader/constants.py +++ b/steam_trader/constants.py @@ -1,10 +1,12 @@ -STEAMGIFT_APPID: int = 753 -CSGO_APPID: int = 730 -TEAM_FORTRESS2_APPID: int = 440 -DOTA2_APPID: int = 570 +from typing import Final, Sequence + +STEAMGIFT_APPID: Final[int] = 753 +CSGO_APPID: Final[int] = 730 +TEAM_FORTRESS2_APPID: Final[int] = 440 +DOTA2_APPID: Final[int] = 570 # CS:GO Временно не поддерживается -SUPPORTED_APPIDS: list[int] = [753, 440, 570] +SUPPORTED_APPIDS: Final[Sequence[int]] = [753, 440, 570] # Web Scraping NAME_BY_APPID: dict[int, str] = { @@ -16,225 +18,225 @@ # Фильтры # TF2 # Качества -TF2_QUALITY_COLLECTORS: int = 263 -TF2_QUALITY_DECORATED: int = 228 -TF2_QUALITY_GENUINE: int = 107 -TF2_QUALITY_HAUNTED: int = 231 -TF2_QUALITY_SELFMADE: int = 348 -TF2_QUALITY_STOCK: int = 274 -TF2_QUALITY_STRANGE: int = 50 -TF2_QUALITY_UNIQUE: int = 28 -TF2_QUALITY_UNUSUAL: int = 232 -TF2_QUALITY_VINTAGE: int = 40 +TF2_QUALITY_COLLECTORS: Final[int] = 263 +TF2_QUALITY_DECORATED: Final[int] = 228 +TF2_QUALITY_GENUINE: Final[int] = 107 +TF2_QUALITY_HAUNTED: Final[int] = 231 +TF2_QUALITY_SELFMADE: Final[int] = 348 +TF2_QUALITY_STOCK: Final[int] = 274 +TF2_QUALITY_STRANGE: Final[int] = 50 +TF2_QUALITY_UNIQUE: Final[int] = 28 +TF2_QUALITY_UNUSUAL: Final[int] = 232 +TF2_QUALITY_VINTAGE: Final[int] = 40 # Типы -TF2_TYPE_ACTION: int = 51 -TF2_TYPE_BUILDING: int = 220 -TF2_TYPE_COSMETIC: int = 39 -TF2_TYPE_CRAFT_ITEM: int = 42 -TF2_TYPE_CRATE: int = 43 -TF2_TYPE_FESTIVIZER: int = 285 -TF2_TYPE_GIFT1: int = 106 -TF2_TYPE_GIFT2: int = 252 -TF2_TYPE_KILLSTREAK_KIT: int = 250 -TF2_TYPE_MELEE: int = 44 -TF2_TYPE_PACKAGE: int = 246 -TF2_TYPE_PARTY_FAVOR: int = 29 -TF2_TYPE_PRIMARY: int = 45 -TF2_TYPE_PRIMARY_PDA: int = 233 -TF2_TYPE_PROFESSIONAL_KILLSTREAK_KIT: int = 259 -TF2_TYPE_RECIPE: int = 46 -TF2_TYPE_SECONDARY_PDA: int = 49 -TF2_TYPE_SECONDARY: int = 41 -TF2_TYPE_SERVER_ENCHANTMENT: int = 47 -TF2_TYPE_SPECIALIZED_KILLSTREAK_KIT: int = 235 -TF2_TYPE_SPELLBOOK_PAGE: int = 302 -TF2_TYPE_STRANGE_FILTER: int = 269 -TF2_TYPE_STRANGE_PART: int = 226 -TF2_TYPE_STRANGIFIER: int = 234 -TF2_TYPE_SUPPLY_CRATE: int = 227 -TF2_TYPE_TAUNT: int = 108 -TF2_TYPE_TF_SPELLTOOL: int = 273 -TF2_TYPE_TOOL: int = 222 -TF2_TYPE_TYPE: int = 223 -TF2_TYPE_UNLOCKED_CRATE: int = 257 -TF2_TYPE_UNUSUALIFIER: int = 303 -TF2_TYPE_USABLE_ITEM: int = 48 -TF2_TYPE_WARPAINT: int = 321 +TF2_TYPE_ACTION: Final[int] = 51 +TF2_TYPE_BUILDING: Final[int] = 220 +TF2_TYPE_COSMETIC: Final[int] = 39 +TF2_TYPE_CRAFT_ITEM: Final[int] = 42 +TF2_TYPE_CRATE: Final[int] = 43 +TF2_TYPE_FESTIVIZER: Final[int] = 285 +TF2_TYPE_GIFT1: Final[int] = 106 +TF2_TYPE_GIFT2: Final[int] = 252 +TF2_TYPE_KILLSTREAK_KIT: Final[int] = 250 +TF2_TYPE_MELEE: Final[int] = 44 +TF2_TYPE_PACKAGE: Final[int] = 246 +TF2_TYPE_PARTY_FAVOR: Final[int] = 29 +TF2_TYPE_PRIMARY: Final[int] = 45 +TF2_TYPE_PRIMARY_PDA: Final[int] = 233 +TF2_TYPE_PROFESSIONAL_KILLSTREAK_KIT: Final[int] = 259 +TF2_TYPE_RECIPE: Final[int] = 46 +TF2_TYPE_SECONDARY_PDA: Final[int] = 49 +TF2_TYPE_SECONDARY: Final[int] = 41 +TF2_TYPE_SERVER_ENCHANTMENT: Final[int] = 47 +TF2_TYPE_SPECIALIZED_KILLSTREAK_KIT: Final[int] = 235 +TF2_TYPE_SPELLBOOK_PAGE: Final[int] = 302 +TF2_TYPE_STRANGE_FILTER: Final[int] = 269 +TF2_TYPE_STRANGE_PART: Final[int] = 226 +TF2_TYPE_STRANGIFIER: Final[int] = 234 +TF2_TYPE_SUPPLY_CRATE: Final[int] = 227 +TF2_TYPE_TAUNT: Final[int] = 108 +TF2_TYPE_TF_SPELLTOOL: Final[int] = 273 +TF2_TYPE_TOOL: Final[int] = 222 +TF2_TYPE_TYPE: Final[int] = 223 +TF2_TYPE_UNLOCKED_CRATE: Final[int] = 257 +TF2_TYPE_UNUSUALIFIER: Final[int] = 303 +TF2_TYPE_USABLE_ITEM: Final[int] = 48 +TF2_TYPE_WARPAINT: Final[int] = 321 # Классы (used_by) -TF2_CLASS_DEMOMAN: int = 33 -TF2_CLASS_ENGINEER: int = 38 -TF2_CLASS_HEAVY: int = 35 -TF2_CLASS_MEDIC: int = 34 -TF2_CLASS_PYRO: int = 36 -TF2_CLASS_SCOUT: int = 30 -TF2_CLASS_SNIPER: int = 31 -TF2_CLASS_SOLDIER: int = 32 -TF2_CLASS_SPY: int = 37 +TF2_CLASS_DEMOMAN: Final[int] = 33 +TF2_CLASS_ENGINEER: Final[int] = 38 +TF2_CLASS_HEAVY: Final[int] = 35 +TF2_CLASS_MEDIC: Final[int] = 34 +TF2_CLASS_PYRO: Final[int] = 36 +TF2_CLASS_SCOUT: Final[int] = 30 +TF2_CLASS_SNIPER: Final[int] = 31 +TF2_CLASS_SOLDIER: Final[int] = 32 +TF2_CLASS_SPY: Final[int] = 37 # Крафт/ковка -TF2_CRAFTABLE: int = 277 -TF2_NOT_CRAFTABLE: int = 278 +TF2_CRAFTABLE: Final[int] = 277 +TF2_NOT_CRAFTABLE: Final[int] = 278 # DOTA2 # Редкости -DOTA2_RARITY_ANCIENT: int = 264 -DOTA2_RARITY_ARCANA: int = 11 -DOTA2_RARITY_COMMON: int = 2 -DOTA2_RARITY_IMMORTAL: int = 20 -DOTA2_RARITY_LEGENDARY: int = 13 -DOTA2_RARITY_MYTHICAL: int = 9 -DOTA2_RARITY_RARE: int = 5 -DOTA2_RARITY_SEASONAL: int = 308 -DOTA2_RARITY_UNCOMMON: int = 8 +DOTA2_RARITY_ANCIENT: Final[int] = 264 +DOTA2_RARITY_ARCANA: Final[int] = 11 +DOTA2_RARITY_COMMON: Final[int] = 2 +DOTA2_RARITY_IMMORTAL: Final[int] = 20 +DOTA2_RARITY_LEGENDARY: Final[int] = 13 +DOTA2_RARITY_MYTHICAL: Final[int] = 9 +DOTA2_RARITY_RARE: Final[int] = 5 +DOTA2_RARITY_SEASONAL: Final[int] = 308 +DOTA2_RARITY_UNCOMMON: Final[int] = 8 # Качества -DOTA2_QUALITY_ASCENDANT: int = 310 -DOTA2_QUALITY_AUSPICIOUS: int = 124 -DOTA2_QUALITY_AUTOGRAPHED: int = 219 -DOTA2_QUALITY_BASE1: int = 266 -DOTA2_QUALITY_BASE2: int = 288 -DOTA2_QUALITY_CORRUPTED: int = 136 -DOTA2_QUALITY_CURSED: int = 147 -DOTA2_QUALITY_ELDER: int = 58 -DOTA2_QUALITY_EXALTED: int = 109 -DOTA2_QUALITY_FROZEN: int = 126 -DOTA2_QUALITY_GENUINE: int = 25 -DOTA2_QUALITY_GLITTER: int = 352 -DOTA2_QUALITY_GOLD: int = 353 -DOTA2_QUALITY_HEROIC: int = 185 -DOTA2_QUALITY_HOLO: int = 354 -DOTA2_QUALITY_INFUSED: int = 286 -DOTA2_QUALITY_INSCRIBED: int = 10 -DOTA2_QUALITY_LEGACY: int = 268 -DOTA2_QUALITY_STANDARD: int = 1 -DOTA2_QUALITY_UNUSUAL: int = 111 +DOTA2_QUALITY_ASCENDANT: Final[int] = 310 +DOTA2_QUALITY_AUSPICIOUS: Final[int] = 124 +DOTA2_QUALITY_AUTOGRAPHED: Final[int] = 219 +DOTA2_QUALITY_BASE1: Final[int] = 266 +DOTA2_QUALITY_BASE2: Final[int] = 288 +DOTA2_QUALITY_CORRUPTED: Final[int] = 136 +DOTA2_QUALITY_CURSED: Final[int] = 147 +DOTA2_QUALITY_ELDER: Final[int] = 58 +DOTA2_QUALITY_EXALTED: Final[int] = 109 +DOTA2_QUALITY_FROZEN: Final[int] = 126 +DOTA2_QUALITY_GENUINE: Final[int] = 25 +DOTA2_QUALITY_GLITTER: Final[int] = 352 +DOTA2_QUALITY_GOLD: Final[int] = 353 +DOTA2_QUALITY_HEROIC: Final[int] = 185 +DOTA2_QUALITY_HOLO: Final[int] = 354 +DOTA2_QUALITY_INFUSED: Final[int] = 286 +DOTA2_QUALITY_INSCRIBED: Final[int] = 10 +DOTA2_QUALITY_LEGACY: Final[int] = 268 +DOTA2_QUALITY_STANDARD: Final[int] = 1 +DOTA2_QUALITY_UNUSUAL: Final[int] = 111 # Типы -DOTA2_TYPE_ANCIENT: int = 371 -DOTA2_TYPE_ANNOUNCER: int = 116 -DOTA2_TYPE_BASE: int = 258 -DOTA2_TYPE_BUNDLE: int = 117 -DOTA2_TYPE_COURIER: int = 18 -DOTA2_TYPE_CURSORS_PACK: int = 15 -DOTA2_TYPE_DIRE_TOWERS: int = 368 -DOTA2_TYPE_EMBLEM: int = 357 -DOTA2_TYPE_EMOTICON_TOOL: int = 287 -DOTA2_TYPE_GEM_OR_RUNE: int = 16 -DOTA2_TYPE_HUD_SKIN: int = 26 -DOTA2_TYPE_LEAGUE: int = 241 -DOTA2_TYPE_LOADING_SCREEN: int = 22 -DOTA2_TYPE_MISC: int = 24 -DOTA2_TYPE_MUSIC: int = 23 -DOTA2_TYPE_PENNANT: int = 152 -DOTA2_TYPE_PLAYER_CARD: int = 225 -DOTA2_TYPE_RADIANT_TOWERS: int = 369 -DOTA2_TYPE_RECIPE: int = 254 -DOTA2_TYPE_RELIC: int = 320 -DOTA2_TYPE_RETIRED_CHEST: int = 66 -DOTA2_TYPE_STICKER: int = 351 -DOTA2_TYPE_STICKER_CAPSULE: int = 355 -DOTA2_TYPE_TAUNT: int = 14 -DOTA2_TYPE_TERRAIN: int = 309 -DOTA2_TYPE_TOOL: int = 3 -DOTA2_TYPE_TREASURE: int = 112 -DOTA2_TYPE_TREASURE_KEY: int = 247 -DOTA2_TYPE_WARD: int = 150 -DOTA2_TYPE_WEARABLE: int = 6 +DOTA2_TYPE_ANCIENT: Final[int] = 371 +DOTA2_TYPE_ANNOUNCER: Final[int] = 116 +DOTA2_TYPE_BASE: Final[int] = 258 +DOTA2_TYPE_BUNDLE: Final[int] = 117 +DOTA2_TYPE_COURIER: Final[int] = 18 +DOTA2_TYPE_CURSORS_PACK: Final[int] = 15 +DOTA2_TYPE_DIRE_TOWERS: Final[int] = 368 +DOTA2_TYPE_EMBLEM: Final[int] = 357 +DOTA2_TYPE_EMOTICON_TOOL: Final[int] = 287 +DOTA2_TYPE_GEM_OR_RUNE: Final[int] = 16 +DOTA2_TYPE_HUD_SKIN: Final[int] = 26 +DOTA2_TYPE_LEAGUE: Final[int] = 241 +DOTA2_TYPE_LOADING_SCREEN: Final[int] = 22 +DOTA2_TYPE_MISC: Final[int] = 24 +DOTA2_TYPE_MUSIC: Final[int] = 23 +DOTA2_TYPE_PENNANT: Final[int] = 152 +DOTA2_TYPE_PLAYER_CARD: Final[int] = 225 +DOTA2_TYPE_RADIANT_TOWERS: Final[int] = 369 +DOTA2_TYPE_RECIPE: Final[int] = 254 +DOTA2_TYPE_RELIC: Final[int] = 320 +DOTA2_TYPE_RETIRED_CHEST: Final[int] = 66 +DOTA2_TYPE_STICKER: Final[int] = 351 +DOTA2_TYPE_STICKER_CAPSULE: Final[int] = 355 +DOTA2_TYPE_TAUNT: Final[int] = 14 +DOTA2_TYPE_TERRAIN: Final[int] = 309 +DOTA2_TYPE_TOOL: Final[int] = 3 +DOTA2_TYPE_TREASURE: Final[int] = 112 +DOTA2_TYPE_TREASURE_KEY: Final[int] = 247 +DOTA2_TYPE_WARD: Final[int] = 150 +DOTA2_TYPE_WEARABLE: Final[int] = 6 # Герои # может позже # SteamGift # Регион -STEAMGIFT_REGION_ASIA: int = 251 -STEAMGIFT_REGION_CHINA: int = 319 -STEAMGIFT_REGION_GLOBAL: int = 73 -STEAMGIFT_REGION_HONG_KONG_OR_TAIWAN: int = 300 -STEAMGIFT_REGION_INDIA: int = 331 -STEAMGIFT_REGION_MIDDLE_EAST: int = 356 -STEAMGIFT_REGION_RUSSIA_CIS: int = 104 -STEAMGIFT_REGION_SOUTH_AFRICA: int = 366 -STEAMGIFT_REGION_SOUTH_AMERICA_OR_BRAZIL: int = 265 -STEAMGIFT_REGION_TURKEY: int = 260 +STEAMGIFT_REGION_ASIA: Final[int] = 251 +STEAMGIFT_REGION_CHINA: Final[int] = 319 +STEAMGIFT_REGION_GLOBAL: Final[int] = 73 +STEAMGIFT_REGION_HONG_KONG_OR_TAIWAN: Final[int] = 300 +STEAMGIFT_REGION_INDIA: Final[int] = 331 +STEAMGIFT_REGION_MIDDLE_EAST: Final[int] = 356 +STEAMGIFT_REGION_RUSSIA_CIS: Final[int] = 104 +STEAMGIFT_REGION_SOUTH_AFRICA: Final[int] = 366 +STEAMGIFT_REGION_SOUTH_AMERICA_OR_BRAZIL: Final[int] = 265 +STEAMGIFT_REGION_TURKEY: Final[int] = 260 # Жанры -STEAMGIFT_GENRE_ACCOUNTING: int = 271 -STEAMGIFT_GENRE_ACTION: int = 74 -STEAMGIFT_GENRE_ADVENTURE: int = 97 -STEAMGIFT_GENRE_ANIMATION_AND_MODELING: int = 270 -STEAMGIFT_GENRE_AUDIO_PRODACTION: int = 242 -STEAMGIFT_GENRE_CASUAL: int = 93 -STEAMGIFT_GENRE_DESIGN_AND_ILLUSTRATION: int = 244 -STEAMGIFT_GENRE_DECUMENTARY: int = 364 -STEAMGIFT_GENRE_EARLY_ACCESS: int = 208 -STEAMGIFT_GENRE_EDUCATION: int = 261 -STEAMGIFT_GENRE_FREE_TO_PLAY: int = 105 -STEAMGIFT_GENRE_GAME_DEVELOPMENT: int = 346 -STEAMGIFT_GENRE_GORE: int = 305 -STEAMGIFT_GENRE_INDIE: int = 75 -STEAMGIFT_GENRE_MASSIVELY_MULTIPLAYER: int = 102 -STEAMGIFT_GENRE_MOVIE: int = 363 -STEAMGIFT_GENRE_NUDITY: int = 306 -STEAMGIFT_GENRE_PHOTO_EDITING: int = 295 -STEAMGIFT_GENRE_RPG: int = 89 -STEAMGIFT_GENRE_RACING: int = 96 -STEAMGIFT_GENRE_SEXUAL_CONTENT: int = 313 -STEAMGIFT_GENRE_SIMULATION: int = 95 -STEAMGIFT_GENRE_SOFTWARE_TRAINING: int = 262 -STEAMGIFT_GENRE_SPORTS: int = 101 -STEAMGIFT_GENRE_STRATEGY: int = 98 -STEAMGIFT_GENRE_UTILITIES: int = 243 -STEAMGIFT_GENRE_VIDEO_PRODACTION: int = 272 -STEAMGIFT_GENRE_VIOLENT: int = 304 -STEAMGIFT_GENRE_WEB_PUBLISHING: int = 245 +STEAMGIFT_GENRE_ACCOUNTING: Final[int] = 271 +STEAMGIFT_GENRE_ACTION: Final[int] = 74 +STEAMGIFT_GENRE_ADVENTURE: Final[int] = 97 +STEAMGIFT_GENRE_ANIMATION_AND_MODELING: Final[int] = 270 +STEAMGIFT_GENRE_AUDIO_PRODACTION: Final[int] = 242 +STEAMGIFT_GENRE_CASUAL: Final[int] = 93 +STEAMGIFT_GENRE_DESIGN_AND_ILLUSTRATION: Final[int] = 244 +STEAMGIFT_GENRE_DECUMENTARY: Final[int] = 364 +STEAMGIFT_GENRE_EARLY_ACCESS: Final[int] = 208 +STEAMGIFT_GENRE_EDUCATION: Final[int] = 261 +STEAMGIFT_GENRE_FREE_TO_PLAY: Final[int] = 105 +STEAMGIFT_GENRE_GAME_DEVELOPMENT: Final[int] = 346 +STEAMGIFT_GENRE_GORE: Final[int] = 305 +STEAMGIFT_GENRE_INDIE: Final[int] = 75 +STEAMGIFT_GENRE_MASSIVELY_MULTIPLAYER: Final[int] = 102 +STEAMGIFT_GENRE_MOVIE: Final[int] = 363 +STEAMGIFT_GENRE_NUDITY: Final[int] = 306 +STEAMGIFT_GENRE_PHOTO_EDITING: Final[int] = 295 +STEAMGIFT_GENRE_RPG: Final[int] = 89 +STEAMGIFT_GENRE_RACING: Final[int] = 96 +STEAMGIFT_GENRE_SEXUAL_CONTENT: Final[int] = 313 +STEAMGIFT_GENRE_SIMULATION: Final[int] = 95 +STEAMGIFT_GENRE_SOFTWARE_TRAINING: Final[int] = 262 +STEAMGIFT_GENRE_SPORTS: Final[int] = 101 +STEAMGIFT_GENRE_STRATEGY: Final[int] = 98 +STEAMGIFT_GENRE_UTILITIES: Final[int] = 243 +STEAMGIFT_GENRE_VIDEO_PRODACTION: Final[int] = 272 +STEAMGIFT_GENRE_VIOLENT: Final[int] = 304 +STEAMGIFT_GENRE_WEB_PUBLISHING: Final[int] = 245 # Mode -STEAMGIFT_MODE_ADDITIONAL_HIGH_QUALITY_AUDIO: int = 358 -STEAMGIFT_MODE_CAPTIONS_AVAILABLE: int = 98 -STEAMGIFT_MODE_CO_OP: int = 84 -STEAMGIFT_MODE_OMMENTARY_AVAILABLE: int = 100 -STEAMGIFT_MODE_CROSS_PLATFORM_MULTIPLAYER: int = 90 -STEAMGIFT_MODE_DOWNLOADABLE_CONTENT: int = 87 -STEAMGIFT_MODE_FAMILY_SHARING: int = 372 -STEAMGIFT_MODE_FULL_CONTROLLER_SUPPORT: int = 94 -STEAMGIFT_MODE_INAPP_PURCHASES: int = 291 -STEAMGIFT_MODE_INCLUDES_SOURCE_SDK: int = 99 -STEAMGIFT_MODE_INCLUDES_LEVEL_EDITOR: int = 92 -STEAMGIFT_MODE_LAN_CO_OP: int = 330 -STEAMGIFT_MODE_LAN_PVP: int = 329 -STEAMGIFT_MODE_MMO: int = 103 -STEAMGIFT_MODE_MULTIPLAYER: int = 76 -STEAMGIFT_MODE_NATIVE_STEAM_CONTROLLER_SUPPORT: int = 284 -STEAMGIFT_MODE_ONLINE_CO_OP: int = 292 -STEAMGIFT_MODE_ONLINE_PVP: int = 294 -STEAMGIFT_MODE_PARTIAL_CONTROLLER_SUPPORT: int = 79 -STEAMGIFT_MODE_PVP: int = 324 -STEAMGIFT_MODE_REMOTE_PLAY_TOGETHER: int = 328 -STEAMGIFT_MODE_REMOTE_PLAY_ON_PHONE: int = 325 -STEAMGIFT_MODE_REMOTE_PLAY_ON_TV: int = 327 -STEAMGIFT_MODE_REMOTE_PLAY_ON_TABLET: int = 236 -STEAMGIFT_MODE_SHARED_OR_SPLIT_SCREEN: int = 85 -STEAMGIFT_MODE_SHARED_OR_SPLIT_SCREEN_CO_OP: int = 293 -STEAMGIFT_MODE_SHARED_OR_SPLIT_SCREEN_PVP: int = 296 -STEAMGIFT_MODE_SINGLEPLAYER: int = 83 -STEAMGIFT_MODE_STATS: int = 82 -STEAMGIFT_MODE_STEAM_ACHIVEMENTS: int = 77 -STEAMGIFT_MODE_STEAM_CLOUD: int = 80 -STEAMGIFT_MODE_STEAM_LEADERBOARDS: int = 86 -STEAMGIFT_MODE_STEAM_TIMELINE: int = 374 -STEAMGIFT_MODE_STEAM_TRADING_CARDS: int = 78 -STEAMGIFT_MODE_STEAM_TURN_NOTIFICATIONS: int = 218 -STEAMGIFT_MODE_STEAM_WORKSHOP1: int = 91 -STEAMGIFT_MODE_STEAM_WORKSHOP2: int = 341 -STEAMGIFT_MODE_STEAM_VR_COLLECTABLES: int = 343 -STEAMGIFT_MODE_TRACKED_CONTROLLER_SUPPORT: int = 359 -STEAMGIFT_MODE_VR_ONLY: int = 361 -STEAMGIFT_MODE_VR_SUPPORT: int = 240 -STEAMGIFT_MODE_VR_SUPPORTED: int = 360 -STEAMGIFT_MODE_VALVE_ANTICHEAT_ENABLED: int = 81 +STEAMGIFT_MODE_ADDITIONAL_HIGH_QUALITY_AUDIO: Final[int] = 358 +STEAMGIFT_MODE_CAPTIONS_AVAILABLE: Final[int] = 98 +STEAMGIFT_MODE_CO_OP: Final[int] = 84 +STEAMGIFT_MODE_OMMENTARY_AVAILABLE: Final[int] = 100 +STEAMGIFT_MODE_CROSS_PLATFORM_MULTIPLAYER: Final[int] = 90 +STEAMGIFT_MODE_DOWNLOADABLE_CONTENT: Final[int] = 87 +STEAMGIFT_MODE_FAMILY_SHARING: Final[int] = 372 +STEAMGIFT_MODE_FULL_CONTROLLER_SUPPORT: Final[int] = 94 +STEAMGIFT_MODE_INAPP_PURCHASES: Final[int] = 291 +STEAMGIFT_MODE_INCLUDES_SOURCE_SDK: Final[int] = 99 +STEAMGIFT_MODE_INCLUDES_LEVEL_EDITOR: Final[int] = 92 +STEAMGIFT_MODE_LAN_CO_OP: Final[int] = 330 +STEAMGIFT_MODE_LAN_PVP: Final[int] = 329 +STEAMGIFT_MODE_MMO: Final[int] = 103 +STEAMGIFT_MODE_MULTIPLAYER: Final[int] = 76 +STEAMGIFT_MODE_NATIVE_STEAM_CONTROLLER_SUPPORT: Final[int] = 284 +STEAMGIFT_MODE_ONLINE_CO_OP: Final[int] = 292 +STEAMGIFT_MODE_ONLINE_PVP: Final[int] = 294 +STEAMGIFT_MODE_PARTIAL_CONTROLLER_SUPPORT: Final[int] = 79 +STEAMGIFT_MODE_PVP: Final[int] = 324 +STEAMGIFT_MODE_REMOTE_PLAY_TOGETHER: Final[int] = 328 +STEAMGIFT_MODE_REMOTE_PLAY_ON_PHONE: Final[int] = 325 +STEAMGIFT_MODE_REMOTE_PLAY_ON_TV: Final[int] = 327 +STEAMGIFT_MODE_REMOTE_PLAY_ON_TABLET: Final[int] = 236 +STEAMGIFT_MODE_SHARED_OR_SPLIT_SCREEN: Final[int] = 85 +STEAMGIFT_MODE_SHARED_OR_SPLIT_SCREEN_CO_OP: Final[int] = 293 +STEAMGIFT_MODE_SHARED_OR_SPLIT_SCREEN_PVP: Final[int] = 296 +STEAMGIFT_MODE_SINGLEPLAYER: Final[int] = 83 +STEAMGIFT_MODE_STATS: Final[int] = 82 +STEAMGIFT_MODE_STEAM_ACHIVEMENTS: Final[int] = 77 +STEAMGIFT_MODE_STEAM_CLOUD: Final[int] = 80 +STEAMGIFT_MODE_STEAM_LEADERBOARDS: Final[int] = 86 +STEAMGIFT_MODE_STEAM_TIMELINE: Final[int] = 374 +STEAMGIFT_MODE_STEAM_TRADING_CARDS: Final[int] = 78 +STEAMGIFT_MODE_STEAM_TURN_NOTIFICATIONS: Final[int] = 218 +STEAMGIFT_MODE_STEAM_WORKSHOP1: Final[int] = 91 +STEAMGIFT_MODE_STEAM_WORKSHOP2: Final[int] = 341 +STEAMGIFT_MODE_STEAM_VR_COLLECTABLES: Final[int] = 343 +STEAMGIFT_MODE_TRACKED_CONTROLLER_SUPPORT: Final[int] = 359 +STEAMGIFT_MODE_VR_ONLY: Final[int] = 361 +STEAMGIFT_MODE_VR_SUPPORT: Final[int] = 240 +STEAMGIFT_MODE_VR_SUPPORTED: Final[int] = 360 +STEAMGIFT_MODE_VALVE_ANTICHEAT_ENABLED: Final[int] = 81 # Обмен -STEAMGIFT_TRADABLE: int = 276 -STEAMGIFT_NOT_TRADABLE: int = 281 +STEAMGIFT_TRADABLE: Final[int] = 276 +STEAMGIFT_NOT_TRADABLE: Final[int] = 281 diff --git a/steam_trader/web/__init__.py b/steam_trader/web/__init__.py index 4acfa14..ed3404e 100644 --- a/steam_trader/web/__init__.py +++ b/steam_trader/web/__init__.py @@ -4,7 +4,7 @@ Web scraper for Steam-Trader. Requires beutifulsoup4 and lxml -Licensed under the BSD 3-Clause License - Copyright (c) 2024-present, Lemon4ksan (aka Bananchiki) +Licensed under the BSD 3-Clause License - Copyright (c) 2024-2025, Lemon4ksan (aka Bananchiki) See LICENSE """ @@ -24,6 +24,7 @@ __all__ = [ 'WebClientObject', 'WebClient', + 'WebClientAsync', 'MainPage', 'MainPageItem', 'ItemDescription', diff --git a/steam_trader/web/_base.py b/steam_trader/web/_base.py index 3ec4a50..29f8dae 100644 --- a/steam_trader/web/_base.py +++ b/steam_trader/web/_base.py @@ -2,11 +2,8 @@ import logging import dataclasses from abc import ABCMeta -from dataclasses import dataclass from collections.abc import MutableSequence -from typing import TypeVar - -_WT = TypeVar("_WT") +from typing import Self class WebClientObject: @@ -14,8 +11,9 @@ class WebClientObject: __metaclass__ = ABCMeta - def remove_html_tags(self, __obj: _WT = '__dataclass__', *, replace_p_with='\n') -> _WT: + def remove_html_tags(self, __obj: str | dict | MutableSequence | Self = '__dataclass__', *, replace_p_with='\n') -> str | dict | MutableSequence | Self: """Преобразует словари, изменяемые последовательности, классы и строки в читабельный формат, без HTML тегов. + Также заменяет теги на гиперссылки для отправки в Телеграм. Аргумент ``replace_p_with`` заменяет теги

на введённый символ. По умолчанию новая строка. @@ -43,16 +41,19 @@ def remove_html_tags(self, __obj: _WT = '__dataclass__', *, replace_p_with='\n') return __obj @classmethod - def de_json(cls: dataclass, data: dict) -> dict: + def _de_json(cls, data: dict) -> dict: """Десериализация объекта. Args: - data (:obj:`dict`): Поля и значения десериализуемого объекта. + data (dict): Поля и значения десериализуемого объекта. Returns: - :obj:`dict`, optional: Словарь с валидными аттрибутами для создания датакласса. + dict, optional: Словарь с валидными аттрибутами для создания датакласса. """ + if not dataclasses.is_dataclass(cls): + raise TypeError("Ожидался датакласс.") + data = data.copy() fields = {f.name for f in dataclasses.fields(cls)} diff --git a/steam_trader/web/_client.py b/steam_trader/web/_client.py index cc0266d..9643496 100644 --- a/steam_trader/web/_client.py +++ b/steam_trader/web/_client.py @@ -1,13 +1,13 @@ import bs4 import httpx import logging -import functools +from functools import wraps from collections.abc import Sequence, Callable -from typing import Optional, LiteralString, TypeVar, Any +from typing import Optional, Literal, TypeVar, Any, Self, cast from ._base import WebClientObject from ._dataclasses import MainPage, ItemInfo, Referal, HistoryItem from steam_trader import constants -from steam_trader.exceptions import Unauthorized, UnsupportedAppID +from steam_trader.exceptions import * logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -17,8 +17,8 @@ def log(method: F) -> F: logger = logging.getLogger(method.__module__) - @functools.wraps(method) - def wrapper(*args, **kwargs) -> Any: + @wraps(method) + def wrapper(*args, **kwargs): logger.debug(f'Entering: {method.__name__}') result = method(*args, **kwargs) @@ -28,7 +28,7 @@ def wrapper(*args, **kwargs) -> Any: return result - return wrapper + return wrapper # type: ignore class WebClient(WebClientObject): @@ -39,21 +39,22 @@ class WebClient(WebClientObject): Это не ошибка https://github.com/encode/httpx/discussions/2931. Args: - sessionid (:obj:`int`), optional: ID сессии. Может быть пустым. - proxy (:obj:`str`, optional): Прокси для запросов. Для использования нужен контекстный менеджер with. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. + sessionid (int, optional): ID сессии. Может быть пустым. + proxy (str, optional): Прокси для запросов. Для использования нужен контекстный менеджер with. + base_url (str, optional): Ссылка на API Steam Trader. **kwargs: Будут переданы httpx клиенту. Например timeout. Attributes: - sessionid (:obj:`int`), optional: ID сессии. - proxy (:obj:`str`, optional): Прокси для запросов. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. + sessionid (int, optional): ID сессии. + proxy (str, optional): Прокси для запросов. + base_url (str, optional): Ссылка на API Steam Trader. """ __slots__ = [ - 'sessionid', 'proxy', - 'base_utl' + 'sessionid', + 'base_url', + 'headers' ] def __init__( @@ -64,7 +65,6 @@ def __init__( base_url: Optional[str] = None, **kwargs ): - self.sessionid = sessionid if base_url is None: @@ -73,39 +73,79 @@ def __init__( self._httpx_client = None self.proxy = proxy - self.kwargs = kwargs + self._kwargs = kwargs - def __enter__(self) -> 'WebClient': - self._httpx_client = httpx.Client(proxy=self.proxy, **self.kwargs) + def __enter__(self) -> Self: + self._httpx_client = httpx.Client(proxy=self.proxy, **self._kwargs) return self - def __exit__(self, exc_type, exc_val, exc_tb): - self._httpx_client.close() + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + if self._httpx_client: + self._httpx_client.close() + + def _get_request( + self, + method: str, + *, + headers: Optional[dict[str, str]] = None, + params: Optional[dict[str, Any]] = None, + cookies: Optional[dict[str, str]] = None, + de_json: bool = True, + **kwargs + ) -> Any: + """Создать GET запрос и вернуть данные. + + Args: + method (str): API метод. + headers (dict[str, str], optional): Заголовки запроса. + params (dict[str, Any], optional): Параметры запроса. + cookies (dict[str, str], optional): Куки запроса. + de_json (bool): Преобразовать запрос в формат для использования. + **kwargs: Будут переданы httpx клиенту. + + Returns: + Any: Ответ сервера. + """ + + url: str = self.base_url + method + result = (self._httpx_client or httpx).get( + url, + headers=headers, + params=params, + cookies=cookies, + **kwargs + ) + if de_json: + return result.json() + return result + @log def get_main_page( - self, - gameid: int, - *, - price_from: int = 0, - price_to: int = 2000, - filters: dict[str, int] = None, - text: Optional[str] = None, - sort: LiteralString = '-rating', - page: int = 1, - items_on_page: int = 24 - ) -> 'MainPage': + self, + gameid: int, + *, + price_from: int = 0, + price_to: int = 2000, + filters: Optional[dict[str, int]] = None, + text: Optional[str] = None, + sort: Literal[ + '-rating', 'rating', '-price', 'price', '-benefit', 'benefit', '-name', 'name' + ] = '-rating', + page: int = 1, + items_on_page: int = 24 + ) -> MainPage: """Получить главную страницу покупки для игры. Args: - gameid (:obj:`int`): AppID игры. - price_from (:obj:`int`): Минимальная цена предмета. - price_to (:obj:`int`): Максимальная цена предмета. + gameid (int): AppID игры. + price_from (int): Минимальная цена предмета. + price_to (int): Максимальная цена предмета. Если больше или равно 2000, ограничение снимается. - filters (:obj:`dict[str, int]`, optional): Словарь пар название/ID. + filters (dict[str, int], optional): Словарь пар название/ID. См web_api.api.Filters для названий и web_api.constants для ID. - text (:obj:`str`, optional): Текст, который должен встречаться в названии. - sort (:obj:`LiteralString`): Метод сортировки. + text (str, optional): Текст, который должен встречаться в названии. + sort (Literal): Метод сортировки. '-rating' - Сначала самые популярные. По умолчанию. 'rating' - Сначала менее популярные. '-price' - Сначала самые дорогие. @@ -114,12 +154,12 @@ def get_main_page( 'benefit' - Сначала самые выгодные. '-name' - В алфавитном порядке UNICODE. 'name' - В обратном алфавитном порядке UNICODE. - page (:obj:`int`): Номер страницы, начиная с 1. - items_on_page (:obj:`int`): Кол-во отображаемых предметов на странице. + page (int): Номер страницы, начиная с 1. + items_on_page (int): Кол-во отображаемых предметов на странице. Значение должно быть в диапазоне от 24 до 120 с интервалом 6. Returns: - :class:`steam_trader.web_api.MainPage`: Главная страница покупки. + MainPage: Главная страница покупки. """ try: @@ -130,33 +170,30 @@ def get_main_page( if items_on_page not in range(24, 121) or items_on_page % 6 != 0: logging.warning(f'Неправильное значение items_on_page >> {items_on_page}') - if sort not in [None, '-rating', 'rating', '-price', 'price', '-benefit', 'benefit', '-name', 'name']: + if sort not in ['-rating', 'rating', '-price', 'price', '-benefit', 'benefit', '-name', 'name']: logging.warning(f'Неправильное значение sort >> {sort}') if filters is None: filters = {} - - url = self.base_url + game_name + '/' - result = (self._httpx_client or httpx).get( - url, - headers={ - 'x-pjax': 'true', - 'x-requested-with': 'XMLHttpRequest', - 'x-pjax-container': 'form.market .items .wrap' - }, - params={ - 'price_from': price_from, - 'price_to': price_to, - **filters, - 'text': text, - 'sort': sort, - 'page': page, - }, - cookies={ - 'sid': self.sessionid, - 'settings': f'%7B%22market_{gameid}_onPage%22%3A{items_on_page}%7D' - } - ).json() + + headers={ + 'x-pjax': 'true', + 'x-requested-with': 'XMLHttpRequest', + 'x-pjax-container': 'form.market .items .wrap' + } + params={ + 'price_from': price_from, + 'price_to': price_to, + **filters, + 'text': text, + 'sort': sort, + 'page': page, + } + cookies={ + 'sid': self.sessionid, + 'settings': f'%7B%22market_{gameid}_onPage%22%3A{items_on_page}%7D' + } + result = self._get_request(game_name + '/', headers=headers, params=params, cookies=cookies) return MainPage.de_json(result) @@ -167,42 +204,40 @@ def get_item_info( *, page: int = 1, items_on_page: int = 24 - ) -> 'ItemInfo': + ) -> ItemInfo: """Получить информацию о предмете через WebAPI. Позволяет увидеть описание индивидуальных предметов. Args: - gid (:obj:`int`): ID группы предметов. - page (:obj:`int`): Номер страницы. - items_on_page (:obj:`int`): Кол-во предметов на странице. + gid (int): ID группы предметов. + page (int): Номер страницы. + items_on_page (int): Кол-во предметов на странице. Значение должно быть в диапазоне от 24 до 120 с интервалом 6. Returns: - :class:`steam_trader.web_api.ItemInfo`: Информацию о предмете. + ItemInfo: Информация о предмете. """ if items_on_page not in range(24, 121) or items_on_page % 6 != 0: - logging.warning(f'Неправильное значение items_on_page >> {items_on_page}') + logging.warning(f"Неправильное значение items_on_page '{items_on_page}'") url = f'{self.base_url}tf2/{gid}-The-Wrap-Assassin' # Сайт перенаправляет на корректную страницу - correct_url = (self._httpx_client or httpx).get( + correct_url = self._get_request( url, follow_redirects=True - ).url - - result = (self._httpx_client or httpx).get( - correct_url, - headers={ - 'x-pjax': 'true', - 'x-requested-with': 'XMLHttpRequest', - 'x-pjax-container': '#content #wrapper' - }, - params={'page': page}, - cookies={ - 'sid': self.sessionid, - 'settings': f'%7B%22item_onPage%22%3A{items_on_page}%7D' - } - ).json() - + ) + + headers={ + 'x-pjax': 'true', + 'x-requested-with': 'XMLHttpRequest', + 'x-pjax-container': '#content #wrapper' + } + params={'page': page} + cookies={ + 'sid': self.sessionid, + 'settings': f'%7B%22item_onPage%22%3A{items_on_page}%7D' + } + + result = self._get_request(str(correct_url.url), headers=headers, params=params, cookies=cookies) return ItemInfo.de_json(result) @log @@ -210,44 +245,42 @@ def get_referral_link(self) -> str: """Получить реферальную ссылку. Returns: - :obj:`str`: Реферальная ссылка. + str: Реферальная ссылка. """ if not self.sessionid: raise Unauthorized('Для использования данного метода нужно указать sessionid (sid). Вы можете найти его в файлах куки.') - url = self.base_url + 'referral/' - result = (self._httpx_client or httpx).get( - url, - headers={ + headers={ 'x-pjax': 'true', 'x-requested-with': 'XMLHttpRequest', 'x-pjax-container': '#content #wrapper' - }, - cookies={'sid': self.sessionid} - ).json() - + } + cookies={'sid': self.sessionid} + + result = self._get_request('referral/', headers=headers, cookies=cookies) html = bs4.BeautifulSoup(result['contents'], 'lxml') - return html.find('input', {'class': 'big'}).get('value') + tag = cast(bs4.Tag, html.find('input', {'class': 'big'})) + return cast(str, tag.get('value')) @log def get_referals( self, - status: Optional[int] = None, + status: Optional[Literal[1, 2]] = None, items_on_page: int = 24 ) -> Sequence[Referal]: """Получить список рефералов. Args: - status (:obj:`int`): Статус реферала. + status (int): Статус реферала. None - Все. По умолчанию. 1 - Активный. 2 - Пассивный. - items_on_page (:obj:`int`): Кол-во рефералов на странице. + items_on_page (int): Кол-во рефералов на странице. Значение должно быть в диапазоне от 24 до 120. Returns: - Sequence[:class:`steam_trader.web_api.Referal`]: Список рефералов. + Sequence[Referal]: Список рефералов. """ if not self.sessionid: @@ -256,18 +289,15 @@ def get_referals( if items_on_page not in range(24, 121) or items_on_page % 6 != 0: logging.warning(f'Неправильное значение items_on_page >> {items_on_page}') - url = self.base_url + 'referral/' - result = (self._httpx_client or httpx).get( - url, - headers={ + headers={ 'x-pjax': 'true', 'x-requested-with': 'XMLHttpRequest', 'x-pjax-container': '#content #wrapper' - }, - params={'type': status}, - cookies={'sid': self.sessionid, 'settings': f'%7B%22referral_onPage%22%3A{items_on_page}%7D'} - ).json() - + } + params={'type': status} + cookies={'sid': self.sessionid, 'settings': f'%7B%22referral_onPage%22%3A{items_on_page}%7D'} + + result = self._get_request('referral/', headers=headers, params=params, cookies=cookies) html = bs4.BeautifulSoup(result['contents'], 'lxml') tds = html.find_all('td') @@ -283,23 +313,23 @@ def get_referals( 'status': divs[1].text, 'sum': divs[1].text } - referals.append(Referal.de_json(data)) + referals.append(Referal._de_json(data)) return referals @log - def get_history_page(self, gameid: int, category: LiteralString = 'last') -> Sequence['HistoryItem']: + def get_history_page(self, gameid: int, category: Literal['last', 'day_most', 'all_time_most'] = 'last') -> Sequence[HistoryItem]: """Получить страницу истории продаж. Args: - gameid (:obj:`int`): AppID игры. - category (:obj:`LiteralString`): Категория истории. + gameid (int): AppID игры. + category (str): Категория истории. 'last': Последние покупки. По умолчанию. 'day_most': Самые дорогие за 24 часа. 'all_time_most': Самые дорогие за все время. Returns: - Sequence[:class:`web_api.web_api.HistoryItem`] + Sequence[HistoryItem]: Последовательность предметов из истории. """ try: @@ -317,8 +347,8 @@ def get_history_page(self, gameid: int, category: LiteralString = 'last') -> Seq case _: raise ValueError('Указано недопустимое значение category.') - url = f'https://steam-trader.com/{game_name}/history/' - page = (self._httpx_client or httpx).get(url) + url = f'{self.base_url}{game_name}/history/' + page = self._get_request(url, de_json=False) html = bs4.BeautifulSoup(page.content, 'lxml') diff --git a/steam_trader/web/_client_async.py b/steam_trader/web/_client_async.py index bafeeb0..c78a33a 100644 --- a/steam_trader/web/_client_async.py +++ b/steam_trader/web/_client_async.py @@ -1,14 +1,17 @@ +###################################################################################### +# ЭТО АВТОМАТИЧЕСКИ СОЗДАННАЯ КОПИЯ СИНХРОННОГО КЛИЕНТА. НЕ ИЗМЕНЯЙТЕ САМОСТОЯТЕЛЬНО # +###################################################################################### + import bs4 import httpx import logging -import functools +from functools import wraps from collections.abc import Sequence, Callable -from typing import Optional, LiteralString, TypeVar, Any - +from typing import Optional, Literal, TypeVar, Any, Self, cast from ._base import WebClientObject from ._dataclasses import MainPage, ItemInfo, Referal, HistoryItem from steam_trader import constants -from steam_trader.exceptions import Unauthorized, UnsupportedAppID +from steam_trader.exceptions import * logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -18,8 +21,8 @@ def log(method: F) -> F: logger = logging.getLogger(method.__module__) - @functools.wraps(method) - async def wrapper(*args, **kwargs) -> Any: + @wraps(method) + async def wrapper(*args, **kwargs): logger.debug(f'Entering: {method.__name__}') result = await method(*args, **kwargs) @@ -29,31 +32,33 @@ async def wrapper(*args, **kwargs) -> Any: return result - return wrapper + return wrapper # type: ignore + class WebClientAsync(WebClientObject): """Этот клиент позволяет получить данные сайта без API ключа или получить информацию, которая недоступна через API. - Для некоторых методов необходимо указать ID сессии. Он сбрасывается раз в неделю и находится в файлах куки (или хедерах). + Для некоторых методов необходимо указать ID сессии. Он находится в файлах куки (или хедерах) и переодически сбрасывается. Если вам не нравятся предупреждения об устаревании от httpx, то повысьте уровень логов модуля. Это не ошибка https://github.com/encode/httpx/discussions/2931. Args: - sessionid (:obj:`int`), optional: ID сессии. Может быть пустым. - proxy (:obj:`str`, optional): Прокси для запросов. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. + sessionid (int, optional): ID сессии. Может быть пустым. + proxy (str, optional): Прокси для запросов. Для использования нужен контекстный менеджер with. + base_url (str, optional): Ссылка на API Steam Trader. **kwargs: Будут переданы httpx клиенту. Например timeout. Attributes: - sessionid (:obj:`int`), optional: ID сессии. - proxy (:obj:`str`, optional): Прокси для запросов. - base_url (:obj:`str`, optional): Ссылка на API Steam Trader. + sessionid (int, optional): ID сессии. + proxy (str, optional): Прокси для запросов. + base_url (str, optional): Ссылка на API Steam Trader. """ __slots__ = [ - 'sessionid', 'proxy', - 'base_utl' + 'sessionid', + 'base_url', + 'headers' ] def __init__( @@ -64,62 +69,103 @@ def __init__( base_url: Optional[str] = None, **kwargs ): - self.sessionid = sessionid if base_url is None: base_url = "https://steam-trader.com/" self.base_url = base_url - self._async_client = None + self._httpx_client = None self.proxy = proxy - self.kwargs = kwargs + self._kwargs = kwargs - async def __aenter__(self) -> 'WebClientAsync': - self._async_client = httpx.AsyncClient(proxy=self.proxy, **self.kwargs) + async def __aenter__(self) -> Self: + self._httpx_client = httpx.AsyncClient(proxy=self.proxy, **self._kwargs) return self - async def __aexit__(self, exc_type, exc_val, exc_tb): - await self._async_client.aclose() + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + if self._httpx_client: + await self._httpx_client.aclose() + + async def _get_request( + self, + method: str, + *, + headers: Optional[dict[str, str]] = None, + params: Optional[dict[str, Any]] = None, + cookies: Optional[dict[str, str]] = None, + de_json: bool = True, + **kwargs + ) -> Any: + """Создать GET запрос и вернуть данные. + + Args: + method (str): API метод. + headers (dict[str, str], optional): Заголовки запроса. + params (dict[str, Any], optional): Параметры запроса. + cookies (dict[str, str], optional): Куки запроса. + de_json (bool): Преобразовать запрос в формат для использования. + **kwargs: Будут переданы httpx клиенту. + Returns: + Any: Ответ сервера. + """ + + if not self._httpx_client: + raise ClientError('Необходимо использовать контекст async with ClientAsync()') + url: str = self.base_url + method + result = await self._httpx_client.get( + url, + headers=headers, + params=params, + cookies=cookies, + **kwargs + ) + + if de_json: + return result.json() + return result + @log async def get_main_page( - self, - gameid: int, - *, - price_from: int = 0, - price_to: int = 2000, - filters: dict[str, int] = None, - text: Optional[str] = None, - sort: LiteralString = '-rating', - page: int = 1, - items_on_page: int = 24 - ) -> 'MainPage': + self, + gameid: int, + *, + price_from: int = 0, + price_to: int = 2000, + filters: Optional[dict[str, int]] = None, + text: Optional[str] = None, + sort: Literal[ + '-rating', 'rating', '-price', 'price', '-benefit', 'benefit', '-name', 'name' + ] = '-rating', + page: int = 1, + items_on_page: int = 24 + ) -> MainPage: """Получить главную страницу покупки для игры. Args: - gameid (:obj:`int`): AppID игры. - price_from (:obj:`int`): Минимальная цена предмета. Только целые числа. - price_to (:obj:`int`): Максимальная цена предмета. Только целые числа. + gameid (int): AppID игры. + price_from (int): Минимальная цена предмета. + price_to (int): Максимальная цена предмета. Если больше или равно 2000, ограничение снимается. - filters (:obj:`dict[str, int]`, optional): Словарь пар название/ID. - См steam_trader.api.Filters для названий и steam_trader.constants для ID. - text (:obj:`str`, optional): Текст, который должен встречаться в названии. - sort (:obj:`LiteralString`): Метод сортировки. + filters (dict[str, int], optional): Словарь пар название/ID. + См web_api.api.Filters для названий и web_api.constants для ID. + text (str, optional): Текст, который должен встречаться в названии. + sort (Literal): Метод сортировки. '-rating' - Сначала самые популярные. По умолчанию. - '+rating' - Сначала менее популярные. + 'rating' - Сначала менее популярные. '-price' - Сначала самые дорогие. - '+price' - Сначала самые дешёвые. + 'price' - Сначала самые дешёвые. '-benefit' - Сначала самые невыгодные. - '+benefit' - Сначала самые выгодные. - 'name' - В обратном алфавитном порядке UNICODE. Это не опечатка. + 'benefit' - Сначала самые выгодные. '-name' - В алфавитном порядке UNICODE. - page (:obj:`int`): Номер страницы, начиная с 1. - items_on_page (:obj:`int`): Кол-во отображаемых предметов на странице. - Значение должно быть в диапазоне от 24 до 120. + 'name' - В обратном алфавитном порядке UNICODE. + page (int): Номер страницы, начиная с 1. + items_on_page (int): Кол-во отображаемых предметов на странице. + Значение должно быть в диапазоне от 24 до 120 с интервалом 6. Returns: - :class:`steam_trader.web.MainPage`: Главная страница покупки. + MainPage: Главная страница покупки. """ try: @@ -130,33 +176,30 @@ async def get_main_page( if items_on_page not in range(24, 121) or items_on_page % 6 != 0: logging.warning(f'Неправильное значение items_on_page >> {items_on_page}') - if sort not in [None, '-rating', '+rating', '-price', '+price', '-benefit', '+benefit', 'name', '-name']: + if sort not in ['-rating', 'rating', '-price', 'price', '-benefit', 'benefit', '-name', 'name']: logging.warning(f'Неправильное значение sort >> {sort}') if filters is None: filters = {} - - url = self.base_url + game_name + '/' - result = (await self._async_client.get( - url, - headers={ - 'x-pjax': 'true', - 'x-requested-with': 'XMLHttpRequest', - 'x-pjax-container': 'form.market .items .wrap' - }, - params={ - 'price_from': price_from, - 'price_to': price_to, - **filters, - 'text': text, - 'sort': sort, - 'page': page, - }, - cookies={ - 'sid': self.sessionid, - 'settings': f'%7B%22market_{gameid}_onPage%22%3A{items_on_page}%7D' - } - )).json() + + headers={ + 'x-pjax': 'true', + 'x-requested-with': 'XMLHttpRequest', + 'x-pjax-container': 'form.market .items .wrap' + } + params={ + 'price_from': price_from, + 'price_to': price_to, + **filters, + 'text': text, + 'sort': sort, + 'page': page, + } + cookies={ + 'sid': self.sessionid, + 'settings': f'%7B%22market_{gameid}_onPage%22%3A{items_on_page}%7D' + } + result = await self._get_request(game_name + '/', headers=headers, params=params, cookies=cookies) return MainPage.de_json(result) @@ -167,41 +210,40 @@ async def get_item_info( *, page: int = 1, items_on_page: int = 24 - ) -> 'ItemInfo': + ) -> ItemInfo: """Получить информацию о предмете через WebAPI. Позволяет увидеть описание индивидуальных предметов. Args: - gid (:obj:`int`): ID группы предметов. - page (:obj:`int`): Номер страницы. - items_on_page (:obj:`int`): Кол-во предметов на странице. - Значение должно быть в диапазоне от 24 до 120. + gid (int): ID группы предметов. + page (int): Номер страницы. + items_on_page (int): Кол-во предметов на странице. + Значение должно быть в диапазоне от 24 до 120 с интервалом 6. Returns: - :class:`steam_trader.web.ItemInfo`: Информацию о предмете. + ItemInfo: Информация о предмете. """ if items_on_page not in range(24, 121) or items_on_page % 6 != 0: - logging.warning(f'Неправильное значение items_on_page >> {items_on_page}') + logging.warning(f"Неправильное значение items_on_page '{items_on_page}'") url = f'{self.base_url}tf2/{gid}-The-Wrap-Assassin' # Сайт перенаправляет на корректную страницу - correct_url = (await self._async_client.get( + correct_url = await self._get_request( url, follow_redirects=True - )).url - result = (await self._async_client.get( - correct_url, - headers={ - 'x-pjax': 'true', - 'x-requested-with': 'XMLHttpRequest', - 'x-pjax-container': '#content #wrapper' - }, - params={'page': page}, - cookies={ - 'sid': self.sessionid, - 'settings': f'%7B%22item_onPage%22%3A{items_on_page}%7D' - } - )).json() - + ) + + headers={ + 'x-pjax': 'true', + 'x-requested-with': 'XMLHttpRequest', + 'x-pjax-container': '#content #wrapper' + } + params={'page': page} + cookies={ + 'sid': self.sessionid, + 'settings': f'%7B%22item_onPage%22%3A{items_on_page}%7D' + } + + result = await self._get_request(str(correct_url.url), headers=headers, params=params, cookies=cookies) return ItemInfo.de_json(result) @log @@ -209,44 +251,42 @@ async def get_referral_link(self) -> str: """Получить реферальную ссылку. Returns: - :obj:`str`: Реферальная ссылка. + str: Реферальная ссылка. """ if not self.sessionid: raise Unauthorized('Для использования данного метода нужно указать sessionid (sid). Вы можете найти его в файлах куки.') - url = self.base_url + 'referral/' - result = (await self._async_client.get( - url, - headers={ + headers={ 'x-pjax': 'true', 'x-requested-with': 'XMLHttpRequest', 'x-pjax-container': '#content #wrapper' - }, - cookies={'sid': self.sessionid} - )).json() - + } + cookies={'sid': self.sessionid} + + result = await self._get_request('referral/', headers=headers, cookies=cookies) html = bs4.BeautifulSoup(result['contents'], 'lxml') - return html.find('input', {'class': 'big'}).get('value') + tag = cast(bs4.Tag, html.find('input', {'class': 'big'})) + return cast(str, tag.get('value')) @log - async def get_referrals( + async def get_referals( self, - status: Optional[int] = None, + status: Optional[Literal[1, 2]] = None, items_on_page: int = 24 ) -> Sequence[Referal]: """Получить список рефералов. Args: - status (:obj:`int`): Статус реферала. + status (int): Статус реферала. None - Все. По умолчанию. 1 - Активный. 2 - Пассивный. - items_on_page (:obj:`int`): Кол-во рефералов на странице. + items_on_page (int): Кол-во рефералов на странице. Значение должно быть в диапазоне от 24 до 120. Returns: - Sequence[:class:`steam_trader.web.Referal`]: Список рефералов. + Sequence[Referal]: Список рефералов. """ if not self.sessionid: @@ -255,18 +295,15 @@ async def get_referrals( if items_on_page not in range(24, 121) or items_on_page % 6 != 0: logging.warning(f'Неправильное значение items_on_page >> {items_on_page}') - url = self.base_url + 'referral/' - result = (await self._async_client.get( - url, - headers={ + headers={ 'x-pjax': 'true', 'x-requested-with': 'XMLHttpRequest', 'x-pjax-container': '#content #wrapper' - }, - params={'type': status}, - cookies={'sid': self.sessionid, 'settings': f'%7B%22referral_onPage%22%3A{items_on_page}%7D'} - )).json() - + } + params={'type': status} + cookies={'sid': self.sessionid, 'settings': f'%7B%22referral_onPage%22%3A{items_on_page}%7D'} + + result = await self._get_request('referral/', headers=headers, params=params, cookies=cookies) html = bs4.BeautifulSoup(result['contents'], 'lxml') tds = html.find_all('td') @@ -282,23 +319,23 @@ async def get_referrals( 'status': divs[1].text, 'sum': divs[1].text } - referals.append(Referal.de_json(data)) + referals.append(Referal._de_json(data)) return referals @log - async def get_history_page(self, gameid: int, category: LiteralString = 'last_purchases') -> Sequence['HistoryItem']: + async def get_history_page(self, gameid: int, category: Literal['last', 'day_most', 'all_time_most'] = 'last') -> Sequence[HistoryItem]: """Получить страницу истории продаж. Args: - gameid (:obj:`int`): AppID игры. - category (:obj:`str`): Категория истории. - 'last_purchases': Последние покупки. По умолчанию. + gameid (int): AppID игры. + category (str): Категория истории. + 'last': Последние покупки. По умолчанию. 'day_most': Самые дорогие за 24 часа. 'all_time_most': Самые дорогие за все время. Returns: - Sequence[:class:`steam_trader.web.HistoryItem`] + Sequence[HistoryItem]: Последовательность предметов из истории. """ try: @@ -307,7 +344,7 @@ async def get_history_page(self, gameid: int, category: LiteralString = 'last_pu raise UnsupportedAppID('Указан недействительный AppID.') match category: - case 'last_purchases': + case 'last': i = 0 case 'day_most': i = 1 @@ -316,8 +353,8 @@ async def get_history_page(self, gameid: int, category: LiteralString = 'last_pu case _: raise ValueError('Указано недопустимое значение category.') - url = f'https://steam-trader.com/{game_name}/history/' - page = await self._async_client.get(url) + url = f'{self.base_url}{game_name}/history/' + page = await self._get_request(url, de_json=False) html = bs4.BeautifulSoup(page.content, 'lxml') diff --git a/steam_trader/web/_dataclasses.py b/steam_trader/web/_dataclasses.py index 373e739..48da1d9 100644 --- a/steam_trader/web/_dataclasses.py +++ b/steam_trader/web/_dataclasses.py @@ -2,9 +2,8 @@ import bs4 from lxml import etree from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional +from typing import Optional, cast from collections.abc import Sequence - from ._base import WebClientObject from steam_trader.exceptions import UnknownItem, NotFoundError @@ -14,16 +13,16 @@ class MainPageItem(WebClientObject): """Класс, представляющий данные предмета на главной странице. Attributes: - benefit (:obj:`bool`): True, если цена ниже чем в steam. - count (:obj:`int`): Кол-во предложений. - description (:obj:`str`): Описание первого предмета. - gid (:obj:`int`): ID группы предметов. - hash_name (:obj:`str`): Параметр hash_name в steam. - image_small (:obj:`str`): Неабсолютная ссылка на маленькое изображение. - name (:obj:`str`): Переведённое название предмета. - outline (:obj:`str`): HEX код цвета названия. - price (:obj:`float`): Цена самого дешёвого предложения. - type (:obj:`int`): Тип/Уровень предмета. + benefit (bool): True, если цена ниже чем в steam. + count (int): Кол-во предложений. + description (str): Описание первого предмета. + gid (int): ID группы предметов. + hash_name (str): Параметр hash_name в steam. + image_small (str): Неабсолютная ссылка на маленькое изображение. + name (str): Переведённое название предмета. + outline (str): HEX код цвета названия. + price (float): Цена самого дешёвого предложения. + type (int): Тип/Уровень предмета. """ benefit: bool @@ -38,10 +37,10 @@ class MainPageItem(WebClientObject): type: str @classmethod - def de_json(cls: dataclass, data: dict) -> 'MainPageItem': + def de_json(cls, data: dict) -> 'MainPageItem': del data['color'] - data = super(MainPageItem, cls).de_json(data) + data = super(MainPageItem, cls)._de_json(data) return cls(**data) @@ -51,13 +50,13 @@ class MainPage(WebClientObject): """Класс, представляющий главную страничку продажи. Attributes: - auth (:obj:`bool`): Истина если был указан правильный sessionid (sid). - items (Sequence[:class:`MainPageItem`]): Последовательность предметов. - currency (:obj:`int`): Валюта. 1 - рубль. - current_page (:obj:`int`): Текущая страница. - page_count (:obj:`int`): Всего страниц. - commission (:obj:`int`, optional): Коммиссия в %. Указывется если auth = True. - discount (:obj:`float`): Скидка на покупки. Указывется если auth = True. + auth (bool): Истина если был указан правильный sessionid (sid). + items (Sequence[MainPageItem]): Последовательность предметов. + currency (int): Валюта. 1 - рубль. + current_page (int): Текущая страница. + page_count (int): Всего страниц. + commission (int, optional): Коммиссия в %. Указывется если auth = True. + discount (float): Скидка на покупки. Указывется если auth = True. """ auth: bool @@ -69,7 +68,7 @@ class MainPage(WebClientObject): discount: Optional[float] = None @classmethod - def de_json(cls: dataclass, data: dict) -> 'MainPage': + def de_json(cls, data: dict) -> 'MainPage': try: _ = data['error'] @@ -88,7 +87,7 @@ def de_json(cls: dataclass, data: dict) -> 'MainPage': del data['body'], data['chat'], data['handler'], data['menu'], data['sorter'], data['title'], data['game'] - data = super(MainPage, cls).de_json(data) + data = super(MainPage, cls)._de_json(data) return cls(**data) @@ -98,12 +97,12 @@ class SellOffer(WebClientObject): """Класс, представляющий предложение о продаже. Attributes: - id (:obj:`int`): Уникальный ID предложения. - itemid (:obj:`int`): ID предмета. - image_url (:obj:`str`): Неабсолютная ссылка на изображение предмета. - name (:obj:`str`): Переведённое название предмета. - type (:obj:`int`): Тип/Уровень предмета. - price (:obj:`float`): Цена предложения. + id (int): Уникальный ID предложения. + itemid (int): ID предмета. + image_url (str): Неабсолютная ссылка на изображение предмета. + name (str): Переведённое название предмета. + type (int): Тип/Уровень предмета. + price (float): Цена предложения. """ id: int @@ -114,13 +113,13 @@ class SellOffer(WebClientObject): price: float @classmethod - def de_json(cls: dataclass, tag: 'bs4.Tag') -> 'SellOffer': + def de_json(cls, tag: bs4.Tag) -> 'SellOffer': - tree = etree.HTML(str(tag)) + tree = etree.HTML(str(tag)) # type: ignore _type = tree.xpath('//div/table/tr/td[2]/div[2]/p[2]')[0].text data = { - 'id': int(tag.get('data-id')), # Названия одинаковые, но это ID предложения + 'id': int(cast(str, tag.get('data-id'))), # Названия одинаковые, но это ID предложения 'itemid': int(tree.xpath('//div')[0].get('data-id')), # А это ItemID 'image_url': tree.xpath('//div/table/tr/td[1]/img')[0].get('src'), 'name': tree.xpath('//div/table/tr/td[2]/div[1]')[0].text, @@ -143,9 +142,9 @@ class ItemDescription(WebClientObject): description: str @classmethod - def de_json(cls: dataclass, data: dict) -> 'ItemDescription': + def de_json(cls, data: dict) -> 'ItemDescription': - data = super(ItemDescription, cls).de_json(data) + data = super(ItemDescription, cls)._de_json(data) return cls(**data) @@ -154,13 +153,13 @@ class ItemInfo(WebClientObject): """Класс, представляющий данные группы предметов. Attributes: - auth (:obj:`bool`): Истина если был указан правильный sessionid (sid). - sell_offers (Sequence[:class:`SellOffer`): Последовательность предложений о продаже. Только для текущей страницы. - descriptions (Sequence[:obj:`dict[:obj:`int`, :class:`ItemDescription`]`]): Словарь с парами ItemID/описание. + auth (bool): Истина если был указан правильный sessionid (sid). + sell_offers (Sequence[SellOffer): Последовательность предложений о продаже. Только для текущей страницы. + descriptions (Sequence[dict[int, ItemDescription]]): Словарь с парами ItemID/описание. Только для текущей страницы. Если предмет типовой, равен None. - item (:obj:`bool`): Истина если... если что? - commission (:obj:`int`, optional): Коммиссия в %. Указывется если auth = True. - discount (:obj:`float`): Скидка на покупки. Указывется если auth = True. + item (bool): Истина если... если что? + commission (int, optional): Коммиссия в %. Указывется если auth = True. + discount (float): Скидка на покупки. Указывется если auth = True. """ auth: bool @@ -171,13 +170,13 @@ class ItemInfo(WebClientObject): discount: Optional[float] = None @classmethod - def de_json(cls: dataclass, data: dict) -> 'ItemInfo': + def de_json(cls, data: dict) -> 'ItemInfo': html = bs4.BeautifulSoup(data['contents'], 'lxml') data['sell_offers'] = [SellOffer.de_json(tag) for tag in html.find_all('div', {'class': 'offer'})] try: - script = html.find('script').text + script = cast(bs4.Tag, html.find('script')).text descriptions = dict(json.loads(script[script.index('var d=') + 6:script.index(';Market.setItemOffers(d,')])) for k, v in descriptions.copy().items(): descriptions[int(k)] = ItemDescription.de_json(v) @@ -191,7 +190,7 @@ def de_json(cls: dataclass, data: dict) -> 'ItemInfo': del data['title'], data['game'], data['menu'], data['contents'] - data = super(ItemInfo, cls).de_json(data) + data = super(ItemInfo, cls)._de_json(data) return cls(**data) @@ -201,10 +200,10 @@ class Referal(WebClientObject): """Класс, представляющий реферала. Attributes: - name (:obj:`str`): Имя рефералла. - date (:obj:`str`): Дата присоединения. - status (:obj:`str`): Статус реферала. - sum (:obj:`float`): Сумма потраченных рефералом средств. + name (str): Имя рефералла. + date (str): Дата присоединения. + status (str): Статус реферала. + sum (float): Сумма потраченных рефералом средств. """ name: str @@ -213,9 +212,9 @@ class Referal(WebClientObject): sum: float @classmethod - def de_json(cls: dataclass, data: dict) -> 'Referal': + def de_json(cls, data: dict) -> 'Referal': - data = super(Referal, cls).de_json(data) + data = super(Referal, cls)._de_json(data) return cls(**data) @@ -225,11 +224,11 @@ class HistoryItem(WebClientObject): """Класс, представляющий предмет из истории продаж. Attributes: - name (:obj:`str`): Название предмета. - date (:obj:`str`): Отформатированная строка времени. - price (:obj:`float`): Цена, за которую был продан предмет. - color (:obj:`str`): HEX код цвета текста названия. - image_url (:obj:`str`): Неабсолютная ссылка на изображение предмета. + name (str): Название предмета. + date (str): Отформатированная строка времени. + price (float): Цена, за которую был продан предмет. + color (str): HEX код цвета текста названия. + image_url (str): Неабсолютная ссылка на изображение предмета. """ name: str @@ -239,9 +238,9 @@ class HistoryItem(WebClientObject): image_url: str @classmethod - def de_json(cls: dataclass, tag: bs4.Tag) -> 'HistoryItem': + def de_json(cls, tag: bs4.Tag) -> 'HistoryItem': - tree = etree.HTML(str(tag)) + tree = etree.HTML(str(tag)) # type: ignore data = { 'image_url': tree.xpath('//span[1]/img')[0].get('src'), From d2f560b75d973834f877b65313dc56785a2bbcda Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Sat, 4 Jan 2025 13:14:48 +0300 Subject: [PATCH 3/7] =?UTF-8?q?impr:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/api/test_convertion.py | 51 +++++++++++++++------------- tests/api/test_ext_requests.py | 5 ++- tests/api/test_ext_requests_async.py | 5 ++- tests/api/test_requests.py | 36 +++++++++++++++----- tests/api/test_requests_async.py | 38 ++++++++++++++++----- tests/web_api/test_requests.py | 4 +-- tests/web_api/test_requests_async.py | 11 ++---- 7 files changed, 98 insertions(+), 52 deletions(-) diff --git a/tests/api/test_convertion.py b/tests/api/test_convertion.py index 163d3b9..ad55a6e 100644 --- a/tests/api/test_convertion.py +++ b/tests/api/test_convertion.py @@ -2,11 +2,16 @@ import unittest import steam_trader.api as steam_trader +from dotenv import load_dotenv +load_dotenv() class IndependentTests(unittest.TestCase): def setUp(self): - self.client = steam_trader.Client(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.Client(token) def assertion(self, test_response, result): for name, value in test_response.items(): @@ -29,9 +34,9 @@ def test_sell_result(self): "commission": 1.5, "nc": "L8RJI7XX9qAtpnls" } - result = steam_trader.SellResult.de_json(test_response1, client=self.client) + result = steam_trader.SellResult.de_json(test_response1) self.assertion(test_response1, result) - result = steam_trader.SellResult.de_json(test_response2, client=self.client) + result = steam_trader.SellResult.de_json(test_response2) self.assertion(test_response2, result) def test_buy_result(self): @@ -44,7 +49,7 @@ def test_buy_result(self): "new_price": 0, "discount": 1.5 } - result = steam_trader.BuyResult.de_json(test_response, client=self.client) + result = steam_trader.BuyResult.de_json(test_response) self.assertion(test_response, result) def test_create_buy_order_result(self): @@ -72,7 +77,7 @@ def test_create_buy_order_result(self): "executed": 1, "placed": 2 } - result = steam_trader.BuyOrderResult.de_json(test_response, client=self.client) + result = steam_trader.BuyOrderResult.de_json(test_response) self.assertion(test_response, result) def test_multi_buy_result(self): @@ -93,7 +98,7 @@ def test_multi_buy_result(self): } ] } - result = steam_trader.MultiBuyResult.de_json(test_response, client=self.client) + result = steam_trader.MultiBuyResult.de_json(test_response) self.assertion(test_response, result) def test_edit_price_result(self): @@ -112,9 +117,9 @@ def test_edit_price_result(self): "price": 10.24, "percent": 1.5 } - result = steam_trader.EditPriceResult.de_json(test_response1, client=self.client) + result = steam_trader.EditPriceResult.de_json(test_response1) self.assertion(test_response1, result) - result = steam_trader.EditPriceResult.de_json(test_response2, client=self.client) + result = steam_trader.EditPriceResult.de_json(test_response2) self.assertion(test_response2, result) def test_delete_item_result(self): @@ -126,7 +131,7 @@ def test_delete_item_result(self): "total_fines": 0, "fine_date": None } - result = steam_trader.DeleteItemResult.de_json(test_response, client=self.client) + result = steam_trader.DeleteItemResult.de_json(test_response) self.assertion(test_response, result) def test_get_down_orders_result(self): @@ -139,7 +144,7 @@ def test_get_down_orders_result(self): 3 ] } - result = steam_trader.GetDownOrdersResult.de_json(test_response, client=self.client) + result = steam_trader.GetDownOrdersResult.de_json(test_response) self.assertion(test_response, result) def test_items_for_exchange(self): @@ -183,7 +188,7 @@ def test_items_for_exchange(self): } } } - result = steam_trader.ItemsForExchange.de_json(test_response, client=self.client) + result = steam_trader.ItemsForExchange.de_json(test_response) self.assertion(test_response, result) def test_exchange_result(self): @@ -210,7 +215,7 @@ def test_exchange_result(self): } ] } - result = steam_trader.ExchangeResult.de_json(test_response, client=self.client) + result = steam_trader.ExchangeResult.de_json(test_response) self.assertion(test_response, result) def test_exchange_p2p_result(self): @@ -266,7 +271,7 @@ def test_exchange_p2p_result(self): "456" ] } - result = steam_trader.ExchangeP2PResult.de_json(test_response, client=self.client) + result = steam_trader.ExchangeP2PResult.de_json(test_response) self.assertion(test_response, result) def test_min_prices(self): @@ -278,7 +283,7 @@ def test_min_prices(self): "count_sell_offers": 30, "count_buy_offers": 15 } - result = steam_trader.MinPrices.de_json(test_response, client=self.client) + result = steam_trader.MinPrices.de_json(test_response) self.assertion(test_response, result) def test_get_item_info(self): @@ -367,7 +372,7 @@ def test_get_item_info(self): [506137857, 401.00] ] } - result = steam_trader.ItemInfo.de_json(test_response, client=self.client) + result = steam_trader.ItemInfo.de_json(test_response) self.assertion(test_response, result) def test_order_book(self): @@ -386,7 +391,7 @@ def test_order_book(self): "total_sell": 9, "total_buy": 8 } - result = steam_trader.OrderBook.de_json(test_response, client=self.client) + result = steam_trader.OrderBook.de_json(test_response) self.assertion(test_response, result) def test_ws_token(self): @@ -395,7 +400,7 @@ def test_ws_token(self): "time": 1504281966, "hash": r"tQ+XqXYXVb+hX9M25wzj\/nhOR5LQyJATY1499qGdK2o=" } - result = steam_trader.WebSocketToken.de_json(test_response, client=self.client) + result = steam_trader.WebSocketToken.de_json(test_response) self.assertion(test_response, result) def test_inventory(self): @@ -423,7 +428,7 @@ def test_inventory(self): } ] } - result = steam_trader.Inventory.de_json(test_response, client=self.client) + result = steam_trader.Inventory.de_json(test_response) self.assertion(test_response, result) def test_buy_orders(self): @@ -452,7 +457,7 @@ def test_buy_orders(self): } ] } - result = steam_trader.BuyOrders.de_json(test_response, client=self.client) + result = steam_trader.BuyOrders.de_json(test_response) self.assertion(test_response, result) def test_discounts(self): @@ -485,7 +490,7 @@ def test_discounts(self): } } } - result = steam_trader.Discounts.de_json(test_response, client=self.client) + result = steam_trader.Discounts.de_json(test_response) self.assertion(test_response, result) def test_operations_history(self): @@ -510,7 +515,7 @@ def test_operations_history(self): } ] } - result = steam_trader.OperationsHistory.de_json(test_response, client=self.client) + result = steam_trader.OperationsHistory.de_json(test_response) self.assertion(test_response, result) def test_inventory_state(self): @@ -520,7 +525,7 @@ def test_inventory_state(self): "lastUpdate": 1509898188, "itemsInCache": 9 } - result = steam_trader.InventoryState.de_json(test_response, client=self.client) + result = steam_trader.InventoryState.de_json(test_response) self.assertion(test_response, result) def test_alt_ws(self): @@ -533,7 +538,7 @@ def test_alt_ws(self): } ] } - result = steam_trader.AltWebSocket.de_json(test_response, client=self.client) + result = steam_trader.AltWebSocket.de_json(test_response) self.assertion(test_response, result) if __name__ == '__main__': diff --git a/tests/api/test_ext_requests.py b/tests/api/test_ext_requests.py index 487e0f9..e123290 100644 --- a/tests/api/test_ext_requests.py +++ b/tests/api/test_ext_requests.py @@ -16,7 +16,10 @@ class IndependentTests(unittest.TestCase): def setUp(self): - self.client = ExtClient(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = ExtClient(token) self.filters = Filters( quality=[Filter(id=TF2_QUALITY_STRANGE), Filter(id=DOTA2_QUALITY_ELDER)], type=[Filter(id=TF2_TYPE_PRIMARY), Filter(id=DOTA2_TYPE_STICKER)], diff --git a/tests/api/test_ext_requests_async.py b/tests/api/test_ext_requests_async.py index 990d69c..f3694e7 100644 --- a/tests/api/test_ext_requests_async.py +++ b/tests/api/test_ext_requests_async.py @@ -16,7 +16,10 @@ class IndependentTests(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.client = ExtClientAsync(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = ExtClientAsync(token) self.filters = Filters( quality=[Filter(id=TF2_QUALITY_STRANGE), Filter(id=DOTA2_QUALITY_ELDER)], type=[Filter(id=TF2_TYPE_PRIMARY), Filter(id=DOTA2_TYPE_STICKER)], diff --git a/tests/api/test_requests.py b/tests/api/test_requests.py index cd884a6..8782833 100644 --- a/tests/api/test_requests.py +++ b/tests/api/test_requests.py @@ -11,12 +11,15 @@ from steam_trader.exceptions import WrongTradeLink, NoTradeItems from dotenv import load_dotenv -load_dotenv() # Для проведения тестов необходимо указать ваш токен и ссылку для обмена Steam в environemntal variables +load_dotenv() class IndependentTests(unittest.TestCase): def setUp(self): - self.client = steam_trader.Client(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.Client(token) self.test_gids = [1220, 1226, 1760, 6231, 6339, 13903, 14324, 71603] self.test_appids = SUPPORTED_APPIDS @@ -59,7 +62,7 @@ def test_get_operations_history(self): operations_history = self.client.get_operations_history() self.assertTrue(operations_history.success) - for operation_type in range(1, 11): + for operation_type in (1, 2, 3, 4, 5, 9, 10): operations_history = self.client.get_operations_history(operation_type=operation_type) self.assertTrue(operations_history.success) @@ -80,8 +83,14 @@ def test_trigger_alt_web_socket(self): class TradeLinkTests(unittest.TestCase): def setUp(self): - self.client = steam_trader.Client(os.getenv('TOKEN')) - self.trade_link = os.getenv('STEAM_TRADE_LINK') + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.Client(token) + trade_link = os.getenv('STEAM_TRADE_LINK') + if not trade_link: + raise ValueError('Ваша ссылка для обмена должена быть указана в переменных среды.') + self.trade_link = trade_link def test_01_remove_trade_link(self): self.client.remove_trade_link() @@ -95,7 +104,10 @@ def test_02_set_trade_link(self): class BuyTests(unittest.TestCase): def setUp(self): - self.client = steam_trader.Client(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.Client(token) self.skip_tests = True self.buy_gid = 1220 # Тренировочный ракетомёт self.multi_buy_gid = 1311 # Офицер запаса @@ -157,7 +169,10 @@ def test_05_get_down_orders(self): class SellTests(unittest.TestCase): def setUp(self): - self.client = steam_trader.Client(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.Client(token) self.skip_tests = True self.buy_gid = 1220 # Тренировочный ракетомёт self.multi_buy_gid = 1311 # Офицер запаса @@ -177,7 +192,7 @@ def test_01_sale(self): if item.status == -2: break - item_on_sale = self.client.sell(item.itemid, item.assetid, self.default_price) + item_on_sale = self.client.sell(item.itemid, item.assetid, self.default_price) # type: ignore self.assertTrue(item_on_sale.success) self._id = item_on_sale.id @@ -202,7 +217,10 @@ def test_03_delete_item(self): class TradeTests(unittest.TestCase): def setUp(self): - self.client = steam_trader.Client(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.Client(token) self.skip_tests = True def test_01_get_items_for_exchange(self): diff --git a/tests/api/test_requests_async.py b/tests/api/test_requests_async.py index c39cc8f..2c2ce5f 100644 --- a/tests/api/test_requests_async.py +++ b/tests/api/test_requests_async.py @@ -14,10 +14,17 @@ from dotenv import load_dotenv load_dotenv() # Для проведения тестов необходимо указать ваш токен и ссылку для обмена Steam в environemntal variables +token = os.getenv('TOKEN') +if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + class IndependentTests(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.client = steam_trader.ClientAsync(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.ClientAsync(token) self.test_gids = [1220, 1226, 1760, 6231, 6339, 13903, 14324, 71603] self.test_appids = SUPPORTED_APPIDS @@ -72,7 +79,7 @@ async def test_get_operations_history(self): operations_history = await self.client.get_operations_history() self.assertTrue(operations_history.success) - tasks = [self.client.get_operations_history(operation_type=operation_type) for operation_type in range(1, 11)] + tasks = [self.client.get_operations_history(operation_type=operation_type) for operation_type in (1, 2, 3, 4, 5, 9, 10)] responses = await asyncio.gather(*tasks) for operations_history in responses: self.assertTrue(operations_history.success) @@ -98,8 +105,14 @@ async def test_trigger_alt_web_socket(self): class TradeLinkTests(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.client = steam_trader.ClientAsync(os.getenv('TOKEN')) - self.trade_link = os.getenv('STEAM_TRADE_LINK') + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.ClientAsync(token) + trade_link = os.getenv('STEAM_TRADE_LINK') + if not trade_link: + raise ValueError('Ваша ссылка для обмена должна быть указан в переменных среды.') + self.trade_link = trade_link async def test_01_remove_trade_link(self): async with self.client: @@ -115,7 +128,10 @@ async def test_02_set_trade_link(self): class BuyTests(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.client = steam_trader.ClientAsync(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.ClientAsync(token) self.skip_tests = True self.buy_gid = 1220 # Тренировочный ракетомёт self.multi_buy_gid = 1311 # Офицер запаса @@ -182,7 +198,10 @@ async def test_05_get_down_orders(self): class SellTests(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.client = steam_trader.ClientAsync(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.ClientAsync(token) self.skip_tests = True self.buy_gid = 1220 # Тренировочный ракетомёт self.multi_buy_gid = 1311 # Офицер запаса @@ -203,7 +222,7 @@ async def test_01_sale(self): if item.status == -2: break - item_on_sale = await self.client.sell(item.itemid, item.assetid, self.default_price) + item_on_sale = await self.client.sell(item.itemid, item.assetid, self.default_price) # type: ignore self.assertTrue(item_on_sale.success) self._id = item_on_sale.id @@ -230,7 +249,10 @@ async def test_03_delete_item(self): class TradeTests(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.client = steam_trader.ClientAsync(os.getenv('TOKEN')) + token = os.getenv('TOKEN') + if not token: + raise ValueError('Ваш api токен должен быть указан в переменных среды.') + self.client = steam_trader.ClientAsync(token) self.skip_tests = True async def test_01_get_items_for_exchange(self): diff --git a/tests/web_api/test_requests.py b/tests/web_api/test_requests.py index 0597233..22146f6 100644 --- a/tests/web_api/test_requests.py +++ b/tests/web_api/test_requests.py @@ -19,8 +19,8 @@ def test_get_main_page(self): self.assertTrue(result.auth) self.assertTrue(len(result.items) == 30) - with self.assertRaises(NotFoundError): - self.client.get_main_page(appid, text='__UNDEFINED__') + with self.assertRaises(NotFoundError): + self.client.get_main_page(appid, text='__UNDEFINED__') def test_get_item_info(self): for gid in self.test_gids: diff --git a/tests/web_api/test_requests_async.py b/tests/web_api/test_requests_async.py index 477dd95..b88ee96 100644 --- a/tests/web_api/test_requests_async.py +++ b/tests/web_api/test_requests_async.py @@ -20,8 +20,8 @@ async def test_get_main_page(self): self.assertTrue(result.auth) self.assertTrue(len(result.items) == 30) - with self.assertRaises(NotFoundError): - await self.client.get_main_page(appid, text='__UNDEFINED__') + with self.assertRaises(NotFoundError): + await self.client.get_main_page(appid, text='__UNDEFINED__') async def test_get_item_info(self): async with self.client: @@ -34,14 +34,9 @@ async def test_get_referral_link(self): result = await self.client.get_referral_link() self.assertIsInstance(result, str) - async def test_get_referrals(self): - async with self.client: - result = await self.client.get_referrals() - self.assertIsInstance(result, list) - async def test_get_history(self): async with self.client: - await self.client.get_history_page(440, 'last_purchases') + await self.client.get_history_page(440, 'last') await self.client.get_history_page(440, 'day_most') await self.client.get_history_page(440, 'all_time_most') From 7276ec92e00595b8ddc5bb8e79bb2d30ff963b13 Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Sat, 4 Jan 2025 13:15:10 +0300 Subject: [PATCH 4/7] =?UTF-8?q?impr:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/TODO | 1 - examples/price_changer.py | 23 +++++++++++++---------- examples/research_full.py | 31 +++++++++++++++---------------- 3 files changed, 28 insertions(+), 27 deletions(-) delete mode 100644 examples/TODO diff --git a/examples/TODO b/examples/TODO deleted file mode 100644 index 468930e..0000000 --- a/examples/TODO +++ /dev/null @@ -1 +0,0 @@ -Добавить примеры использования библиотеки \ No newline at end of file diff --git a/examples/price_changer.py b/examples/price_changer.py index 66160d8..22628ba 100644 --- a/examples/price_changer.py +++ b/examples/price_changer.py @@ -4,37 +4,40 @@ from time import sleep from datetime import datetime -from steam_trader.api import Client +from typing import cast +from steam_trader.api import Client, OrderBook +from steam_trader.constants import TEAM_FORTRESS2_APPID client = Client('Ваш токен') with client: while True: - sell_items = client.get_inventory(440, status=[0]).items + sell_items = client.get_inventory(TEAM_FORTRESS2_APPID, status=[0]).items for item in sell_items: - - if item.price == 0.5: + + item_price = cast(float, item.price) + if item_price == 0.5: continue - market_price = client.get_min_prices(item.gid).market_price - sell_orders = client.get_order_book(item.gid) + market_price = cast(float, client.get_min_prices(item.gid).market_price) + sell_orders = cast(OrderBook, client.get_order_book(item.gid)) new_price = round(market_price - 0.01, 2) - if item.price > market_price and new_price > sell_orders.buy[0][0]: + if item_price > market_price and new_price > sell_orders.buy[0][0]: print(f'{datetime.now():%H:%M:%S} | ' f'{client.get_item_info(item.gid).name.center(67)} | ' f'{str(item.price).center(8)} -> {str(new_price).center(8)} | ' f'уменьшение') - client.edit_price(item.id, new_price) + client.edit_price(cast(int, item.id), new_price) - elif item.price < round(sell_orders.sell[1][0] - 0.01, 2) and sell_orders.sell[0][1] == 1: + elif item_price < round(sell_orders.sell[1][0] - 0.01, 2) and sell_orders.sell[0][1] == 1: new_price = round(sell_orders.sell[1][0] - 0.01, 2) print(f'{datetime.now():%H:%M:%S} | ' f'{client.get_item_info(item.gid).name.center(67)} | ' f'{str(item.price).center(8)} -> {str(new_price).center(8)} | ' f'увеличение') - client.edit_price(item.id, new_price) + client.edit_price(cast(int, item.id), new_price) sleep(30) diff --git a/examples/research_full.py b/examples/research_full.py index 3477bf5..76fc6b3 100644 --- a/examples/research_full.py +++ b/examples/research_full.py @@ -10,30 +10,20 @@ import steam_trader.api as api from time import sleep from dotenv import load_dotenv -from typing import Sequence -logging.basicConfig(level=logging.INFO) -load_dotenv() - -client = web.WebClientAsync(timeout=None) -api_client = api.ClientAsync(os.getenv('TOKEN'), timeout=None) - - -async def get_pages(gid: int) -> Sequence[Sequence['web.MainPageItem']]: +async def get_pages(gid: int) -> list[list[web.MainPageItem]]: async with client: page_count = (await client.get_main_page(gid, items_on_page=60)).page_count tasks = [client.get_main_page(gid, items_on_page=60, page=i) for i in range(1, page_count + 1)] - main_pages: Sequence['web.MainPage'] = await asyncio.gather(*tasks) + main_pages: list[web.MainPage] = list(await asyncio.gather(*tasks)) - pages = [] - for page in main_pages: - pages.append(page.items) + pages: list[list[web.MainPageItem]] = [list(page.items) for page in main_pages] return pages -async def get_info(pages: Sequence[Sequence['web.MainPageItem']]) -> Sequence['api.ItemInfo']: +async def get_info(pages: list[list[web.MainPageItem]]) -> list[api.ItemInfo]: info = [] for page in pages: async with api_client: @@ -50,8 +40,8 @@ async def main(gid): items_info = await get_info(pages) items = [] - for item in pages: - items.extend(item) # Объединяем всё в один список + for main_item in pages: + items.extend(main_item) # Объединяем всё в один список with open('items.csv', 'w', encoding='utf-8') as csvfile: csvwriter = csv.writer(csvfile, delimiter=';', lineterminator='\n') @@ -91,4 +81,13 @@ async def main(gid): if __name__ == '__main__': + logging.basicConfig(level=logging.WARNING) + load_dotenv() + + client = web.WebClientAsync(timeout=None) + + token = os.getenv('TOKEN') + if not token: + raise ValueError('Необходимо указать API ключ в переменных среды.') + api_client = api.ClientAsync(token, timeout=None) asyncio.run(main(constants.TEAM_FORTRESS2_APPID)) From 8e978b7a462de172fb996a015cbf68e8fc91d66c Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Sat, 4 Jan 2025 13:16:48 +0300 Subject: [PATCH 5/7] =?UTF-8?q?git:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 91 +-- README.md | 49 +- docs/async.md | 28 +- docs/client.md | 310 +---------- docs/dataclasses.md | 1288 +------------------------------------------ docs/exceptions.md | 84 +-- docs/ext.md | 146 +---- docs/ext_guide.md | 26 +- docs/index.md | 11 +- docs/quickstart.md | 90 +-- docs/web.md | 16 + mkdocs.yml | 32 +- 12 files changed, 190 insertions(+), 1981 deletions(-) create mode 100644 docs/web.md diff --git a/.gitignore b/.gitignore index 82f9275..57cc707 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - # Distribution / packaging .Python build/ @@ -26,12 +23,6 @@ share/python-wheels/ *.egg MANIFEST -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -51,23 +42,6 @@ coverage.xml .pytest_cache/ cover/ -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - # Sphinx documentation docs/_build/ @@ -75,52 +49,6 @@ docs/_build/ .pybuilder/ target/ -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - # Environments .env .venv @@ -130,13 +58,6 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - # mkdocs documentation /site @@ -151,12 +72,6 @@ dmypy.json # pytype static type analyzer .pytype/ -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# IDE stuff +.vscode/ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 8c799c8..cee631e 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,41 @@ # SteamTrader-Wrapper + ![PyPI - Downloads](https://img.shields.io/pypi/dm/steam-trader) ![PyPI - License](https://img.shields.io/pypi/l/steam-trader) ![PyPI - Status](https://img.shields.io/pypi/status/steam-trader) ⚠️ Это неофициальная библиотека. -### Содержание - - [Введение](#введение) - - [Получение токена](#получение-токена) - - [Установка](#установка) - - [Начало работы](#начало-работы) - - [Примеры](#примеры) - - [Документация](#документация) - - [Лицензия](#лицензия) +## Содержание +- [Введение](#введение) + - [Получение токена](#получение-токена) + - [Получение sessionid](#получение-sessionid) +- [Установка](#установка) +- [Начало работы](#начало-работы) +- [Примеры](#примеры) +- [Документация](#документация) +- [Лицензия](#лицензия) -### Введение +## Введение Эта библиотека представляет Python обёртку для REST и WEB API [Steam-Trader](https://steam-trader.com/). Она совместима с версиями Python 3.12+ и поддерживает работу как с синхронным, так и с асинхронным (asyncio) кодом. -В дополнение к реализации чистого API данная библиотека имеет ряд объектов высокого уровня и логирование, +В дополнение к реализации чистого API данная библиотека имеет ряд объектов высокого уровня и логирование, дабы сделать разработку клиентов и скриптов простой и понятной. Документация была написана исходя из API документации сайта. -#### Получение токена +### Получение токена Токен можно получить на сайте перейдя на вкладку [API](https://steam-trader.com/api/). В коде указывается один раз при создании клиента. -#### Получение sessionid +### Получение sessionid Чтобы получить ID сессии, нужно зарегистрироваться на сайте, зайти в панель разработчика браузера, перейти к файлам куки и скопировать значение ключа sid. Учтите, что это значение сбрасывается когда вы выходите из своего аккаунта. -### Установка +## Установка Вы можете установить или обновить Steam-Trader API с помощью команды: @@ -49,7 +51,7 @@ cd SteamTrader-Wrapper python setup.py install ``` -### Начало работы +## Начало работы Приступив к работе, первым делом необходимо создать экземпляр клиента. @@ -65,7 +67,7 @@ from steam_trader.api import ClientAsync client = ClientAsync('Ваш токен') ``` -Для использования логирования, добавьте эти строчки в свой код. Будет приходить полученный результат всех методов, +Для использования логирования, добавьте эти строчки в свой код. Будет приходить полученный результат всех методов, а если указать уровень logging.DEBUG, то будут приходить входы и выходы из функций. ```python @@ -92,7 +94,7 @@ from steam_trader.exceptions import Unauthorized, UnknownItem, WrongTradeLink from steam_trader.constants import TEAM_FORTRESS2_APPID, TF2_CRAFTABLE, DOTA2_RARITY_COMMON ``` -### Примеры +## Примеры Пример скрипта для продажи всего Очищенного металла в инвентаре TF2 с помощью синхронного клиента. @@ -109,7 +111,7 @@ for item in inventory.items: client.sell(item.itemid, item.assetid, price=new_price) ``` -С помощью get_inventory мы получаем все предметы из инвенторя TF2, которые не находятся в продаже, проходим по каждому, +С помощью get_inventory мы получаем все предметы из инвенторя TF2, которые не находятся в продаже, проходим по каждому, находим Очищенный металл и выставляем его по цене на копейку меньше рыночной, чтобы быть впереди. Узнать минимальную стоимость предмета можно через get_min_prices. Пример покупки всех предметов по GID ниже заданной стоимости. @@ -171,20 +173,17 @@ with WebClient('Ваш токен') as client: print(item_info.descriptions[offer.itemid]) ``` -### Документация - -Полную документацию можно найти здесь: https://lemon4ksan.github.io/steam-trader/ +## Документация -### Внесение своего вклада в проект +Полную документацию можно найти [здесь](https://lemon4ksan.github.io/steam-trader/). -Внесение своего вклада максимально приветствуется! +## Внесение своего вклада в проект Вы можете помочь, сообщив о [баге](https://github.com/Lemon4ksan/SteamTrader-Wrapper/issues/new?assignees=&labels=bug&projects=&template=bug-report.md&title=) или [предложив](https://github.com/Lemon4ksan/SteamTrader-Wrapper/issues/new?assignees=&labels=feature-request&projects=&template=feature-request.md&title=) новый функционал. -Данная библиотека будет переодически обновляться и дополняться. - ### Лицензия -См. Оригинал на английском [LICENSE](https://github.com/Lemon4ksan/SteamTrader-Wrapper/blob/master/LICENSE) + +См. Оригинал [LICENSE](https://github.com/Lemon4ksan/SteamTrader-Wrapper/blob/master/LICENSE). Разрешается повторное распространение и использование как в виде исходного кода, так и в двоичной форме, с изменениями или без, при соблюдении следующих условий: diff --git a/docs/async.md b/docs/async.md index f241b0e..0428637 100644 --- a/docs/async.md +++ b/docs/async.md @@ -4,17 +4,19 @@ Этот раздел пройдётся по тому, как его настроить. ## Инициализация клиента + Инициализация асинхронного клиента похожа на инициализацию синхронного. ```python -from steam_trader import ClientAsync +from steam_trader.api import ClientAsync client = steam_trader.ClientAsync('Ваш токен') ``` Для того чтобы создавать асинхронные запросы, необходимо поместить ваш код в асинхронную функцию. + ```python -from steam_trader import ClientAsync +from steam_trader.api import ClientAsync client = steam_trader.ClientAsync('Ваш токен') @@ -23,8 +25,9 @@ async def main() ``` Для работы с клиентом нужно использовать асинхронный контекстный менеджер. + ```python -from steam_trader import ClientAsync +from steam_trader.api import ClientAsync client = steam_trader.ClientAsync('Ваш токен') @@ -35,8 +38,9 @@ async def main() При вызове асинхронной функции она не выполняется, а возвращает объект корутины. С помощью await мы показываем где мы будем ждать чего-то. + ```python -from steam_trader import ClientAsync +from steam_trader.api import ClientAsync client = steam_trader.ClientAsync('Ваш токен') @@ -46,9 +50,10 @@ async def main() ``` Если вы попробуете вызвать функцию main, то получите ошибку. Входить в первую асинхронную функцию нужно с помощью встроенного модуля asyncio. + ```python import asyncio -from steam_trader import ClientAsync +from steam_trader.api import ClientAsync client = steam_trader.ClientAsync('Ваш токен') @@ -63,11 +68,12 @@ if __name__ == '__main__': Теперь клиент готов к выполнению задач. ## Пример использования + Представим, что нам нужно получить информацию о большом количестве предметов. Для начала напишем код для синхронного клиента. ```python -from steam_trader import ClientAsync +from steam_trader.api import ClientAsync client = ClientAsync('Ваш токен') @@ -83,18 +89,20 @@ if __name__ == '__main__': ``` Замерим скорость выполнения. + ```pycon >>> from timeit import timeit >>> timeit(main, number=1) 3.7272357000038028 ``` + Данный запрос занял 3 секунды, может показаться, что это немного, но что будет если таких запросов будет больше? В оптимизации нам поможет асинхронный клиент. ```python import asyncio -from steam_trader import ClientAsync +from steam_trader.api import ClientAsync client = ClientAsync('Ваш токен') @@ -111,13 +119,15 @@ asyncio.run(main()) ``` Замерим скорость выполнения. + ```pycon >>> from timeit import timeit >>> timeit(main, number=1) 1.1095618000254035 ``` -Как мы выдим, результат пришёл в 3 раза быстрее! Но как это работает? + +Как мы выдим, результат пришёл в 3 раза быстрее! Сначала мы создаём все корутины, которые мы собираемся выполнить, затем с помощью asyncio.gather() мы выполняем их *одновременно*. Благодаря асинхронности, интерпретатор не ждёт пока с сервера придёт один запрос, а переключается на следующий. -Это далеко не единственный пример использования асинхронного клиента, но определённо самый простой для понимания. \ No newline at end of file +Это далеко не единственный пример использования асинхронного клиента, но определённо самый простой для понимания. diff --git a/docs/client.md b/docs/client.md index cb74249..f94d10f 100644 --- a/docs/client.md +++ b/docs/client.md @@ -1,313 +1,5 @@ # Основной клиент -## `Client` - Одинаково для синхронного и асинхронного клиента. -::: steam_trader.Client -> Класс, представляющий клиент Steam Trader. - -> **Аргументы** - -> * **api_token** `str`: Уникальный ключ для аутентификации. -> * **proxy** `str`, optional: Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. -> * **base_url** `str`, optional: Ссылка на API Steam Trader. -> * **headers** `dict`, optional: Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. - -`api_token` -> Уникальный ключ для аутентификации. -> -> **Тип**: `str` - -`proxy`, optional -> Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. -> -> **Тип**: `str` - -`base_url`, optional -> Ссылка на API Steam Trader. -> -> **Тип**: `str` - -`headers`, optional -> Словарь, содержащий сведения об устройстве, с которого выполняются запросы. -> -> **Тип**: `dict` - -**Использование**: -```python -from steam_trader import Client - -client = Client('Ваш токен') -... - -# или - -with client: - ... -``` - -```python -from steam_trader import ClientAsync - -client = ClientAsync('Ваш токен') - -async def main(): - async with client: - ... -``` ---- - -#### **properety** `balance` -> Баланс клиента. -> -> **Возвращает**: `float` - -#### `sell`(*self, itemid, assetid, price*) -> Создать предложение о продаже определённого предмета. -> -> Если при создании предложения о ПРОДАЖЕ указать цену меньше, чем у имеющейся заявки на ПОКУПКУ, -> предложение о ПРОДАЖЕ будет исполнено моментально по цене заявки на ПОКУПКУ. -> -> Например, на сайте есть заявка на покупку за 10 ₽, а продавец собирается выставить предложение за 5 ₽ -> (дешевле), то сделка совершится по цене 10 ₽. -> -> **Аргументы** -> -> * **itemid** `int`: Уникальный ID предмета. -> * **assetid** `int`: AssetID предмета в Steam. -> * **price** `int`: Цена, за которую хотите продать предмет без учёта комиссии/скидки. -> -> **Возвращает**: *class* [`SellResult`](dataclasses.md#sellresult) - -#### `buy`(*self, id, type, price, currency=1*) -> Создать предложение о покупке предмета по строго указанной цене. -> Если в момент покупки цена предложения о продаже изменится, покупка не совершится. -> -> **Аргументы** -> -> * **id** Union[ `str`, `int` ]: В качества ID может выступать: - - GID для варианта покупки Commodity. - - Часть ссылки после nc/ (nc/L8RJI7XR96Mmo3Bu) для варианта покупки NoCommission. - - ID предложения о продаже для варианта покупки Offer (найти их можно в ItemInfo). -> -> * **type** `int`: Вариант покупки (указаны выше) - 1 / 2 / 3. -> * **price** `float`: Цена предложения о продаже без учёта комиссии/скидки. -> * **currency** `int`: Валюта покупки. Значение 1 - рубль. -> !!! Заметка - Сайт пока работает только с рублями. Не меняйте значение currency. -> -> **Возвращает**: *class* [`BuyResult`](dataclasses.md#buyresult) - -#### `create_buy_order`(*self, gid, price, \*, count=1*) -> Создать заявку на покупку предмета с определённым GID. -> -> При создании запроса может произойти моментальная покупка по аналогии тому, -> как это сделано в методе sell. -> -> **Аргументы** -> -> * **gid** `int`: ID группы предметов. -> * **price** `float`: Цена предмета, за которую будете его покупать без учёта комиссии/скидки. -> * **count** `int`: Количество заявок для размещения (не более 500). По умолчанию - 1. -> -> **Возвращает**: *class* [`BuyOrderResult`](dataclasses.md#buyorderresult) - -#### `multi_buy`(*self, gid, max_price, count*) -> Создать запрос о покупке нескольких предметов с определённым GID. -> Будут куплены самые лучшие (дешёвые) предложения о продаже. -> -> Если максимальная цена ПОКУПКИ будет указана больше, чем у имеющихся предложений о ПРОДАЖЕ, ПОКУПКА -> совершится по цене предложений. Например, на сайте есть 2 предложения о продаже по цене 10 и 11 ₽, -> если при покупке указать максмальную цену 25 ₽, то сделки совершатся по цене 10 и 11 ₽, -> а общая сумма потраченных средств - 21 ₽. -> -> **Аргументы** -> -> * **gid** `int`: ID группы предметов. -> * **max_price** `float`: Максимальная цена одного предмета без учёта комиссии/скидки. -> * **count** `int`: Количество предметов для покупки. -> -> **Возвращает**: *class* [`MultiBuyResult`](dataclasses.md#multibuyresult) - -#### `edit_price`(*self, id, price*) -> Редактировать цену предмета/заявки на покупку. -> При редактировании может произойти моментальная продажа/покупка по аналогии тому, -> как это сделано в методах sell и create_buy_order. -> -> **Аргументы** -> -> * **id** `int`: ID предложения о продаже/заявки на покупку. -> * **price** `float`: Новая цена, за которую хотите продать/купить предмет без учёта комиссии/скидки. -> -> **Возвращает**: *class* [`EditPriceResult`](dataclasses.md#editpriceresult) - -#### `delete_item`(*self, id*) -> Снять предмет с продажи/заявку на покупку. -> -> **Аргументы** -> -> * **id** `int`: ID продажи/заявки на покупку. -> -> **Возвращает**: *class* [`DeleteitemResult`](dataclasses.md#deleteitemresult) - -#### `get_down_orders`(*self, gameid, \*, order_type='sell'*) -> Снять все заявки на продажу/покупку предметов. -> -> **Аргументы** -> -> * **gameid** `int`: AppID приложения в Steam. -> * **order_type** `LiteralString`: Тип заявок для удаления: - - "sell" - предложения о ПРОДАЖЕ. Значение по умолчанию. - - "buy" - предложения о ПОКУПКЕ. -> -> **Возвращает**: *class* [`GetDownOrdersResult`](dataclasses.md#getdownordersresult) - -#### `get_items_for_exchange`(*self*) -> Получить список предметов для p2p обмена. -> -> **Возвращает**: *class* [`ItemsForExchange`](dataclasses.md#itemsforexchange) - -#### `exchange`(*self*) -> Выполнить обмен с ботом. -> -> !!! Заметка - Вы сами должны принять трейд в приложении Steam, у вас будет 3 часа на это. - В противном случае трейд будет отменён. -> -> **Возвращает**: *class* [`ExchangeResult`](dataclasses.md#exchangeresult) - -#### `get_items_for_exchange_p2p`(*self*) -> Получить список предметов для p2p обмена. -> -> **Возвращает**: *class* [`ItemsForExchange`](dataclasses.md#itemsforexchange) - -#### `exchange_p2p`(*self*) -> Выполнить p2p обмен. -> -> !!! Заметка - Вы сами должны передать предмет клиенту из полученной информации. -> -> **Возвращает**: *class* [`ExchangeP2PResult`](dataclasses.md#exchangep2presult) - -#### `get_min_prices`(*self, gid, currency=1*) -> Получить минимальные/максимальные цены предмета. -> -> **Аргументы** -> -> * **gid** `int`: ID группы предметов. -> * **currency** `int`: Валюта, значение 1 - рубль. -> !!! Заметка - Сайт пока работает только с рублями. Не меняйте значение currency. -> -> **Возвращает**: *class* [`MinPrices`](dataclasses.md#minprices) - -#### `get_item_info`(*self, gid*) -> Получить информацию о группе предметов. -> -> **Аргументы** -> -> * **gid** `int`: ID группы предметов. -> -> **Возвращает**: *class* [`ItemInfo`](dataclasses.md#iteminfo) - -#### `get_order_book`(*self, gid, \*, mode='all', limit=None*) -> Получить заявки о покупке/продаже предмета. -> -> **Аргументы** -> -> * **gid** `int`: ID группы предметов -> * **mode** `LiteralString`: Режим отображения: - - "all" - отображать покупки и продажи. Значение по умолчанию. - - "sell" - отображать только заявки на ПРОДАЖУ. - - "buy" - отображать только заявки на ПОКУПКУ. -> * **limit** `int`, optional: Максимальное количество строк в списке. По умолчанию - неограниченно. -> -> **Возвращает**: *class* [`OrderBook`](dataclasses.md#orderbook) - -#### `get_web_socket_token`(*self*) -> Получить токен для авторизации в WebSocket. -> -> **Возвращает**: *class* [`WebSocketToken`](dataclasses.md#websockettoken) - -#### `get_inventory`(*self, gameid, \*, status=None*) -> Получить инвентарь клиента, включая заявки на покупку и купленные предметы. -> По умолчанию возвращает список предметов из инвентаря Steam, которые НЕ выставлены на продажу. -> -> **Аргументы** -> -> * **gameid** `int`: AppID приложения в Steam. -> * **status** Sequence[ `int` ], optional: Указывается, чтобы получить список предметов с определенным статусом. - - 0 - В продаже - - 1 - Принять - - 2 - Передать - - 3 - Ожидается - - 4 - Заявка на покупку -> -> **Возвращает**: *class* [`Inventory`](dataclasses.md#inventory) - -#### `get_buy_orders`(*self, \*, gameid=None, gid=None*) -> Получить последовательность заявок на покупку. По умолчанию возвращаются заявки для всех предметов из всех разделов. -> При указании соответствующих параметров можно получить заявки из определённого раздела и/или предмета. -> -> **Аргументы** -> -> * **gameid** `int`, optonal: AppID приложения в Steam. -> * **gid** `int`, optonal: ID группы предметов. -> -> **Возвращает**: *class* [`BuyOrders`](dataclasses.md#buyorders) - -#### `get_discounts`(*self*) -> Получить комиссии/скидки и оборот на сайте. -> Данные хранятся в словаре data, где ключ - это AppID игры в Steam. -> -> **Возвращает**: *class* [`Discounts`](dataclasses.md#discounts) - -#### `set_trade_link`(*self, trade_link*) -> Установить ссылку для обмена. -> -> **Аргументы** -> -> * **trade_link** `str`: Ссылка для обмена. Например, https://steamcommunity.com/tradeoffer/new/?partner=453486961&token=ZhXMbDS9 - -#### `remove_trade_link`(*self*) -> Удалить ссылку для обмена. - -#### `get_operations_history`(*self, \*, operation_type=None, page=0*) -> Получить историю операций. По умолчанию все типы. -> -> **Аргументы** -> -> * **operation_type** `int`, optional: Тип операции. Может быть пустым. - - 1 - Покупка предмета - - 2 - Продажа предмета - - 3 - Возврат за покупку - - 4 - Пополнение баланса - - 5 - Вывести средства - - 9 - Ожидание покупки - - 10 - Штрафной балл -> * **page** `int`: Страница операций. Отсчёт начинается с 0. -> -> **Возвращает**: *class* [`OperationsHistory`](dataclasses.md#operationshistory) - -#### `update_inventory`(*self, gameid*) -> Обновить инвентарь игры на сайте. -> -> **Аргументы** -> -> * **gameid** `int`: AppID приложения в Steam. - -#### `get_inventory_state`(*self, gameid*) -> Получить текущий статус обновления инвентаря. -> -> **Аргументы** -> -> * **gameid** `int`: AppID приложения в Steam. -> -> **Возвращает**: *class* [`InventoryState`](dataclasses.md#inventorystate) - -#### `trigger_alt_web_socket`(*self*) -> Создать запрос альтернативным WebSocket. -> Для поддержания активного соединения нужно делать этот запрос каждые 2 минуты. -> -> **Возвращает**: *class* [`AltWebSocket`](dataclasses.md#altwebsocket), optional \ No newline at end of file +::: steam_trader.api._client.Client diff --git a/docs/dataclasses.md b/docs/dataclasses.md index ddd0888..173df1f 100644 --- a/docs/dataclasses.md +++ b/docs/dataclasses.md @@ -4,1281 +4,49 @@ ## Продажа -### `SellResult` - -::: steam_trader.SellResult -> Класс, представляющий информацию о выставленном на продажу предмете. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`id` -> ID продажи. -> -> **Тип**: `int` - -`position` -> Позиция предмета в очереди. -> -> **Тип**: `int` - -`fast_execute` -> Был ли предмет продан моментально. -> -> **Тип**: `bool` - -`nc` -> Идентификатор для бескомиссионной продажи предмета. -> -> **Тип**: `str` - -`price` -> Цена, за которую был продан предмет с учетом комиссии. -> Указывается, если 'fast_execute' = True -> -> **Тип**: `float`, optional - -`commission` -> Размер комиссии в процентах, за которую был продан предмет. -> Указывается, если 'fast_execute' = True -> -> **Тип**: `float`, optional - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] +::: steam_trader.api._sale + options: + heading_level: 3 + filters: ["!de_json"] ## Покупка -### `BuyResult` - -::: steam_trader.BuyResult -> Класс, представляющий результат покупки. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`id` -> ID покупки. -> -> **Тип**: `int` - -`gid` -> ID группы предметов. -> -> **Тип**: `int` - -`itemid` -> Униклаьный ID купленного предмета. -> -> **Тип**: `int` - -`price` -> Цена, за которую был куплен предмет с учётом скидки. -> -> **Тип**: `float` - -`new_price` -> Новая цена лучшего предложения о продаже для варианта покупки Commodity, -> если у группы предметов ещё имеются предложения о продаже. Для остальных вариантов покупки будет 0 -> -> **Тип**: `float` - -`discount` -> Размер скидки в процентах, за которую был куплен предмет. -> -> **Тип**: `float` - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `BuyOrderResult` - -::: steam_trader.BuyOrderResult -> Класс, представляющий результат запроса на покупку. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`executed` -> Количество исполненных заявок. -> -> **Тип**: `int` - -`placed` -> Количество размещённых на маркет заявок. -> -> **Тип**: `int` - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `MultiBuyResult` - -::: steam_trader.MultiBuyResult -> Класс, представляющий результат мульти-покупки. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`balance` -> Баланс после покупки предметов. Указывается если 'success' = True -> -> **Тип**: `float`, optional - -`spent` -> Сумма потраченных средств на покупку предметов. Указывается если 'success' = True -> -> **Тип**: `float`, optional - -`orders` -> Последовательность купленных предметов. Указывается если 'success' = True -> -> **Тип**: Sequence[ *class* [`MultiBuyOrder`](#multibuyitem) ], optional - -`left` -> Сколько предметов по этой цене осталось. Если операция прошла успешно, всегда равен 0. -> -> **Тип**: `int` - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] +::: steam_trader.api._buy + options: + heading_level: 3 + filters: ["!de_json"] ## Редактирование -### `EditPriceResult` - -::: steam_trader.EditPriceResult -> Класс, представляющий результат запроса на изменение цены. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`type` -> Тип заявки. 0 - продажа, 1 - покупка. -> -> **Тип**: `int` - -`position` -> Позиция предмета в очереди. -> -> **Тип**: `int` - -`fast_execute` -> Был ли предмет продан/куплен моментально. -> -> **Тип**: `bool` - -`new_id` -> Новый ID заявки. Указывается, если 'fast_execute' = True. -> Новый ID присваивается только заявкам на ПОКУПКУ и только в случае редактирования уже имеющейся заявки. -> -> **Тип**: `int`, optional - -`price` -> Цена, за которую был продан/куплен предмет с учётом комиссии/скидки. -> Указывается, если 'fast_execute' = True. -> -> **Тип**: `float`, optional - -`percent` -> Размер комиссии/скидки в процентах, за которую был продан/куплен предмет. -> Указывается, если 'fast_execute' = true. -> -> **Тип**: `float`, optional - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `DeleteItemResult` - -::: steam_trader.DeleteItemResult -> Класс, представляющий результат запроса снятия предмета с продажи/заявки на покупку. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`has_ex` -> Есть ли доступный обмен на сайте. -> -> **Тип**: `bool` - -`has_bot_ex` -> Есть ли доступный обмен с ботом. -> -> **Тип**: `bool` - -`has_p2p_ex` -> Есть ли доступный P2P обмен. -> -> **Тип**: `bool` - -`total_fines` -> Общее количество штрафных баллов. -> -> **Тип**: `int` - -`fine_date` -> Дата снятия штрафных баллов. Если None - штрафных баллов нет. -> -> **Тип**: `int`, optional - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `GetDownOrdersResult` - -::: steam_trader.GetDownOrdersResult -> Класс, представляющий результат снятия всех заявок на продажу/покупку. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`count` -> Количество удалённых предложений. -> -> **Тип**: `int` - -`ids` -> Список из ID удалённых предложений. -> -> **Тип**: Sequence[ int ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] +::: steam_trader.api._edit_item + options: + heading_level: 3 + filters: ["!de_json"] ## Обмен -### `ItemsForExchange` - -::: steam_trader.ItemsForExchange -> Класс, представляющий предметы для обмена с ботом. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`items` -> Последовательность предметов для обмена с ботом. -> -> **Тип**: Sequence[ *class* [`ItemForExchange`](#itemforexchange) ] - -`description` -> Описания предметов для обмена с ботом. Ключ - itemid предмета. -> -> **Тип**: dict[ int, *class* `TradeDescription` ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `ExchangeResult` - -::: steam_trader.ExchangeResult -> Класс, представляющий результат инициализации обмена с ботом. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`offer_id` -> ID обмена в Steam. -> -> **Тип**: `int` - -`code` -> Код проверки обмена. -> -> **Тип**: `str` - -`bot_steamid` -> SteamID бота, который отправил обмен. -> -> **Тип**: `int` - -`bot_nick` -> Ник бота. -> -> **Тип**: `str` - -`items` -> Cписок предметов для обмена с ботом. -> -> **Тип**: Sequence[ *class* [`ExchangeItem`](#exchangeitem) ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `ExchangeP2PResult` -> Класс, представляющий результат инициализации p2p обмена. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`send` -> Массив с данными для создания нового обмена в Steam. -> -> **Тип**: Sequence[ *class* [`P2PSendObject`](#p2psendobject) ] - -`recieve` -> Массив с данными для принятия обмена. -> -> **Тип**: Sequence[ *class* [`P2PRecieveObject`](#p2preceiveobject) ] - -`confirm` -> Массив с данными для подтверждения обмена в мобильном аутентификаторе. -> -> **Тип**: Sequence[ *class* [`P2PConfirmObject`](#p2pconfirmobject) ] - -`cancel` -> Массив из ID обменов, которые нужно отменить. -> -> **Тип**: Sequence[ `str` ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] +::: steam_trader.api._trade + options: + heading_level: 3 + filters: ["!de_json"] ## Информация -### `MinPrices` - -::: steam_trader.MinPrices -> Класс, представляющий минимальную/максимальную цену на предмет. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`market_price` -> Минимальная цена продажи. Может быть пустым. -> -> **Тип**: `float`, optional - -`buy_price` -> Максимальная цена покупки. Может быть пустым. -> -> **Тип**: `float`, optional - -`steam_price` -> Минимальная цена в Steam. Может быть пустым. -> -> **Тип**: `float`, optional - -`count_sell_offers` -> Количество предложений о продаже. -> -> **Тип**: `int`, optional - -`count_buy_offers` -> Количество предложений о покупке. -> -> **Тип**: `int`, optional - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `ItemInfo` - -::: steam_trader.ItemInfo -> Класс, представляющий информацию о группе предметов на сайте. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`name` -> Локализованное (переведённое) название предмета. -> -> **Тип**: `str` - -`hash_name` -> Параметр 'market_hash_name' в Steam. -> -> **Тип**: `float` - -`type` -> Тип предмета (из Steam). -> -> **Тип**: `str` - -`gameid` -> AppID приложения в Steam. -> -> **Тип**: `int` - -`contextid` -> ContextID приложения в Steam. -> -> **Тип**: `int` - -`color` -> Hex код цвета предмета (из Steam). -> -> **Тип**: `str` - -`small_image` -> Абсолютная ссылка на маленькое изображение предмета. -> -> **Тип**: `str` - -`large_image` -> Абсолютная ссылка на большое изображение предмета. -> -> **Тип**: `str` - -`marketable` -> Параметр 'marketable' в Steam. -> -> **Тип**: `bool` - -`tradable` -> Параметр 'tradable' в Steam. -> -> **Тип**: `bool` - -`description` -> Локализованное (переведённое) описание предмета. -> -> **Тип**: `str` - -`market_price` -> Минимальная цена продажи. Может быть пустым. -> -> **Тип**: `float`, optional - -`buy_price` -> Максимальная цена покупки. Может быть пустым. -> -> **Тип**: `float`, optional - -`steam_price` -> Минимальная цена в Steam. Может быть пустым. -> -> **Тип**: `float`, optional - -`filters` -> Фильтры, используемые для поиска на сайте. -> -> **Тип**: *class* `Filters`, optional - -`sell_offers` -> Последовательность с предложениями о продаже. -> -> **Тип**: Sequence[ *class* [`SellOffer`](#selloffer) ] - -`buy_offers` -> Последовательность с предложениями о покупке. -> -> **Тип**: Sequence[ *class* [`BuyOffer`](#buyoffer) ] - -`sell_history` -> Последовательность истории продаж. -> -> **Тип**: Sequence[ *class* [`SellHistoryItem`](#sellhistoryitem) ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `OrderBook` - -::: steam_trader.OrderBook -> Класс, представляющий заявоки о покупке/продаже предмета. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`sell` -> Сгруппированный по цене список заявок на продажу. -> Каждый элемент в списке является массивом, где первый элемент - это цена, а второй - количество заявок. -> -> **Тип**: Sequence[ Sequence[ `int`, `int` ] ] - -`buy` -> Сгруппированный по цене список заявок на покупку. -> Каждый элемент в списке является массивом, где первый элемент - это цена, а второй - количество заявок. -> -> **Тип**: Sequence[ Sequence[ `int`, `int` ] ] - -`total_sell` -> Количество всех заявок на продажу. -> -> **Тип**: `int`, optional - -`total_buy` -> Количество всех заявок на покупку. -> -> **Тип**: `int`, optional - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] +::: steam_trader.api._item_info + options: + heading_level: 3 + filters: ["!de_json"] ## Аккаунт -### `WebSocketToken` -> Незадокументированно - -### `Inventory` - -::: steam_trader.Inventory -> Класс, представляющий инвентарь клиента. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`count` -> Количество всех предметов в инвентаре Steam. -> -> **Тип**: `int` - -`gameid` -> AppID игры к которой принадлежит инвентарь. -> -> **Тип**: `int` - -`last_update` -> Timestamp последнего обновления инвентаря. -> -> **Тип**: `int` - -`items` -> Последовательность с предметами в инвентаре. -> -> **Тип**: Sequence[ *class* [`InventoryItem`](#inventoryitem) ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `BuyOrders` - -::: steam_trader.BuyOrders -> Класс, представляющий ваши запросы на покупку. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`data` -> Последовательность запросов на покупку. -> -> **Тип**: Sequence[ *class* [`BuyOrder`](#buyorder) ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `Discounts` - -::: steam_trader.Discounts -> Класс, представляющий комиссии/скидки на игры, доступные на сайте. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`data` -> Словарь, содержащий комисии/скидки. -> -> **Тип**: dict[ `int`, *class* [`Discount`](#discount) ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `OperationsHistory` - -::: steam_trader.OperationsHistory -> Класс, представляющий истории операций, произведённых на сайте. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`data` -> Последовательность историй операций. -> -> **Тип**: Sequence[ *class* [`OperationsHistoryItem`](#operationshistoryitem) ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `InventoryState` - -::: steam_trader.InventoryState -> Класс, представляющий текущий статус инвентаря. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`updating_now` -> Инвентарь обновляется в данный момент. -> -> **Тип**: `bool` - -`last_update` -> Timestamp, когда последний раз был обновлён инвентарь. -> -> **Тип**: `int` - -`items_in_cache` -> Количество предметов в инвентаре. -> -> **Тип**: `int` - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `AltWebSocket` - -::: steam_trader.AltWebSocket -> Класс, представляющий запрос альтернативным WebSocket. - -`success` -> Результат запроса. Если false, сообщений в поле messages не будет, при этом соединение будет поддержано. -> -> **Тип**: `bool` - -`messages` -> Последовательность с WebSocket сообщениями. -> -> **Тип**: Sequence[ *class* [`AltWebSocketMessage`](#altwebsocketmessage) ] - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -## Фильтры - -### `Filters` - -::: steam_trader.Filters -> Класс, представляющий фильтры, используемые для поиска на сайте. - -`quality` -> Качество предмета (TF2, DOTA2). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`type` -> Тип предмета (TF2, DOTA2). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`used_by` -> Класс, который использует предмет (TF2). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`craft` -> Информация о карфте (TF2). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`region` -> Регион игры (SteamGift). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`genre` -> Жанр игры (SteamGift). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`mode` -> Тип игры, взаимодействие с Steam (SteamGift). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`trade` -> Информация об обмене (SteamGift). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`rarity` -> Редкость предмета (DOTA2). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -`hero` -> Герой, который использует предмет (DOTA2). -> -> **Тип**: Sequence[ *class* [`Filter`](#filter) ], optional - -### `Filter` - -::: steam_trader.Filter -> Класс, представляющий фильтр. - -`id` -> ID данного фильтра, может быть пустым. Если вы создаёте класс вручную, то обязательно укажите этот параметр. -> -> **Тип**: `int`, optional - -`title` -> Тайтл данного фильтра, может быть пустым. -> -> **Тип**: `str`, optional - -`color` -> Цвет данного фильтра, может быть пустым. -> -> **Тип**: `str`, optional +::: steam_trader.api._account + options: + heading_level: 3 + filters: ["!de_json"] ## Подклассы -### `MultiBuyOrder` - -::: steam_trader.MultiBuyOrder -> Класс, представляющий предмет из запроса на мульти-покупку - -`id` -> ID заявки. -> -> **Тип**: `int` - -`itemid` -> Уникальный ID предмета. -> -> **Тип**: `int` - -`price` -> Цена, за которую был куплен предмет с учётом скидки. -> -> **Тип**: `float` - -### `ItemForExchange` - -::: steam_trader.ItemForExchange -> Класс, представляющий информацию о предмете для передачи/получения боту. - -`id` -> ID покупки/продажи. -> -> **Тип**: `int` - -`assetid` -> AssetID предмета в Steam. -> -> **Тип**: `int` - -`gameid` -> AppID приложения в Steam. -> -> **Тип**: `int` - -`contextid` -> ContextID приложения в Steam. -> -> **Тип**: `int` - -`classid` -> Параметр ClassID в Steam. -> -> **Тип**: `int` - -`instanceid` -> Параметр InstanceID в Steam. -> -> **Тип**: `int` - -`gid` -> ID группы предметов. -> -> **Тип**: `int` - -`itemid` -> Уникальный ID предмета. -> -> **Тип**: `int` - -`price` -> Цена предмета, за которую купили/продали, без учета комиссии/скидки. -> -> **Тип**: `float` - -`currency` -> Валюта покупки/продажи. -> -> **Тип**: `int` - -`timer` -> Cколько времени осталось до передачи боту/окончания гарантии. -> -> **Тип**: `int` - -`asset_type` -> Значение 0 - этот предмет для передачи боту. Значение 1 - для приёма предмета от бота. -> -> **Тип**: `int` - -`percent` -> Размер комиссии/скидки в процентах, за которую был продан/куплен предмет. -> -> **Тип**: `float` - -`steam_item` -> Присутствует ли предмет в вашем инвентаре Steam. -> -> **Тип**: `bool` - -### `ExchangeItem` - -::: steam_trader.ExchangeItem -> Класс, представляющий предмет, на который был отправлен обмен. - -`id` -> ID покупки/продажи. -> -> **Тип**: `int` - -`assetid` -> AssetID предмета в Steam. -> -> **Тип**: `int` - -`gameid` -> AppID приложения в Steam. -> -> **Тип**: `int` - -`contextid` -> ContextID приложения в Steam. -> -> **Тип**: `int` - -`classid` -> Параметр ClassID в Steam. -> -> **Тип**: `int` - -`instanceid` -> Параметр InstanceID в Steam. -> -> **Тип**: `int` - -`type` -> Значение 0 - этот предмет для передачи боту. Значение 1 - для приёма предмета от бота. -> -> **Тип**: `int` - -`itemid` -> Уникальный ID предмета. -> -> **Тип**: `int` - -`gid` -> ID группы предметов. -> -> **Тип**: `int` - -`price` -> Цена, за которую предмет был куплен/продан с учётом скидки/комиссии. -> -> **Тип**: `float` - -`currency` -> Валюта покупки/продажи. -> -> **Тип**: `int` - -`percent` -> Размер комиссии/скидки в процентах, за которую был продан/куплен предмет. -> -> **Тип**: `float` - -### `P2PTradeOffer` - -[//]: # (::: steam_trader.P2PTradeOffer) -> Класс, представляющий данные для совершения p2p трейда. Незадокументированно. - -### `P2PSendObject` - -::: steam_trader.P2PSendObject -> Класс, представляющий ссылку на p2p обмен и сам обмен. - -`trade_link` -> Ссылка для p2p обмена. -> -> **Тип**: `str` - -`trade_offer` -> Параметры для POST запроса (https://steamcommunity.com/tradeoffer/new/send) при создании обмена в Steam. -> Вместо {sessionid} нужно указывать ID своей сессии в Steam. -> -> **Тип**: *class* [`P2PTradeOffer`](#p2ptradeoffer) - -### `P2PReceiveObject` - -::: steam_trader.P2PReceiveObject -> Класс, представляющий массив с данными для принятия обмена. - -`offerid` -> ID обмена в Steam. -> -> **Тип**: `int` - -`code` -> Код проверки обмена. -> -> **Тип**: `str` - -`items` -> Ссылка для p2p обмена. -> -> **Тип**: Sequence[ *class* [`ExchangeItem`](#exchangeitem)] - -`partner_steamid` -> SteamID покупателя. -> -> **Тип**: `int` - -### `P2PConfirmObject` - -::: steam_trader.P2PConfirmObject -> Класс, представляющий массив с данными для подтверждения обмена в мобильном аутентификаторе. - -`offerid` -> ID обмена в Steam. -> -> **Тип**: `int` - -`code` -> Код проверки обмена. -> -> **Тип**: `str` - -`partner_steamid` -> SteamID покупателя. -> -> **Тип**: `int` - - -### `SellOffer` - -::: steam_trader.SellOffer -> Класс, представляющий информацию о предложении продажи. - -`id` -> ID заявки. -> -> **Тип**: `int` - -`classid` -> ClassID предмета в Steam. -> -> **Тип**: `int` - -`instanceid` -> InstanceID предмета в Steam. -> -> **Тип**: `int` - -`itemid` -> Уникальный ID предмета. -> -> **Тип**: `int` - -`price` -> Цена предложения о покупке/продаже. -> -> **Тип**: `float` - -`currency` -> Валюта покупки/продажи. -> -> **Тип**: `int` - -### `BuyOffer` - -::: steam_trader.BuyOffer -> Класс, представляющий информацию о предложении продажи. - -`id` -> ID заявки. -> -> **Тип**: `int` - -`price` -> Цена предложения о покупке/продаже. -> -> **Тип**: `float` - -`currency` -> Валюта покупки/продажи. -> -> **Тип**: `int` - -### `SellHistoryItem` - -::: steam_trader.SellHistoryItem -> Класс, представляющий информацию о предмете в истории продаж. - -`date` -> Timestamp времени продажи. -> -> **Тип**: `int` - -`price` -> Цена предложения о покупке/продаже. -> -> **Тип**: `float` - -### `BuyOrder` - -::: steam_trader.BuyOrder -> Класс, представляющий информацию о запросе на покупку. - -`id` -> ID заявки на покупку. -> -> **Тип**: `int` - -`gid` -> ID группы предметов. -> -> **Тип**: `int` - -`gameid` -> AppID приложения в Steam. -> -> **Тип**: `int` - -`hash_name` -> Параметр market_hash_name в Steam. -> -> **Тип**: `str` - -`date` -> Timestamp подачи заявки. -> -> **Тип**: `int` - -`price` -> Предлагаемая цена покупки без учёта скидки. -> -> **Тип**: `float` - -`currency` -> Валюта, значение 1 - рубль. -> -> **Тип**: `int` - -`position` -> Позиция заявки в очереди. -> -> **Тип**: `int` - -### `InventoryItem` - -::: steam_trader.InventoryItem -> Класс, представляющий предмет в инвентаре. - -`id` -> ID заявки на покупку/продажу. Может быть пустым. -> -> **Тип**: `int`, optional - -`assetid` -> AssetID предмета в Steam. Может быть пустым. -> -> **Тип**: `int`, optional - -`gid` -> ID группы предметов. -> -> **Тип**: `int` - -`itemid` -> Уникальный ID предмета. -> -> **Тип**: `int` - -`price` -> Цена, за которую предмет был выставлен/куплен/продан предмет без учёта скидки/комиссии. Может быть пустым. -> -> **Тип**: `float`, optional - -`price` -> Валюта, за которую предмет был выставлен/куплен/продан. Значение 1 - рубль. Может быть пустым. -> -> **Тип**: `int`, optional - -`timer` -> Время, которое доступно для приема/передачи этого предмета. Может быть пустым. -> -> **Тип**: `int`, optional - -`type` -> Тип предмета. 0 - продажа, 1 - покупка. Может быть пустым. -> -> **Тип**: `int`, optional - -`status` -> Статус предмета. -> -* 2 - Предмет в инвентаре Steam не выставлен на продажу. -* 0 - Предмет выставлен на продажу или выставлена заявка на покупку. Для различия используется поле type. -* 1 - Предмет был куплен/продан и ожидает передачи боту или P2P способом. Для различия используется поле type. -* 2 - Предмет был передан боту или P2P способом и ожидает приёма покупателем. -* 6 - Предмет находится в режиме резервного времени. На сайте отображается как "Проверяется" после истечения времени на передачу боту или P2P способом. - -> **Тип**: `int` - -`position` -> Позиция предмета в списке заявок на покупку/продажу. Может быть пустым. -> -> **Тип**: `int`, optional - -`nc` -> ID заявки на продажу для бескомиссионной ссылки. Может быть пустым. -> -> **Тип**: `int`, optional - -`percent` -> Размер скидки/комиссии в процентах, с которой был куплен/продан предмет. Может быть пустым. -> -> **Тип**: `float`, optional - -`steam_item` -> Присутствует ли предмет в вашем инвентаре Steam. -> -> **Тип**: `bool` - -`nm` -> Незадокументированно. -> -> **Тип**: `bool` - -### `Discount` - -::: steam_trader.Discount -> Класс, представляющий информацию о комиссии/скидке в определённой игре. - -`total_buy` -> Cколько денег потрачено на покупки. -> -> **Тип**: `float` - -`total_sell` -> Cколько денег получено с продажи предметов. -> -> **Тип**: `float` - -`discount` -> Cкидка на покупку. Величина в %. -> -> **Тип**: `float` - -`percent` -> Комиссия на продажу. Величина в %. -> -> **Тип**: `float` - -### `OperationsHistoryItem` - -::: steam_trader.OperationsHistoryItem -> Класс, представляющий информацию о предмете в истории операций. - -`id` -> ID Операции. -> -> **Тип**: `int` - -`name` -> Название операции. -> -> **Тип**: `str` - -`type` -> Тип операции. 0 - продажа, 1 - покупка. -> -> **Тип**: `int` - -`amount` -> Сумма операции. -> -> **Тип**: `float` - -`currency` -> Валюта, значение 1 - рубль. -> -> **Тип**: `int` - -`date` -> Timestamp операции. -> -> **Тип**: `int` - -### `AltWebSocketMessage` - -::: steam_trader.AltWebSocketMessage -> Класс, представляющий AltWebSsocket сообщение. - -`type` -> -> **Тип**: `int` - -`data` -> -> **Тип**: `str` +::: steam_trader.api._misc + options: + heading_level: 3 + filters: ["!de_json"] diff --git a/docs/exceptions.md b/docs/exceptions.md index 6d85c63..69e2970 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -1,85 +1,3 @@ # Исключения -### `SteamTraderError` -> Базовый класс, представляющий исключения общего характера. - -### `UnsupportedAppID` -> Класс исключения, вызываемый в случае использования неподдерживаемого AppID. - -### `ClientError` -> Класс исключения, вызываемый в случае ошибки с клиентом. - -### `Unauthorized` -> Класс исключения, вызываемый в случае, если клиент не зарегистрирован. - -### `AuthenticatorError` -> Класс исключения, вызываемый в случае, если мобильный аутентификатор не поключён, или не прошло 7 ней с момента его активации. - -### `TradeError` -> Базовый класс исключений, вызываемых для ошибок, связанных с обменом. - -### `TradeCreationFail` -> Класс исключения, вызываемый в случае, если не удалось создать предложение обмена. - -### `NoTradeLink` -> Класс исключения, вызываемый в случае, если нет ссылки на обмен. - -### `NoSteamAPIKey` -> Класс исключения, вызываемый в случае, если нет ключа Steam API. - -### `WrongTradeLink` -> Класс исключения, вызываемый в случае, если клиент указал ссылку для обмена от другого Steam аккаунта. - -### `ExpiredTradeLink` -> Класс исключения, вызываемый в случае, если ссылка для обмена больше недействительна. - -### `NoBuyOrders` -> Класс исключения, вызываемый в случае, если у клиента нет запросов на покупку. - -### `TradeBlockError` -> Класс исключения, вызываемый в случае, если не включён Steam Guard или стоит блокировка обменов. - -### `MissingRequiredItems` -> Класс исключения, вызываемый в случае, если в инвентаре Steam отсутствуют необходимые для передачи предметы. - -### `HiddenInventory` -> Класс исключения, вызываемый в случае, если инвентарь скрыт. - -### `NoTradeItems` -> Класс исключения, вызываемый в случае, если у клиента нет предметов для обмена. - -### `IncorrectPrice` -> Класс исключения, вызываемый в случае, если выставленная цена ниже минимальной или больше максимальной. - -### `ItemAlreadySold` -> Класс исключения, вызываемый в случае, если предмет уже выставлен на продажу. - -### `NoLongerExists` -> Класс исключения, вызываемый в случае, если предмет больше не существует. - -### `NotEnoughMoney` -> Класс исключения, вызываемый в случае, если на балансе недостаточно средств для совершения операции. - -### `NetworkError` -> Базовый класс исключений, вызываемых для ошибок, связанных с запросами к серверу. - -### `OperationFail` -> Класс представляющий исключения, вызываемый в случае, если запрос был правильным, но операция не прошла успешно. - -### `UnknownItem` -> Класс исключения, вызываемый в случае, если предмет не был найден. - -### `SaveFail` -> Класс исключения, вызываемый в случае, если не удалось сохранить изменённый праметр на сайте. - -### `InternalError` -> Класс исключения, вызываемый в случае, если при выполнении запроса произошла неизвестая ошибка. - -### `BadRequestError` -> Класс исключения, вызываемый в случае отправки неправильного запроса. - -### `TooManyRequests` -> Класс исключения, вызываемый в случае отправки чрезмерно большого количества запросов на сервер. - -### `NotFoundError` -> Класс исключения, вызываемый в случае ответа от сервера со статус кодом 404. +::: steam_trader.exceptions diff --git a/docs/ext.md b/docs/ext.md index 49ec998..e21a97f 100644 --- a/docs/ext.md +++ b/docs/ext.md @@ -6,148 +6,4 @@ Наследует все методы основного клиента. -::: steam_trader.ext.ExtClient - -> **Аргументы** - -> * **api_token** `str`: Уникальный ключ для аутентификации. -> * **proxy** `str`, optional: Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. -> * **base_url** `str`, optional: Ссылка на API Steam Trader. -> * **headers** `dict`, optional: Словарь, содержащий сведения об устройстве, с которого выполняются запросы. Используется при каждом запросе на сайт. - -`api_token` -> Уникальный ключ для аутентификации. -> -> **Тип**: `str` - -`proxy`, optional -> Прокси для запросов. Для работы необходимо использовать контекстный менеджер with. -> -> **Тип**: `str` - -`base_url`, optional -> Ссылка на API Steam Trader. -> -> **Тип**: `str` - -`headers`, optional -> Словарь, содержащий сведения об устройстве, с которого выполняются запросы. -> -> **Тип**: `dict` - -**Использование**: -```python -from steam_trader.ext import ExtClient - -client = ExtClient('Ваш токен') -... - -# или - -with client: - ... -``` - -```python -from steam_trader.ext import ExtClientAsync - -client = ExtClientAsync('Ваш токен') - -async def main(): - async with client: - ... -``` ---- - -#### `get_inventory`(*self, gameid, \*, filters=None, status=None*) -> Получить инвентарь клиента, включая заявки на покупку и купленные предметы. -> По умолчанию возвращает список предметов из инвентаря Steam, которые НЕ выставлены на продажу. -> -> **Аргументы** -> -> * **gameid** `int`: AppID приложения в Steam. -> * **filters** *class* [`Filters`](dataclasses.md#filters), optional: Фильтр для отсеивания предметов. -> * **status** Sequnce[ `int` ], optional: Указывается, чтобы получить список предметов с определенным статусом. - - 0 - В продаже - - 1 - Принять - - 2 - Передать - - 3 - Ожидается - - 4 - Заявка на покупку -> -> **Возвращает**: *class* [Inventory](dataclasses.md#inventory), optional - -#### `multi_sell`(*self, gameid, gid, price, count*) -> Продать множество вещей из инвенторя с одним gid. -> -> **Аргументы** -> -> * **gameid** `int`: AppID приложения в Steam. -> * **gid** `int`: ID группы предметов. -> * **price** `float`: Цена для выставления на продажу. -> * **count** `int`: Количество предметов для продажи. Если число больше чем предметов в инвенторе, будут проданы те, что имеются. -> -> *Возвращает*: Sequence[ *class* [`SellResult`](dataclasses.md#sellresult), optional ] - -#### `set_trade_mode`(*self, state*) -> Задать режим торговли. -> -> **Аргументы** -> -> * **state** `int`: Режим торговли. - - 0 - Торговля отключена. - - 1 - Торговля включена. -> -> *Возвращает*: *class* [`TradeMode`](#trademode) - -#### `get_price_range`(*self, gid, mode='sell'*) -> Получить размах цен. -> -> **Аргументы** -> -> * **gid** `int`: ID группы предметов. -> * **mode** `str`: Режим получения - - 'sell' - Цены запросов на продажу. Значение по умолчанию. - - 'buy' - Цены запросов на покупку. - - 'history' - Цены из истории продаж. Максимум 100 пунктов. -> -> *Возвращает*: *NamedTupple* [`PriceRange`](#pricerange) - -## Датаклассы - -### `TradeMode` - -[//]: # (::: steam_trader.ext.TradeMode) -> Класс, представляющий режим торговли. - -`success` -> Результат запроса. -> -> **Тип**: `bool` - -`state` -> Режим обычной торговли. -> -> **Тип**: `bool` - -`p2p` -> Режим p2p торговли. -> -> **Тип**: `bool` - -`client` -> Клиент Steam Trader. -> -> **Тип**: Union[ *class* [`Client`](client.md#client), *class* [`ClientAsync`](client.md#client), `None` ] - -### `PriceRange` -> NamedTuple, представляющий размах цен. - -`lowest` -> Минимальная цена. -> -> **Тип**: `float` - -`highest` -> Максимальная цена. -> -> **Тип**: `float` \ No newline at end of file +::: steam_trader.api.ext.ExtClient diff --git a/docs/ext_guide.md b/docs/ext_guide.md index 6cf4bff..47201bd 100644 --- a/docs/ext_guide.md +++ b/docs/ext_guide.md @@ -1,9 +1,12 @@ # Расширенный функционал + Библиотека предоставляет дополнительный функционал, который отсутствует в документации сайта. Такой функционал будет полезен для оптимизации рутинных задач, или упрощения создания простых вещей. ## Инициализация + Инициализация расширенного клиента похожа на инициализацию обычного. + ```python from steam_trader.ext import ExtClient @@ -21,11 +24,13 @@ client = ExtClientAsync('Ваш токен') ## Изменённые методы ### get_inventory() + Добавлен аргумент filters для отсеивания предметов. Для использования фильтра необходимо создать его экземпляр. Обязательно укажите id, другие поля опциональны. + ```python -from steam_trader import Filters, Filter -from steam_trader.ext import ExtClient +from steam_trader.api import Filters, Filter +from steam_trader.api.ext import ExtClient from steam_trader.constants import * client = ExtClient('Ваш токен') @@ -46,10 +51,12 @@ filtered_inventory = client.get_inventory(TEAM_FORTRESS2_APPID, filters=filters) ## Новые методы ### multi_sell() + Аналог multi_buy. Продаёт все предметы в инвентаре по gid. В отличие от него, возвращает последовательноасть из результатов продаж, а не один объект. Если количество продаж больше чем соответствующих предметов в инвентаре, будут проданы те, что есть. + ```python -from steam_trader.ext import ExtClient +from steam_trader.api.ext import ExtClient from steam_trader.constants import TEAM_FORTRESS2_APPID client = ExtClient('Ваш токен') @@ -58,11 +65,13 @@ multi_sell_result = client.multi_sell(TEAM_FORTRESS2_APPID, 1220, 9.2, 10) ``` ### set_trade_mode() + Задать режим торговли. Данного метода нет в документации сайта. Режим 0 - торговля отключена. Режим 1 - торговля включена. + ```python -from steam_trader.ext import ExtClient +from steam_trader.api.ext import ExtClient client = ExtClient('Ваш токен') @@ -70,18 +79,19 @@ trade_mode = client.set_trade_mode(1) ``` ### get_price_range() + Получить размах цен. Режим получения: -'sell' - Цены запросов на продажу. Значение по умолчанию. -'buy' - Цены запросов на покупку. +'sell' - Цены запросов на продажу. Значение по умолчанию. +'buy' - Цены запросов на покупку. 'history' - Цены из истории продаж. Максимум 100 пунктов. ```python -from steam_trader.ext import ExtClient +from steam_trader.api.ext import ExtClient client = ExtClient('Ваш токен') price_range = client.get_price_range(1220, mode='sell') # PriceRange(lowest=1.04, highest=10) -``` \ No newline at end of file +``` diff --git a/docs/index.md b/docs/index.md index 8d4ce1c..fd602e4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ # Введение + ![PyPI - Downloads](https://img.shields.io/pypi/dm/steam-trader) ![PyPI - License](https://img.shields.io/pypi/l/steam-trader) ![PyPI - Status](https://img.shields.io/pypi/status/steam-trader) @@ -7,7 +8,7 @@ Она совместима с версиями Python 3.12+ и поддерживает работу как с синхронным, так и с асинхронным (asyncio) кодом. -В дополнение к реализации чистого API данная библиотека имеет ряд объектов высокого уровня и логирование, +В дополнение к реализации чистого API данная библиотека имеет ряд объектов высокого уровня и логирование, дабы сделать разработку клиентов и скриптов простой и понятной. Документация была написана исходя из API документации сайта. ## Возможности библиотеки @@ -20,7 +21,6 @@ * Подробная документация и тайп-хинты. * Поддержка логов и исключений. * Синхронный и асинхронный клиенты. -* Будущая поддержку от автора и сообщества. * Реализация всего функционала, предоставляемого сайтом. * Надёжность и корректность запросов. * Расширенный функционал ext. @@ -51,12 +51,13 @@ python setup.py install Для полного содержания библиотеки см. [Справочник по API](client.md). ## Лицензия + См. Оригинал на английском [LICENSE](https://github.com/Lemon4ksan/SteamTrader-Wrapper/blob/master/LICENSE). Разрешается повторное распространение и использование как в виде исходного кода, так и в двоичной форме, с изменениями или без, при соблюдении следующих условий: -- При повторном распространении исходного кода должно оставаться указанное выше уведомление об авторском праве, этот список условий и последующий отказ от гарантий. -- При повторном распространении двоичного кода должна сохраняться указанная выше информация об авторском праве, этот список условий и последующий отказ от гарантий в документации и/или в других материалах, поставляемых при распространении. -- Ни имя автора, ни имена участников не могут быть использованы в качестве поддержки или продвижения продуктов, основанных на этом ПО без предварительного письменного разрешения. +* При повторном распространении исходного кода должно оставаться указанное выше уведомление об авторском праве, этот список условий и последующий отказ от гарантий. +* При повторном распространении двоичного кода должна сохраняться указанная выше информация об авторском праве, этот список условий и последующий отказ от гарантий в документации и/или в других материалах, поставляемых при распространении. +* Ни имя автора, ни имена участников не могут быть использованы в качестве поддержки или продвижения продуктов, основанных на этом ПО без предварительного письменного разрешения. ЭТА ПРОГРАММА ПРЕДОСТАВЛЕНА ВЛАДЕЛЬЦАМИ АВТОРСКИХ ПРАВ И/ИЛИ ДРУГИМИ СТОРОНАМИ «КАК ОНА ЕСТЬ» БЕЗ КАКОГО-ЛИБО ВИДА ГАРАНТИЙ, ВЫРАЖЕННЫХ ЯВНО ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ, ПОДРАЗУМЕВАЕМЫЕ ГАРАНТИИ КОММЕРЧЕСКОЙ ЦЕННОСТИ И ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ. НИ В КОЕМ СЛУЧАЕ НИ ОДИН ВЛАДЕЛЕЦ АВТОРСКИХ ПРАВ И НИ ОДНО ДРУГОЕ ЛИЦО, КОТОРОЕ МОЖЕТ ИЗМЕНЯТЬ И/ИЛИ ПОВТОРНО РАСПРОСТРАНЯТЬ ПРОГРАММУ, КАК БЫЛО СКАЗАНО ВЫШЕ, НЕ НЕСЁТ ОТВЕТСТВЕННОСТИ, ВКЛЮЧАЯ ЛЮБЫЕ ОБЩИЕ, СЛУЧАЙНЫЕ, СПЕЦИАЛЬНЫЕ ИЛИ ПОСЛЕДОВАВШИЕ УБЫТКИ, ВСЛЕДСТВИЕ ИСПОЛЬЗОВАНИЯ ИЛИ НЕВОЗМОЖНОСТИ ИСПОЛЬЗОВАНИЯ ПРОГРАММЫ (ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ПОТЕРЕЙ ДАННЫХ, ИЛИ ДАННЫМИ, СТАВШИМИ НЕПРАВИЛЬНЫМИ, ИЛИ ПОТЕРЯМИ, ПРИНЕСЕННЫМИ ИЗ-ЗА ВАС ИЛИ ТРЕТЬИХ ЛИЦ, ИЛИ ОТКАЗОМ ПРОГРАММЫ РАБОТАТЬ СОВМЕСТНО С ДРУГИМИ ПРОГРАММАМИ), ДАЖЕ ЕСЛИ ТАКОЙ ВЛАДЕЛЕЦ ИЛИ ДРУГОЕ ЛИЦО БЫЛИ ИЗВЕЩЕНЫ О ВОЗМОЖНОСТИ ТАКИХ УБЫТКОВ. diff --git a/docs/quickstart.md b/docs/quickstart.md index 7464194..50c4e24 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,26 +1,31 @@ # Начало работы ## Получение ключа + Перед началом работы необходимо получить токен для авторизации. Его можно получить [тут](https://steam-trader.com/api/). ## Инициализация клиента + Начнём с импортирования библиотеки. + ```python import steam_trader ``` Есть несколько способов работать с запросами: синхронно и асинхронно. Здесь мы разберём работу с синхронным клиентом. + ```python -from steam_trader import Client +from steam_trader.api import Client client = steam_trader.Client('Ваш токен') ``` Для оптимизации общения между клиентом и сервером можно также использовать контекстный менеджер with для поддержания сеанса, но это необязательно. + ```python -from steam_trader import Client +from steam_trader.api import Client client = steam_trader.Client('Ваш токен') with client: @@ -28,6 +33,7 @@ with client: ``` ## Основные понятия в запросах + Далее в документации будут упоминаться определённые термины, которые необходимо понимать. ### Что такое AppID, AssetID, ClassID, InstanceID и ContextID? @@ -36,79 +42,83 @@ AppID - это идентификатор игры с которой вы хот В библиотеке и документации встречается как gameid. AppID можно встретить в большинстве операций с предметами. Рекомендуется использовать константы для большего понимания. + ```python from steam_trader.constants import TEAM_FORTRESS2_APPID, DOTA2_APPID from steam_trader.constants import SUPPORTED_APPIDS ``` + --- -AssetID - это уникальный идентификатор для актива (предмета) в Steam. Активы также имеют ClassID и InstanceID, которые являются указанием на фактическое представление элемента. +**AssetID** - это уникальный идентификатор для актива (предмета) в Steam. Активы также имеют ClassID и InstanceID, которые являются указанием на фактическое представление элемента. Объекты активов также могут иметь свойства количества, чтобы указать, сколько этого точного экземпляра пользователь имеет в случае однотипных предметов. Идентификаторы активов также могут меняться при торговле предметом, хотя classid и instanceid должны оставаться прежними, ЕСЛИ свойства предмета не изменились во время торговли. -AssetID можно встретить в -[get_items_for_exchange](client.md#get_items_for_exchangeself), -[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), -[exchange](client.md#exchangeself), -[exchange_p2p](client.md#exchange_p2pself) и +AssetID можно встретить в +[get_items_for_exchange](client.md#get_items_for_exchangeself), +[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), +[exchange](client.md#exchangeself), +[exchange_p2p](client.md#exchange_p2pself) и [get_inventory](client.md#get_inventoryself-gameid--statusnone). --- -ClassID - это идентификатор, который определяет класс элемента, свойства которого одинаковы для всех элементов с этим идентификатором класса. +**ClassID** - это идентификатор, который определяет класс элемента, свойства которого одинаковы для всех элементов с этим идентификатором класса. -ClassID можно встретить в -[get_items_for_exchange](client.md#get_items_for_exchangeself), -[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), -[exchange](client.md#exchangeself), -[exchange_p2p](client.md#exchange_p2pself) и +ClassID можно встретить в +[get_items_for_exchange](client.md#get_items_for_exchangeself), +[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), +[exchange](client.md#exchangeself), +[exchange_p2p](client.md#exchange_p2pself) и [get_item_info](client.md#get_item_infoself-gid). --- -InstanceID - Это идентификатор, описывающий экземпляр элемента, который наследует свойства от класса, причём идентификатор класса указан в экземпляре. +**InstanceID** - Это идентификатор, описывающий экземпляр элемента, который наследует свойства от класса, причём идентификатор класса указан в экземпляре. -InstanceID можно встретить в -[get_items_for_exchange](client.md#get_items_for_exchangeself), -[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), -[exchange](client.md#exchangeself), -[exchange_p2p](client.md#exchange_p2pself) и +InstanceID можно встретить в +[get_items_for_exchange](client.md#get_items_for_exchangeself), +[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), +[exchange](client.md#exchangeself), +[exchange_p2p](client.md#exchange_p2pself) и [get_item_info](client.md#get_item_infoself-gid). --- -ContextID - это способ организации/категоризирования предметов/активов/валюты. +**ContextID** - это способ организации/категоризирования предметов/активов/валюты. Это просто целое число, но в документации Steam описывается способ сделать его в некоторой степени основанным на папках, разделив целое число на диапазоны битов и используя каждый диапазон битов для обозначения чего-то другого. -ContextID можно встретить в -[get_items_for_exchange](client.md#get_items_for_exchangeself), -[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), -[exchange](client.md#exchangeself), -[exchange_p2p](client.md#exchange_p2pself) и +ContextID можно встретить в +[get_items_for_exchange](client.md#get_items_for_exchangeself), +[get_items_for_exchange_p2p](client.md#get_items_for_exchange_p2pself), +[exchange](client.md#exchangeself), +[exchange_p2p](client.md#exchange_p2pself) и [get_item_info](client.md#get_item_infoself-gid). ### Что такое GID и ItemID? -GID - Это идентификатор группы предметов. Под ним подразумеваются все предложения о продаже предмета и общая информация о нём. +**GID** - Это идентификатор группы предметов. Под ним подразумеваются все предложения о продаже предмета и общая информация о нём. GID можно найти в ссылке при просмотре на сайте или в предметах, получаемых через [get_inventory](client.md#get_inventoryself-gameid--statusnone). или напрямую через сайт. -``` + +```None https://steam-trader.com/tf2/1226-Refined-Metal - |---------^^^^----------| + |---------^^^^---------| Данные цифры являются GID ``` + Если вы хотите самостоятельно посмотреть предмет с соответствующим GID, вы можете поменять эти цифры на другие. --- -ItemID - Это идентификатор конкретного предмета. Он позволяет отличить его от других предметов в одной категории. +**ItemID** - Это идентификатор конкретного предмета. Он позволяет отличить его от других предметов в одной категории. ItemID можно получить при покупке предмета или через [get_inventory](client.md#get_inventoryself-gameid--statusnone). - ## Создание запросов + Для создания запросов необходимо использовать предоставленные клиентом методы. Весь их список можно найти в [Справочнике по API](client.md). ```python -from steam_trader import Client +from steam_trader.api import Client from steam_trader.constants import TEAM_FORTRESS2_APPID -client = steam_trader.Client('Ваш токен') +client = Client('Ваш токен') item_info = client.get_item_info(1220) inventory = client.get_inventory(TEAM_FORTRESS2_APPID, status=[0, 1, 2]) @@ -117,9 +127,9 @@ inventory = client.get_inventory(TEAM_FORTRESS2_APPID, status=[0, 1, 2]) После выполнения запроса возвращается объект класса с ответом от сервера. Чтобы получить значения, используйте соответствующие аттрибуты. ```python -from steam_trader import Client +from steam_trader.api import Client -client = steam_trader.Client('Ваш токен') +client = Client('Ваш токен') orders = client.get_order_book(1556) sell_orders = orders.sell @@ -131,14 +141,15 @@ for inventory_item in inventory_items: ``` ## Работа с исключениями + Во время выполнения вашей программы могут прийти неудачные ответы от сервера. Вам необходимо предусмотреть действия при их возникновении. Данная библиотека предоставляет все ошибки, которые могут произойти на сайте. ```python -from steam_trader import Client +from steam_trader.api import Client from steam_trader import exceptions -client = steam_trader.Client('Ваш токен') +client = Client('Ваш токен') try: client.buy(40814) @@ -151,6 +162,7 @@ except exceptions.SteamTraderError as e: ``` ## Использование констант + Для того чтобы вам не пришлось запоминать все уникальные ID, в библиотеке предусмотренны константы. ```python @@ -164,6 +176,7 @@ print(constants.TF2_TYPE_PRIMARY) ``` ## Подключение логов + Согласитесь, писать принты каждый раз для проверки валидности данных утомляет. Поэтому данная библиотека поддерживает логи! Чтобы подключить логирование, добавьте следующие строчки в свой код: @@ -175,9 +188,10 @@ logging.basicConfig(level=logging.INFO) !!! Подсказка Если вы хотите получать меньше логов от модуля httpx то добавите это в свой код: ```logging.getLogger('httpx').setLevel(logging.WARNING)```. - Так вы будете получать только логи предупреждения и выше. + Так вы будете получать только логи уровня предупреждение и выше. ## Заключение + Теперь вы знаете всё для начала работы. Для дальнейшего ознакомления изучите работу с [асинхронным клиентом](async.md) и [ext функционалом](ext_guide.md). Для полного содержания библиотеки см. [Справочник по API](client.md) diff --git a/docs/web.md b/docs/web.md new file mode 100644 index 0000000..a18bfc9 --- /dev/null +++ b/docs/web.md @@ -0,0 +1,16 @@ +# Web API + +## Клиент + +Одинаково для синхронного и асинхронного клиента. + +::: steam_trader.web._client.WebClient + options: + heading_level: 3 + +## Датаклассы + +::: steam_trader.web._dataclasses + options: + heading_level: 3 + filters: ["!de_json"] diff --git a/mkdocs.yml b/mkdocs.yml index 97b286e..c3bb3f7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,18 +22,28 @@ repo_url: https://github.com/Lemon4ksan/SteamTrader-Wrapper edit_uri: "" nav: - - Введение: 'index.md' - - Начало работы: 'quickstart.md' - - Асинхронный клиент: 'async.md' - - Расширенный функционал: 'ext_guide.md' - - Справочник по API: - - Основной клиент: 'client.md' - - Датаклассы: 'dataclasses.md' - - Ext клиент: 'ext.md' - - Исключения: 'exceptions.md' + - Введение: 'index.md' + - Начало работы: 'quickstart.md' + - Асинхронный клиент: 'async.md' + - Расширенный функционал: 'ext_guide.md' + - Справочник по API: + - Основной клиент: 'client.md' + - Датаклассы: 'dataclasses.md' + - Ext клиент: 'ext.md' + - Исключения: 'exceptions.md' + - Web API: 'web.md' + +plugins: +- mkdocstrings: + handlers: + python: + options: + show_source: false + show_symbol_type_heading: true + docstring_style: google +- search markdown_extensions: - admonition - codehilite: - css_class: highlight - - mkautodoc \ No newline at end of file + css_class: highlight \ No newline at end of file From 419fd116e79d93245d6b1daa7397a10f9eb3a802 Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Sat, 4 Jan 2025 13:17:52 +0300 Subject: [PATCH 6/7] =?UTF-8?q?git:=20=D0=9F=D0=B5=D1=80=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BD=D0=B0=201.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c422aa4..60463ad 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='steam-trader', - version='0.4.0', + version='1.0.0', author='Lemon4ksan (Bananchiki)', author_email='senya20151718@gmail.com', license='BSD License', From 29019d841905b6ec87753ee7f56ee368c32ed9eb Mon Sep 17 00:00:00 2001 From: Lemon4ksan Date: Sat, 4 Jan 2025 13:18:11 +0300 Subject: [PATCH 7/7] =?UTF-8?q?git:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20.github?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug-report.md | 3 ++- .github/ISSUE_TEMPLATE/feature-request.md | 2 +- .github/workflows/ci.yml | 30 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index a4080ce..dd86fbd 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -10,6 +10,7 @@ labels: bug **Воспроизведение:** Шаги для воспроизведения бага: + 1. Создать объект '...' 2. Вызвать метод '....' 3. Увидеть ошибку @@ -21,4 +22,4 @@ labels: bug Если это возможно, прикрепите сообщения об ошибке и полученный стектрэйс. **Дополнительная информация:** -Добавьте любой другой контекст о проблеме здесь. \ No newline at end of file +Добавьте любой другой контекст о проблеме здесь. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 0cb6d03..b462405 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -12,4 +12,4 @@ labels: feature-request Опишите как вашу идею можно реализовать, или приложите ваши наработки **Дополнительная информация:** -Добавьте любой другой контекст о проблеме здесь. \ No newline at end of file +Добавьте любой другой контекст о проблеме здесь. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5372904 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: ci +on: + push: + branches: + - master + - main +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install 'mkdocstrings[python]' + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force \ No newline at end of file