From 23f31ac002f7d58107bce5e0119786e6bcbb36b4 Mon Sep 17 00:00:00 2001 From: gleb Date: Thu, 15 Apr 2021 12:45:55 +0300 Subject: [PATCH 01/14] some refactor --- glQiwiApi/basic_requests_api.py | 47 +++++---------------------------- 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/glQiwiApi/basic_requests_api.py b/glQiwiApi/basic_requests_api.py index 6e0af52e..6e028336 100644 --- a/glQiwiApi/basic_requests_api.py +++ b/glQiwiApi/basic_requests_api.py @@ -28,45 +28,6 @@ '(KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36' -class Core: - """ - Class, which include some help methods to HttpXParser - - """ - - def __getattr__(self, item: Any) -> Any: - """ - Method, which can get an attribute of base_headers by this method - - :param item: key name of base_headers dict data - :return: - """ - return super().__getattribute__(item) - - def __eq__(self, other: Any) -> bool: - """ - Method to compare instances of parsers - - :param other: other object - :return: bool - """ - if isinstance(other, HttpXParser): - if other.base_headers == self.base_headers: - return True - return False - - def __setitem__(self, key, value) -> None: - """ - - :param key: key of base_headers dict - :param value: value of base_headers dict - :return: None - """ - self.base_headers.update( - {key: value} - ) - - class HttpXParser(AbstractParser): """ Обвертка над aiohttp @@ -80,7 +41,6 @@ def __init__(self): 'User-Agent': USER_AGENT, 'Accept-Language': "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7" } - self._core = Core() self._timeout = ClientTimeout( total=5, connect=None, @@ -133,7 +93,7 @@ async def _request( aiohttp.ClientSession initialization :return: Response instance """ - headers = headers if isinstance(headers, dict) else self.base_headers + headers = self.get_headers(headers) if isinstance(proxy, ProxyService): self._connector = ProxyConnector() @@ -195,6 +155,11 @@ async def _request( url=response.url.__str__() ) + def get_headers(self, headers: Optional[dict]) -> Optional[dict]: + if isinstance(headers, dict): + return headers + return self.base_headers + async def fetch( self, *, From ecadcc864bfcc10e29e2955298a334510d952801 Mon Sep 17 00:00:00 2001 From: gleb Date: Mon, 19 Apr 2021 23:48:58 +0300 Subject: [PATCH 02/14] add async to sync connector --- examples/basic_methods.py | 2 - examples/p2p.py | 9 +- examples/without_context.py | 4 +- glQiwiApi/__init__.py | 31 +- glQiwiApi/aiohttp_custom_api.py | 39 +- glQiwiApi/basic_requests_api.py | 21 +- glQiwiApi/mixins.py | 4 - glQiwiApi/qiwi/basic_qiwi_config.py | 6 +- glQiwiApi/qiwi/qiwi_api.py | 151 +++++++- glQiwiApi/types/__init__.py | 36 +- glQiwiApi/types/qiwi_types/__init__.py | 7 +- glQiwiApi/types/qiwi_types/payment_info.py | 33 ++ glQiwiApi/types/qiwi_types/qiwi_master.py | 18 + glQiwiApi/utils/basics.py | 157 +++++++- glQiwiApi/utils/basics.pyi | 396 ++++++++++++++++++++- glQiwiApi/utils/exceptions.py | 20 +- glQiwiApi/yoo_money/yoomoney_api.py | 3 +- 17 files changed, 857 insertions(+), 80 deletions(-) create mode 100644 glQiwiApi/types/qiwi_types/payment_info.py create mode 100644 glQiwiApi/types/qiwi_types/qiwi_master.py diff --git a/examples/basic_methods.py b/examples/basic_methods.py index c7379009..3f0d451e 100644 --- a/examples/basic_methods.py +++ b/examples/basic_methods.py @@ -5,7 +5,6 @@ TOKEN = "YOUR_API_ACCESS_TOKEN" WALLET = "+NUMBER" -PUBLIC_KEY = 'YOUR_PUBLIC_P2P_TOKEN' SECRET_KEY = 'YOUR_SECRET_P2P_TOKEN' @@ -13,7 +12,6 @@ async def basic_usage(): async with QiwiWrapper( api_access_token=TOKEN, phone_number=WALLET, - public_p2p=PUBLIC_KEY, secret_p2p=SECRET_KEY ) as wallet: # Так вы можете получить информацию по транзакции, зная её айди и тип diff --git a/examples/p2p.py b/examples/p2p.py index c9306d83..431c3d27 100644 --- a/examples/p2p.py +++ b/examples/p2p.py @@ -2,12 +2,11 @@ from glQiwiApi import QiwiWrapper, RequestError -PUBLIC_KEY = 'P2P PUBLIC_KEY' SECRET_KEY = 'P2P SECRET_KEY' async def p2p_usage(): - async with QiwiWrapper(secret_p2p=SECRET_KEY, public_p2p=PUBLIC_KEY) as w: + async with QiwiWrapper(secret_p2p=SECRET_KEY) as w: # bill id будет сгенерирован как str(uuid.uuid4()), если не был передан bill = await w.create_p2p_bill( amount=1, @@ -15,12 +14,12 @@ async def p2p_usage(): ) print(bill) # Так можно проверить статус на оплаченный - status = (await w.check_p2p_bill_status( + status_1 = (await w.check_p2p_bill_status( bill_id=bill.bill_id )) == 'PAID' # Или, начиная с версии 0.2.0 - status = await bill.check() - print(status) + status_2 = await bill.check() + print(status_1 == status_2) # Это выдаст ошибку, так как не передан api_access_token и phone_number # Вы можете в любой момент переназначить токен или номер try: diff --git a/examples/without_context.py b/examples/without_context.py index 065e7246..d73809d6 100644 --- a/examples/without_context.py +++ b/examples/without_context.py @@ -3,11 +3,11 @@ from glQiwiApi import QiwiWrapper -# Создаем объект кошелька и обязательно передаем without_context = True, иначе будут проблемы с aiohttp.ClientSession +# Создаем объект кошелька и обязательно передаем without_context = True, +# иначе будут проблемы с aiohttp.ClientSession wallet = QiwiWrapper( api_access_token='token', phone_number="+number", - public_p2p="your public_p2p token", secret_p2p="your secret p2p", without_context=True ) diff --git a/glQiwiApi/__init__.py b/glQiwiApi/__init__.py index 7c1fb754..708b8e43 100644 --- a/glQiwiApi/__init__.py +++ b/glQiwiApi/__init__.py @@ -1,14 +1,27 @@ import sys -from glQiwiApi.qiwi import QiwiWrapper -from glQiwiApi.utils.exceptions import RequestError, VersionError -from glQiwiApi.yoo_money import YooMoneyAPI - -__all__ = [ - 'QiwiWrapper', - 'YooMoneyAPI', - 'RequestError' -] +from .qiwi import QiwiWrapper # NOQA +from .utils.basics import sync, to_datetime # NOQA +from .utils.exceptions import * # NOQA +from .yoo_money import YooMoneyAPI # NOQA + +__all__ = ( + ( + 'QiwiWrapper', + 'YooMoneyAPI', + 'RequestError', + 'to_datetime', + 'sync' + ) + utils.exceptions.__all__ # NOQA +) + + +class VersionError(Exception): + """ + Ошибка возникает, если ваша версия python не поддерживается библиотекой + + """ + if not sys.version_info[:2] >= (3, 7): raise VersionError( diff --git a/glQiwiApi/aiohttp_custom_api.py b/glQiwiApi/aiohttp_custom_api.py index 7610ae3c..a6c82d70 100644 --- a/glQiwiApi/aiohttp_custom_api.py +++ b/glQiwiApi/aiohttp_custom_api.py @@ -1,5 +1,7 @@ from typing import Dict, Optional, Any, Union +import aiohttp + import glQiwiApi from glQiwiApi.basic_requests_api import HttpXParser, SimpleCache from glQiwiApi.types import Response @@ -25,32 +27,46 @@ def __init__( self._without_context = without_context self.messages = messages self._cache = SimpleCache(cache_time) + self._cached_key = "session" def clear_cache(self) -> None: self._cache.clear(force=True) + def get_cached_session(self) -> Optional[aiohttp.ClientSession]: + cached = self._cache[self._cached_key] + if self.check_session(cached): + return cached + + def set_cached_session(self): + cached_session = self.get_cached_session() + if cached_session: + self.session = cached_session + async def _request(self, *args, **kwargs) -> Response: # Получаем текущий кэш используя ссылку как ключ response = self._cache.get_current(kwargs.get('url')) if not self._cache.validate(kwargs): + self.set_cached_session() response = await super()._request(*args, **kwargs) # Проверяем, не был ли запрос в кэше, если нет, # то проверяем статус код и если он не 200 - выбрасываем ошибку if not isinstance(response, CachedResponse): if response.status_code != 200: + await self._close_session() self.raise_exception( response.status_code, json_info=response.response_data ) - if self._without_context: - await self.session.close() + await self._close_session() self._cache.update_data( result=response.response_data, kwargs=kwargs, status_code=response.status_code ) + self.cache_session(self.session) + return response def raise_exception( @@ -65,3 +81,22 @@ def raise_exception( additional_info=f"{glQiwiApi.__version__} version api", json_info=json_info ) + + def cache_session(self, session: aiohttp.ClientSession) -> None: + if self.check_session(session): + self._cache[self._cached_key] = session + + async def _close_session(self) -> None: + if self._without_context: + await self.session.close() + + @staticmethod + def check_session(session: Any) -> bool: + if isinstance(session, aiohttp.ClientSession): + if not session.closed: + return True + return False + + def create_session(self, **kwargs) -> None: + self.set_cached_session() + super().create_session(**kwargs) diff --git a/glQiwiApi/basic_requests_api.py b/glQiwiApi/basic_requests_api.py index 6e028336..428af3b5 100644 --- a/glQiwiApi/basic_requests_api.py +++ b/glQiwiApi/basic_requests_api.py @@ -1,11 +1,15 @@ +import abc import asyncio import time from itertools import repeat from typing import ( - Optional, Union, Dict, - List, Tuple, Any, AsyncGenerator + Dict, + Tuple, + AsyncGenerator ) +from typing import Optional, List, Any, Union +import aiohttp from aiohttp import ( ClientTimeout, ClientRequest, @@ -33,6 +37,7 @@ class HttpXParser(AbstractParser): Обвертка над aiohttp """ + _sleep_time = 2 def __init__(self): @@ -78,7 +83,7 @@ async def _request( может возвращать в Response ProxyError в качестве response_data, это означает, что вы имеете проблемы с подключением к прокси, возможно нужно добавить дополнительные post данные, - если вы используете method = POST, или headers, + если вы используете method = POST, или headers, если запрос GET @@ -244,6 +249,16 @@ def clear(self, key: Optional[str] = None, force: bool = False) -> Any: return self.tmp_data.clear() return self.tmp_data.pop(key) + def __setitem__(self, key, value) -> None: + self.tmp_data.update( + {key: value} + ) + + def __getitem__(self, item) -> Union[ + CachedResponse, aiohttp.ClientSession + ]: + return self.tmp_data.get(item) + def update_data( self, result: Any, diff --git a/glQiwiApi/mixins.py b/glQiwiApi/mixins.py index cc777f5e..3aa53d00 100644 --- a/glQiwiApi/mixins.py +++ b/glQiwiApi/mixins.py @@ -49,7 +49,3 @@ def __deepcopy__(self, memo) -> 'ToolsMixin': setattr(result, k, copy.deepcopy(v, memo)) return result - - @property - def parser(self): - return self._parser diff --git a/glQiwiApi/qiwi/basic_qiwi_config.py b/glQiwiApi/qiwi/basic_qiwi_config.py index 9ded4a9d..0ccb9076 100644 --- a/glQiwiApi/qiwi/basic_qiwi_config.py +++ b/glQiwiApi/qiwi/basic_qiwi_config.py @@ -12,10 +12,7 @@ } ERROR_CODE_NUMBERS = { - "400": "Ошибка синтаксиса запроса (неправильный формат данных)." - "Если вы используете to_wallet или to_card," - "вероятно, у вас недостаточно денег для перевода " - "или вы указали неверный номер счета для перевода", + "400": "Недостаточно средств для проведения операции", "401": "Неверный токен или истек срок действия токена API", "403": "Нет прав на данный запрос" "(недостаточно разрешений у токена API)", @@ -118,7 +115,6 @@ 'PAYMENTS_PROVIDER_PAYOUT', 'WITHDRAW_CASH' ] - LIMIT_TYPES_TRANSFER = { 'max': 'max_limit', 'type': 'limit_type' diff --git a/glQiwiApi/qiwi/qiwi_api.py b/glQiwiApi/qiwi/qiwi_api.py index 72365587..1b25569d 100644 --- a/glQiwiApi/qiwi/qiwi_api.py +++ b/glQiwiApi/qiwi/qiwi_api.py @@ -4,6 +4,7 @@ from typing import Union, Optional, Dict, List, Any import aiofiles +import aiohttp import glQiwiApi.utils.basics as api_helper from glQiwiApi.abstracts import AbstractPaymentWrapper @@ -28,7 +29,9 @@ Response, Sum, Commission, - OptionalSum + OptionalSum, + PaymentInfo, + OrderDetails ) from glQiwiApi.types.basics import DEFAULT_CACHE_TIME from glQiwiApi.types.qiwi_types.bill import RefundBill @@ -49,7 +52,6 @@ class QiwiWrapper(AbstractPaymentWrapper, ToolsMixin): 'api_access_token', 'phone_number', 'secret_p2p', - 'public_p2p', '_parser', ) @@ -58,7 +60,6 @@ def __init__( api_access_token: Optional[str] = None, phone_number: Optional[str] = None, secret_p2p: Optional[str] = None, - public_p2p: Optional[str] = None, without_context: bool = False, cache_time: Union[float, int] = DEFAULT_CACHE_TIME ) -> None: @@ -66,7 +67,6 @@ def __init__( :param api_access_token: токен, полученный с https://qiwi.com/api :param phone_number: номер вашего телефона с + :param secret_p2p: секретный ключ, полученный с https://p2p.qiwi.com/ - :param public_p2p: публичный ключ, полученный с https://p2p.qiwi.com/ :param without_context: bool, указывает будет ли объект класса "глобальной" переменной или будет использована в async with контексте :param cache_time: Время кэширование запросов в секундах, @@ -74,8 +74,6 @@ def __init__( запрос не будет использовать кэш по дефолту, максимальное время кэширование 60 секунд """ - super().__init__() - if isinstance(phone_number, str): self.phone_number = phone_number.replace('+', '') if self.phone_number.startswith('8'): @@ -86,7 +84,6 @@ def __init__( cache_time=cache_time ) self.api_access_token = api_access_token - self.public_p2p = public_p2p self.secret_p2p = secret_p2p def _auth_token(self, headers: dict, p2p: bool = False) -> dict: @@ -95,6 +92,11 @@ def _auth_token(self, headers: dict, p2p: bool = False) -> dict: ) return headers + @property + def session(self) -> aiohttp.ClientSession: + """Return aiohttp session object""" + return self._parser.session + @property def stripped_number(self) -> str: return self.phone_number.replace("+", "") @@ -199,7 +201,7 @@ async def transactions( operation: str = 'ALL', start_date: Optional[datetime] = None, end_date: Optional[datetime] = None - ) -> Union[Optional[List[Transaction]], dict]: + ) -> List[Transaction]: """ Метод для получения транзакций на счёту Более подробная документация: @@ -303,7 +305,7 @@ async def transaction_info( self, transaction_id: Union[str, int], transaction_type: str - ) -> Optional[Transaction]: + ) -> Transaction: """ Метод для получения полной информации о транзакции\n Подробная документация: @@ -572,7 +574,7 @@ async def create_p2p_bill( :param pay_source_filter: При открытии формы будут отображаться только указанные способы перевода """ - if not self.public_p2p or not self.secret_p2p: + if not self.secret_p2p: raise InvalidData('Не задан p2p токен') if not isinstance(bill_id, (str, int)): @@ -617,7 +619,7 @@ async def check_p2p_bill_status(self, bill_id: str) -> str: :param bill_id: номер p2p транзакции :return: статус транзакции строкой """ - if not self.public_p2p or not self.secret_p2p: + if not self.secret_p2p: raise InvalidData('Не задан p2p токен') data = deepcopy(P2P_DATA) @@ -636,7 +638,7 @@ async def reject_p2p_bill(self, bill_id: str) -> Bill: :param bill_id: номер p2p транзакции :return: Bill obj """ - if not self.public_p2p or not self.secret_p2p: + if not self.secret_p2p: raise InvalidData('Не задан p2p токен') data = deepcopy(P2P_DATA) headers = self._auth_token(data.headers, p2p=True) @@ -921,3 +923,128 @@ async def refund_bill( ) else json_bill_data.json() ): return RefundBill.parse_raw(response.response_data) + + async def buy_qiwi_master(self) -> PaymentInfo: + """ + Метод для покупки пакета QIWI Мастер + + Для вызова методов API вам потребуется токен API QIWI Wallet + с разрешениями на следующие действия: + + 1. Управление виртуальными картами, + 2. Запрос информации о профиле кошелька, + 3. Просмотр истории платежей, + 4. Проведение платежей без SMS. + + Эти права вы можете выбрать при создании нового апи токена, + чтобы пользоваться апи QIWI Master + """ + url, payload = api_helper.qiwi_master_data(self.stripped_number) + async for response in self._parser.fast().fetch( + url=url, + json=payload, + method='POST', + headers=self._auth_token(deepcopy(DEFAULT_QIWI_HEADERS)) + ): + return PaymentInfo.parse_raw(response.response_data) + + async def __pre_qiwi_master_request( + self, + card_alias: str = 'qvc-cpa' + ) -> OrderDetails: + """ + Метод для выпуска виртуальной карты QIWI Мастер + + :param card_alias: Тип карты + :return: OrderDetails + """ + url = BASE_QIWI_URL + "/cards/v2/persons/{number}/orders" + async for response in self._parser.fast().fetch( + url=url.format(number=self.stripped_number), + headers=self._auth_token(deepcopy(DEFAULT_QIWI_HEADERS)), + json={"cardAlias": card_alias}, + method='POST' + ): + return OrderDetails.parse_raw(response.response_data) + + async def _confirm_qiwi_master_request( + self, + card_alias: str = 'qvc-cpa' + ) -> OrderDetails: + """ + Подтверждение заказа выпуска карты + + :param card_alias: Тип карты + :return: OrderDetails + """ + details = await self.__pre_qiwi_master_request(card_alias) + url = BASE_QIWI_URL + '/cards/v2/persons/{}/orders' + async for response in self._parser.fast().fetch( + url=url.format( + self.stripped_number + ) + f'/{details.order_id}/submit', + headers=self._auth_token(deepcopy(DEFAULT_QIWI_HEADERS)), + method='PUT' + ): + return OrderDetails.parse_raw(response.response_data) + + async def __buy_new_qiwi_card( + self, + **kwargs + ) -> OrderDetails: + """ + Покупка карты, если она платная + + :param kwargs: + :return: OrderDetails + """ + url, payload = api_helper.new_card_data(**kwargs) + async for response in self._parser.fast().fetch( + url=url, + json=payload, + headers=self._auth_token(deepcopy(DEFAULT_QIWI_HEADERS)) + ): + return OrderDetails.parse_raw(response.response_data) + + async def issue_qiwi_master_card( + self, + card_alias: str = 'qvc-cpa' + ) -> OrderDetails: + """ + Выпуск новой карты, используя Qiwi Master API + + При выпуске карты производиться 3, а возможно 3 запроса, + а именно по такой схеме: + - __pre_qiwi_master_request - данный метод создает заявку + - _confirm_qiwi_master_request - подтверждает выпуск карты + - __buy_new_qiwi_card - покупает новую карту, + если такая карта не бесплатна + + + Подробная документация: + + https://developer.qiwi.com/ru/qiwi-wallet-personal/#qiwi-master-issue-card + + :param card_alias: Тип карты + :return: OrderDetails + """ + pre_response = await self._confirm_qiwi_master_request(card_alias) + if pre_response.status == 'COMPLETED': + return pre_response + return await self.__buy_new_qiwi_card( + ph_number=self.stripped_number, + order_id=pre_response.order_id + ) + + async def _cards_qiwi_master(self): + """ + Метод для получение списка всех ваших карт QIWI Мастер + + """ + url = BASE_QIWI_URL + '/cards/v1/cards/?vas-alias=qvc-master' + async for response in self._parser.fast().fetch( + url=url, + headers=self._auth_token(deepcopy(DEFAULT_QIWI_HEADERS)), + method='GET' + ): + return response diff --git a/glQiwiApi/types/__init__.py b/glQiwiApi/types/__init__.py index f11f7f05..4aa9df21 100644 --- a/glQiwiApi/types/__init__.py +++ b/glQiwiApi/types/__init__.py @@ -1,9 +1,18 @@ -from typing import Union +import concurrent.futures as futures +from typing import Union, TypeVar, Optional, Callable, Any from .basics import ( - Type, Sum, Commission, OptionalSum + Type, + Sum, + Commission, + OptionalSum +) +from .particular import ( + Response, + ProxyService, + WrapperData, + proxy_list ) -from .particular import (Response, ProxyService, WrapperData, proxy_list) from .qiwi_types import ( Bill, BillError, @@ -13,7 +22,10 @@ Limit, Account, QiwiAccountInfo, - Transaction + Transaction, + PaymentInfo, + OrderDetails, + RefundBill ) from .yoomoney_types import ( OperationType, @@ -35,6 +47,15 @@ Bill, BillError, Statistic, Balance, Identification ] +E_ = TypeVar( + 'E_', + futures.ThreadPoolExecutor, + futures.ProcessPoolExecutor, + Optional[None] +) + +FuncT = TypeVar('FuncT', bound=Callable[..., Any]) + __all__ = [ 'QiwiAccountInfo', 'Transaction', @@ -61,5 +82,10 @@ 'OptionalSum', 'Commission', 'BasicTypes', - 'PydanticTypes' + 'PydanticTypes', + 'PaymentInfo', + 'OrderDetails', + 'RefundBill', + 'E_', + 'FuncT' ] diff --git a/glQiwiApi/types/qiwi_types/__init__.py b/glQiwiApi/types/qiwi_types/__init__.py index 66c75efb..21c7e46a 100644 --- a/glQiwiApi/types/qiwi_types/__init__.py +++ b/glQiwiApi/types/qiwi_types/__init__.py @@ -1,13 +1,16 @@ from .account import Account from .account_info import QiwiAccountInfo from .balance import Balance -from .bill import Bill, BillError +from .bill import Bill, BillError, RefundBill from .identification import Identification from .limit import Limit +from .payment_info import PaymentInfo +from .qiwi_master import OrderDetails from .stats import Statistic from .transaction import Transaction __all__ = [ 'QiwiAccountInfo', 'Transaction', 'Bill', 'BillError', - 'Statistic', 'Limit', 'Account', 'Balance', 'Identification' + 'Statistic', 'Limit', 'Account', 'Balance', 'Identification', + 'PaymentInfo', 'OrderDetails', 'RefundBill' ] diff --git a/glQiwiApi/types/qiwi_types/payment_info.py b/glQiwiApi/types/qiwi_types/payment_info.py new file mode 100644 index 00000000..ebe73c57 --- /dev/null +++ b/glQiwiApi/types/qiwi_types/payment_info.py @@ -0,0 +1,33 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +from glQiwiApi.types import Sum + + +class Fields(BaseModel): + account: str + + +class State(BaseModel): + code: str + + +class TransactionInfo(BaseModel): + txn_id: int = Field(..., alias="id") + state: State + + +class PaymentInfo(BaseModel): + payment_id: int = Field(..., alias="id") + terms: str + fields: Fields + payment_sum: Sum = Field(..., alias="sum") + source: str + transaction: Optional[TransactionInfo] = None + comment: Optional[str] = None + + +__all__ = [ + 'PaymentInfo' +] diff --git a/glQiwiApi/types/qiwi_types/qiwi_master.py b/glQiwiApi/types/qiwi_types/qiwi_master.py new file mode 100644 index 00000000..340a0aa4 --- /dev/null +++ b/glQiwiApi/types/qiwi_types/qiwi_master.py @@ -0,0 +1,18 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +from glQiwiApi.types import Sum + + +class OrderDetails(BaseModel): + order_id: str = Field(..., alias="id") + card_alias: str = Field(..., alias="cardAlias") + status: str + price: Optional[Sum] = None + card_id: Optional[str] = Field(alias="cardId", default=None) + + +__all__ = ( + 'OrderDetails' +) diff --git a/glQiwiApi/utils/basics.py b/glQiwiApi/utils/basics.py index e095f332..08aacfd2 100644 --- a/glQiwiApi/utils/basics.py +++ b/glQiwiApi/utils/basics.py @@ -1,13 +1,18 @@ +import asyncio +import concurrent.futures as futures import functools as ft +import json import re import time import warnings +from copy import deepcopy from dataclasses import is_dataclass from datetime import datetime +from json import JSONDecodeError from typing import Optional, Union, Type import pytz -from pydantic import ValidationError +from pydantic import ValidationError, BaseModel from pytz.reference import LocalTimezone try: @@ -19,8 +24,26 @@ ) import json as orjson +# Локальная таймзона Local = LocalTimezone() +QIWI_MASTER = { + "id": str(int(time.time() * 1000)), + "sum": { + "amount": 2999, + "currency": "643" + }, + "paymentMethod": { + "type": "Account", + "accountId": "643" + }, + "comment": "test", + "fields": { + "account": "", + "vas_alias": "qvc-master" + } +} + def measure_time(func): """ @@ -30,8 +53,11 @@ def measure_time(func): @ft.wraps(func) async def wrapper(*args, **kwargs): start_time = time.monotonic() - await func(*args, **kwargs) - print(f'Executed for {time.monotonic() - start_time} secs') + result = await func(*args, **kwargs) + execute_time = time.monotonic() - start_time + print( + f'Function `{func.__name__}` executed for {execute_time} secs') + return result return wrapper @@ -46,7 +72,8 @@ def datetime_to_str_in_iso(obj, yoo_money_format=False): ' ').replace(" ", "T") + "Z" local_date = str(datetime.now(tz=Local)) pattern = re.compile(r'[+]\d{2}[:]\d{2}') - return obj.isoformat(' ').replace(" ", "T") + re.findall(pattern, local_date)[0] + from_pattern = re.findall(pattern, local_date)[0] + return obj.isoformat(' ').replace(" ", "T") + from_pattern def parse_auth_link(response_data): @@ -104,7 +131,8 @@ def format_objects(iterable_obj, obj, transfers=None): if key in obj.__dict__.get( '__annotations__').keys() or key in transfers.keys(): try: - fill_key = key if not transfers.get(key) else transfers.get( + fill_key = key if not transfers.get( + key) else transfers.get( key) except AttributeError: fill_key = key @@ -175,8 +203,12 @@ async def wrapper(*args, **kwargs): def custom_load(data): - import json - return json.loads(json.dumps(data)) + return orjson.loads(json.dumps(data)) + + +# Модель pydantic для перевода строки в datetime +class Parser(BaseModel): + dt: datetime def allow_response_code(status_code): @@ -196,3 +228,114 @@ async def wrapper(*args, **kwargs): return wrapper return wrap_func + + +def qiwi_master_data(ph_number): + url = "https://edge.qiwi.com" + '/sinap/api/v2/terms/28004/payments' + payload = deepcopy(QIWI_MASTER) + payload["fields"]["account"] = ph_number + return url, payload + + +def to_datetime(string_representation): + """ + Вспомогательная функция для перевода строки во время + + :param string_representation: дата в виде строки + :return: datetime representation + """ + try: + parsed = json.dumps( + {'dt': string_representation} + ) + return Parser.parse_raw(parsed).dt + except (ValidationError, JSONDecodeError) as ex: + return ex.json(indent=4) + + +def new_card_data(ph_number, order_id): + payload = deepcopy(QIWI_MASTER) + url = "https://edge.qiwi.com" + "/sinap/api/v2/terms/32064/payments" + payload["fields"].pop("vas_alias") + payload["fields"].update(order_id=order_id) + payload["fields"]["account"] = ph_number + return url, payload + + +def sync_measure_time(func): + @ft.wraps(func) + def wrapper(*args, **kwargs): + start_time = time.monotonic() + result = func(*args, **kwargs) + execute_time = time.monotonic() - start_time + print( + f'Function `{func.__name__}` executed for {execute_time} secs') + return result + + return wrapper + + +def _run_forever_safe(loop) -> None: + """ run a loop for ever and clean up after being stopped """ + + loop.run_forever() + # NOTE: loop.run_forever returns after calling loop.stop + + # -- cancel all tasks and close the loop gracefully + + loop_tasks_all = asyncio.all_tasks(loop=loop) + + for task in loop_tasks_all: + task.cancel() + # NOTE: `cancel` does not guarantee that the Task will be cancelled + + for task in loop_tasks_all: + if not (task.done() or task.cancelled()): + try: + # wait for task cancellations + loop.run_until_complete(task) + except asyncio.CancelledError: + pass + # Finally, close event loop + loop.close() + + +def _await_sync(future, executor, loop): + """ synchronously waits for a task """ + exception = future.exception() + if exception is not None: + _cancel_future( + loop=loop, + executor=executor, + future=future + ) + loop.call_soon_threadsafe(_stop_loop, loop) + return future.result() + + +def _cancel_future(loop, future, executor) -> None: + """ cancels future if any exception occurred """ + executor.submit(loop.call_soon_threadsafe, future.cancel) + + +def _stop_loop(loop) -> None: + """ stops an event loop """ + + loop.stop() + + +def sync(func, *args, **kwargs): + loop = asyncio.new_event_loop() # construct a new event loop + + executor = futures.ThreadPoolExecutor(2, 'sync_connector_') + my_task = asyncio.run_coroutine_threadsafe( + func(*args, **kwargs), + loop=loop + ) + # Run loop.run_forever(), but do it safely + executor.submit(_run_forever_safe, loop) + # Sync await our result + result = _await_sync(my_task, executor=executor, loop=loop) + # close created early loop + loop.call_soon_threadsafe(_stop_loop, loop) + return result diff --git a/glQiwiApi/utils/basics.pyi b/glQiwiApi/utils/basics.pyi index 170c6094..b628ddc4 100644 --- a/glQiwiApi/utils/basics.pyi +++ b/glQiwiApi/utils/basics.pyi @@ -1,5 +1,7 @@ -from datetime import datetime -from typing import Any, Union, Optional, Dict, Type, List, MutableMapping, Tuple +import asyncio +from datetime import datetime, timedelta +from typing import Any, Union, Optional, Dict, Type, List, MutableMapping, \ + Tuple, overload, Coroutine, Callable from glQiwiApi import types @@ -7,7 +9,8 @@ from glQiwiApi import types def measure_time(func: Any) -> None: ... -def datetime_to_str_in_iso(obj: datetime, yoo_money_format: bool = False) -> str: ... +def datetime_to_str_in_iso(obj: datetime, + yoo_money_format: bool = False) -> str: ... def format_objects_for_fill(data: dict, transfers: Dict[str, str]) -> dict: ... @@ -16,11 +19,13 @@ def format_objects_for_fill(data: dict, transfers: Dict[str, str]) -> dict: ... def parse_auth_link(response_data: Union[str, bytes]) -> Optional[str]: ... -def parse_headers(content_json: bool = False, auth: bool = False) -> Dict[Any, Any]: ... +def parse_headers(content_json: bool = False, auth: bool = False) -> Dict[ + Any, Any]: ... def format_objects(iterable_obj: Union[list, tuple], obj: Type, - transfers: Optional[Dict[str, str]] = None) -> Optional[List[types.BasicTypes]]: ... + transfers: Optional[Dict[str, str]] = None) -> Optional[ + List[types.BasicTypes]]: ... def set_data_to_wallet(data: types.WrapperData, @@ -41,7 +46,8 @@ def set_data_p2p_create( def multiply_objects_parse( - lst_of_objects: Union[List[str], Tuple[str, ...]], model: Type[types.PydanticTypes] + lst_of_objects: Union[List[str], Tuple[str, ...]], + model: Type[types.PydanticTypes] ) -> List[types.PydanticTypes]: ... @@ -49,10 +55,384 @@ def dump_response(func: Any) -> Any: ... def simple_multiply_parse(lst_of_objects: Union[List[Union[dict, str]], dict], - model: Type[types.PydanticTypes]) -> List[types.PydanticTypes]: ... + model: Type[types.PydanticTypes]) -> List[ + types.PydanticTypes]: ... def custom_load(data: Dict[Any, Any]) -> Dict[str, Any]: ... -def allow_response_code(status_code: Union[str, int]) -> Any: ... \ No newline at end of file +def allow_response_code(status_code: Union[str, int]) -> Any: ... + + +def qiwi_master_data(ph_number: str) -> Tuple[str, dict]: ... + + +def new_card_data(ph_number: str, order_id: str) -> Tuple[str, dict]: ... + + +def to_datetime(string_representation: str) -> datetime: ... + + +def sync_measure_time(func: Any) -> Any: ... + + +def _run_forever_safe(loop: asyncio.AbstractEventLoop) -> None: ... + + +def _await_sync(future: asyncio.Future, executor: types.E_, + loop: asyncio.AbstractEventLoop) -> Any: ... + + +def _cancel_future(loop: asyncio.AbstractEventLoop, future: asyncio.Future, + executor: types.E_) -> None: ... + + +def _stop_loop(loop: asyncio.AbstractEventLoop) -> None: ... + + +def sync( + func: types.FuncT, + *args: object, + **kwargs: object) -> Any: ... + + +@overload +def sync( + func: Callable[ + [str, str, str, str, str, str, str, str], Coroutine[ + Any, Any, Optional[Dict[str, bool]] + ] + ], + *args: Any, + **kwargs: Any +) -> Optional[Dict[str, bool]]: ... + + +@overload +def sync( + func: Callable[[Union[float, int], str], Coroutine[ + Any, Any, Union[types.Response, str]]], + *args: Any, + **kwargs: Any +) -> Union[types.Response, str]: ... + + +@overload +def sync( + func: Callable[[], Coroutine[Any, Any, types.Sum]], + *args: Any, + **kwargs: Any +) -> types.Sum: ... + + +@overload +def sync( + func: Callable[ + [int, str, Optional[datetime], Optional[datetime]], Coroutine[ + Any, Any, List[types.Transaction] + ] + ], + *args: Any, + **kwargs: Any +) -> List[types.Transaction]: ... + + +@overload +def sync( + func: Callable[ + [str, Union[str, float, int], str, str], Coroutine[ + Any, Any, Union[str, types.Response]] + ], + *args: Any, + **kwargs: Any +) -> Union[str, types.Response]: ... + + +@overload +def sync( + func: Callable[[Union[str, int], str], Coroutine[ + Any, Any, types.Transaction] + ], + *args: Any, + **kwargs: Any +) -> types.Transaction: ... + + +@overload +def sync( + func: Callable[[], Coroutine[Any, Any, Union[ + List[Dict[str, str]], Exception + ]]], + *args: Any, + **kwargs: Any +) -> Union[ + List[Dict[str, str]], Exception +]: ... + + +@overload +def sync( + func: Callable[[], Coroutine[Any, Any, types.Identification]], + *args: Any, + **kwargs: Any +) -> types.Identification: ... + + +@overload +def sync( + func: Callable[ + [ + Union[int, float], + str, + Optional[str], + int, + Optional[str] + ], + Coroutine[Any, Any, bool] + ], + *args: Any, + **kwargs: Any +) -> bool: ... + + +@overload +def sync( + func: Callable[[], Coroutine[Any, Any, Dict[str, types.Limit]]], + *args: Any, + **kwargs: Any +) -> Dict[str, types.Limit]: ... + + +@overload +def sync( + func: Callable[[int], Coroutine[Any, Any, List[types.Bill]]], + *args: Any, + **kwargs: Any +) -> List[types.Bill]: ... + + +@overload +def sync( + func: Callable[[int, ...], Coroutine[ + Any, Any, Union[types.Bill, types.BillError]]], + *args: Any, + **kwargs: Any +) -> Union[types.Bill, types.BillError]: ... + + +@overload +def sync( + func: Callable[[str], Coroutine[Any, Any, types.Bill]], + *args: Any, + **kwargs: Any +) -> types.Bill: ... + + +@overload +def sync( + func: Callable[[Union[str, int], str, Optional[str]], Coroutine[ + Any, Any, Union[bytearray, int]] + ], + *args: Any, + **kwargs: Any +) -> Union[bytearray, int]: ... + + +@overload +def sync( + func: Callable[ + [str, Union[int, float]], Coroutine[Any, Any, types.Commission]], + *args: Any, + **kwargs: Any +) -> types.Commission: ... + + +@overload +def sync( + func: Callable[ + [], Coroutine[Any, Any, types.QiwiAccountInfo]], + *args: Any, + **kwargs: Any +) -> types.QiwiAccountInfo: ... + + +@overload +def sync( + func: Callable[ + [Union[datetime, timedelta], Union[datetime, timedelta], str, ...], + Coroutine[Any, Any, types.Statistic]], + *args: Any, + **kwargs: Any +) -> types.Statistic: ... + + +@overload +def sync( + func: Callable[ + [], Coroutine[Any, Any, List[types.Account]]], + *args: Any, + **kwargs: Any +) -> List[types.Account]: ... + + +@overload +def sync( + func: Callable[ + [str], Coroutine[Any, Any, Optional[Dict[str, bool]]]], + *args: Any, + **kwargs: Any +) -> Optional[Dict[str, bool]]: ... + + +@overload +def sync( + func: Callable[ + [], Coroutine[Any, Any, List[types.Balance]]], + *args: Any, + **kwargs: Any +) -> List[types.Balance]: ... + + +@overload +def sync( + func: Callable[ + [Union[str, int], Union[str, int], + Union[types.OptionalSum, Dict[str, Union[str, int]]] + ], Coroutine[Any, Any, types.RefundBill]], + *args: Any, + **kwargs: Any +) -> types.RefundBill: ... + + +@overload +def sync( + func: Callable[ + [], Coroutine[Any, Any, types.PaymentInfo]], + *args: Any, + **kwargs: Any +) -> types.PaymentInfo: ... + + +@overload +def sync( + func: Callable[ + [str], Coroutine[Any, Any, types.OrderDetails]], + *args: Any, + **kwargs: Any +) -> types.OrderDetails: ... + + +@overload +def sync( + func: Callable[ + [List[str], str, str], Coroutine[Any, Any, str]], + *args: Any, + **kwargs: Any +) -> str: ... + + +@overload +def sync( + func: Callable[ + [str, str, str], Coroutine[Any, Any, str]], + *args: Any, + **kwargs: Any +) -> str: ... + + +@overload +def sync( + func: Callable[ + [], Coroutine[Any, Any, Optional[Dict[str, bool]]]], + *args: Any, + **kwargs: Any +) -> Optional[Dict[str, bool]]: ... + + +@overload +def sync( + func: Callable[ + [], Coroutine[Any, Any, types.AccountInfo]], + *args: Any, + **kwargs: Any +) -> types.AccountInfo: ... + + +@overload +def sync( + func: Callable[ + [Optional[ + Union[ + List[types.OperationType], Tuple[ + types.OperationType, ...] + ] + ], + Optional[datetime], Optional[datetime], Optional[int], int, + Optional[Union[str, int]] + ], Coroutine[Any, Any, List[types.Operation]]], + *args: Any, + **kwargs: Any +) -> List[types.Operation]: ... + + +@overload +def sync( + func: Callable[ + [str], Coroutine[Any, Any, types.OperationDetails]], + *args: Any, + **kwargs: Any +) -> types.OperationDetails: ... + + +@overload +def sync( + func: Callable[ + [ + str, Union[int, float], str, str, str, Optional[str], + bool, Optional[str], Optional[str], int + ], Coroutine[Any, Any, types.Payment]], + *args: Any, + **kwargs: Any +) -> types.Payment: ... + + +@overload +def sync( + func: Callable[ + [Optional[None]], Coroutine[Any, Any, Optional[Union[float, int]]]], + *args: Any, + **kwargs: Any +) -> Optional[Union[float, int]]: ... + + +@overload +def sync( + func: Callable[ + [str, str], Coroutine[Any, Any, types.IncomingTransaction]], + *args: Any, + **kwargs: Any +) -> types.IncomingTransaction: ... + + +@overload +def sync( + func: Callable[ + [str], Coroutine[Any, Any, Dict[str, str]] + ], + *args: Any, + **kwargs: Any +) -> Dict[str, str]: ... + + +@overload +def sync( + func: Callable[ + [ + Union[int, float], str, Optional[str], + int, Optional[str] + ], Coroutine[Any, Any, bool] + ], + *args: Any, + **kwargs: Any +) -> bool: ... diff --git a/glQiwiApi/utils/exceptions.py b/glQiwiApi/utils/exceptions.py index ede691e0..0babbbbc 100644 --- a/glQiwiApi/utils/exceptions.py +++ b/glQiwiApi/utils/exceptions.py @@ -40,13 +40,6 @@ class InvalidData(Exception): """ -class VersionError(Exception): - """ - Ошибка возникает, если ваша версия python не поддерживается библиотекой - - """ - - class RequestError(Exception): """ Возникает при ошибках сервиса или неправильной передаче параметров @@ -79,12 +72,14 @@ def __repr__(self) -> str: def json(self) -> str: import json - if not isinstance(self.json_info, dict): - return self.__str__() - return json.dumps(self.__str__(), indent=2, ensure_ascii=False) + return json.dumps( + self.__str__() if not self.json_info else self.json_info, + indent=2, + ensure_ascii=False + ) -__all__ = [ +__all__ = ( 'InvalidData', 'NoUrlFound', 'RequestAuthError', @@ -93,5 +88,4 @@ def json(self) -> str: 'InvalidCardNumber', 'InvalidToken', 'RequestError', - 'VersionError' -] +) diff --git a/glQiwiApi/yoo_money/yoomoney_api.py b/glQiwiApi/yoo_money/yoomoney_api.py index 522bde10..fcb903f0 100644 --- a/glQiwiApi/yoo_money/yoomoney_api.py +++ b/glQiwiApi/yoo_money/yoomoney_api.py @@ -71,7 +71,8 @@ def _auth_token(self, headers: dict) -> Dict[Any, Any]: @classmethod async def build_url_for_auth( - cls, scope: List[str], + cls, + scope: List[str], client_id: str, redirect_uri: str = 'https://example.com' ) -> str: From c6eaf17f7bcc4e48521bef41b558d1d8ac8af904 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 08:17:23 +0300 Subject: [PATCH 03/14] change docs and add example --- docs/conf.py | 4 +--- docs/examples/index.rst | 4 +++- docs/examples/sync_support.rst | 10 ++++++++++ docs/examples/usage_with_aiogram.rst | 8 ++++++++ examples/sync_usage.py | 0 examples/usage_in_bots.py | 0 6 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 docs/examples/sync_support.rst create mode 100644 docs/examples/usage_with_aiogram.rst create mode 100644 examples/sync_usage.py create mode 100644 examples/usage_in_bots.py diff --git a/docs/conf.py b/docs/conf.py index af7fd383..74fdc1bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,9 +73,7 @@ # The full version, including alpha/beta/rc tags. release = glQiwiApi.__version__ -releaselevel = 'dev' if parsed_version.dev else 'alpha' \ - if 'a' in version else 'beta' \ - if 'b' in version else 'stable' +releaselevel = 'beta' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 2d9e49c0..b6fe117e 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -6,4 +6,6 @@ Qiwi usage examples p2p basic_methods usage_without_context - cache \ No newline at end of file + cache + sync_support + usage_with_aiogram \ No newline at end of file diff --git a/docs/examples/sync_support.rst b/docs/examples/sync_support.rst new file mode 100644 index 00000000..5ebcaef3 --- /dev/null +++ b/docs/examples/sync_support.rst @@ -0,0 +1,10 @@ +================= +Synchronous usage +================= + +*glQiwiApi* also coverage synchronous usage, you can use it like in the example + +.. literalinclude:: ./../../examples/sync_usage.py + :caption: sync_usage.py + :language: python + :linenos: diff --git a/docs/examples/usage_with_aiogram.rst b/docs/examples/usage_with_aiogram.rst new file mode 100644 index 00000000..e1a1b4eb --- /dev/null +++ b/docs/examples/usage_with_aiogram.rst @@ -0,0 +1,8 @@ +================== +Usage with aiogram +================== + + +.. literalinclude:: ./../../examples/usage_in_bots.py + :caption: usage_in_bots.py + :language: python \ No newline at end of file diff --git a/examples/sync_usage.py b/examples/sync_usage.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/usage_in_bots.py b/examples/usage_in_bots.py new file mode 100644 index 00000000..e69de29b From 789e50e0a36e5202138a64722ed2164762db72bb Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 08:18:16 +0300 Subject: [PATCH 04/14] change readme and add examples --- README.md | 14 ++++++----- examples/sync_usage.py | 26 +++++++++++++++++++++ examples/usage_in_bots.py | 49 +++++++++++++++++++++++++++++++++++++++ glQiwiApi/__init__.py | 2 +- glQiwiApi/mixins.py | 17 ++++++++++++-- 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 78914db0..51ba4373 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@

-

-[![PyPI version](https://img.shields.io/pypi/v/glQiwiApi.svg)](https://pypi.org/project/glQiwiApi/) [![Python](https://img.shields.io/badge/Python-3.7+-blue)](https://www.python.org/downloads/) [![Code Quality Score](https://www.code-inspector.com/project/20780/score/svg)](https://frontend.code-inspector.com/public/project/20780/glQiwiApi/dashboard) ![Code Grade](https://www.code-inspector.com/project/20780/status/svg) ![Downloads](https://img.shields.io/pypi/dm/glQiwiApi) ![docs](https://readthedocs.org/projects/pip/badge/?version=latest) -### :loudspeaker:New feature. Add YooMoney support and `pydantic` models to library! +[![PyPI version](https://img.shields.io/pypi/v/glQiwiApi.svg)](https://pypi.org/project/glQiwiApi/) [![Python](https://img.shields.io/badge/Python-3.7+-blue)](https://www.python.org/downloads/) [![Code Quality Score](https://www.code-inspector.com/project/20780/score/svg)](https://frontend.code-inspector.com/public/project/20780/glQiwiApi/dashboard) ![Code Grade](https://www.code-inspector.com/project/20780/status/svg) ![Downloads](https://img.shields.io/pypi/dm/glQiwiApi) ![docs](https://readthedocs.org/projects/pip/badge/?version=latest) +[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/GLEF1X/glQiwiApi.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/GLEF1X/glQiwiApi/context:python) + + -## Official api resources: - * __:fire:Docs: [here](https://glqiwiapi.readthedocs.io/en/master/index.html)__ +## :globe_with_meridians:Official api resources: + * :mortar_board:__Docs: [here](https://glqiwiapi.readthedocs.io/en/master/index.html)__ * 🖱️ __Developer contacts: [![Dev-Telegram](https://img.shields.io/badge/Telegram-blue.svg?style=flat-square&logo=telegram)](https://t.me/GLEF1X)__ +### :loudspeaker:New feature. Add YooMoney support and `pydantic` models to library! ### :floppy_disk:Installation ```bash @@ -387,4 +389,4 @@ async def main(): asyncio.run(main()) -``` +``` \ No newline at end of file diff --git a/examples/sync_usage.py b/examples/sync_usage.py index e69de29b..dcf18bba 100644 --- a/examples/sync_usage.py +++ b/examples/sync_usage.py @@ -0,0 +1,26 @@ +import datetime + +from glQiwiApi import QiwiWrapper, sync + +TOKEN = 'Api token from https://qiwi.com/api' +WALLET = '+phone_number' + +# As always, we create an instance of the class, +# but pass on "without_context" as True +wallet = QiwiWrapper( + api_access_token=TOKEN, + phone_number=WALLET, + without_context=True # add this param to use sync connector soon +) + + +def sync_function() -> None: + start_date = datetime.datetime.now() - datetime.timedelta(days=5) + # Use the sync () function and pass the function we want to execute + # without calling it, that is, pass it as a regular variable + result = sync(wallet.transactions, rows_num=50, start_date=start_date, + end_date=datetime.datetime.now()) + print(result) + + +sync_function() diff --git a/examples/usage_in_bots.py b/examples/usage_in_bots.py index e69de29b..61145300 100644 --- a/examples/usage_in_bots.py +++ b/examples/usage_in_bots.py @@ -0,0 +1,49 @@ +from typing import Union + +from aiogram import Bot, Dispatcher, types +from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram.dispatcher import FSMContext +from aiogram.utils import executor + +from glQiwiApi import QiwiWrapper, types as qiwi_types + +wallet = QiwiWrapper( + secret_p2p='secret_p2p' +) + +BOT_TOKEN = 'TELEGRAM_BOT_TOKEN' + +bot = Bot(token=BOT_TOKEN, parse_mode=types.ParseMode.HTML) +storage = MemoryStorage() +dp = Dispatcher(bot, storage=storage) + + +async def create_payment(amount: Union[float, int] = 1) -> qiwi_types.Bill: + async with wallet as w: + return await w.create_p2p_bill(amount) + + +@dp.message_handler(text='Хочу оплатить') +async def payment(message: types.Message, state: FSMContext): + bill = await create_payment() + await message.answer( + text=f'Хорошо, вот ваш счёт для оплаты:\n {bill.pay_url}' + ) + await state.set_state('payment') + await state.update_data(bill=bill) + + +@dp.message_handler(state='payment', text='Оплатил') +async def successful_payment(message: types.Message, state: FSMContext): + async with state.proxy() as data: + bill: qiwi_types.Bill = data.get('bill') + status = await bill.check() + if status: + await message.answer('Вы успешно оплатили счет') + await state.finish() + else: + await message.answer('Счет не оплачен') + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) diff --git a/glQiwiApi/__init__.py b/glQiwiApi/__init__.py index 708b8e43..927aa33c 100644 --- a/glQiwiApi/__init__.py +++ b/glQiwiApi/__init__.py @@ -29,4 +29,4 @@ class VersionError(Exception): "Минимальная версия для использования python 3.7" ) -__version__ = '0.2.12' +__version__ = '0.2.13' diff --git a/glQiwiApi/mixins.py b/glQiwiApi/mixins.py index 3aa53d00..254db5e4 100644 --- a/glQiwiApi/mixins.py +++ b/glQiwiApi/mixins.py @@ -25,6 +25,7 @@ async def check(self) -> bool: class ToolsMixin(object): _parser = None + slots_ = set() async def __aenter__(self): """Создаем сессию, чтобы не пересоздавать её много раз""" @@ -37,12 +38,24 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): await self._parser.session.close() self._parser.clear_cache() + def get_(self, item: Any) -> Any: + try: + + obj = super().__getattribute__(item) + if obj is not None: + self.slots_.add(item) + return obj + except AttributeError: + return None + def __deepcopy__(self, memo) -> 'ToolsMixin': cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result - values = [getattr(self, slot) for slot in self.__slots__] - items = it.zip_longest(self.__slots__, values) + self.slots_.clear() + values = [self.get_(slot) for slot in self.__slots__ if + self.get_(slot) is not None] + items = it.zip_longest(self.slots_, values) for k, v in items: if k == '_parser': v.session = None From 1f92c82cfffb614c3ca994c8d02307efaa66147c Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 08:50:56 +0300 Subject: [PATCH 05/14] bug fix --- examples/usage_in_bots.py | 14 +++++++------- glQiwiApi/mixins.py | 16 ++++------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/examples/usage_in_bots.py b/examples/usage_in_bots.py index 61145300..5dbae1a7 100644 --- a/examples/usage_in_bots.py +++ b/examples/usage_in_bots.py @@ -5,27 +5,27 @@ from aiogram.dispatcher import FSMContext from aiogram.utils import executor -from glQiwiApi import QiwiWrapper, types as qiwi_types +from glQiwiApi import QiwiWrapper, types as qiwi_types, sync wallet = QiwiWrapper( - secret_p2p='secret_p2p' + secret_p2p='YOUR_SECRET_P2P_TOKEN', + without_context=True ) -BOT_TOKEN = 'TELEGRAM_BOT_TOKEN' +BOT_TOKEN = 'BOT_TOKEN' bot = Bot(token=BOT_TOKEN, parse_mode=types.ParseMode.HTML) storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) -async def create_payment(amount: Union[float, int] = 1) -> qiwi_types.Bill: - async with wallet as w: - return await w.create_p2p_bill(amount) +def create_payment(amount: Union[float, int] = 1) -> qiwi_types.Bill: + return sync(wallet.create_p2p_bill, amount) @dp.message_handler(text='Хочу оплатить') async def payment(message: types.Message, state: FSMContext): - bill = await create_payment() + bill = create_payment() await message.answer( text=f'Хорошо, вот ваш счёт для оплаты:\n {bill.pay_url}' ) diff --git a/glQiwiApi/mixins.py b/glQiwiApi/mixins.py index 254db5e4..86a53814 100644 --- a/glQiwiApi/mixins.py +++ b/glQiwiApi/mixins.py @@ -1,5 +1,4 @@ import copy -import itertools as it from typing import Optional, Any @@ -25,7 +24,6 @@ async def check(self) -> bool: class ToolsMixin(object): _parser = None - slots_ = set() async def __aenter__(self): """Создаем сессию, чтобы не пересоздавать её много раз""" @@ -40,11 +38,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): def get_(self, item: Any) -> Any: try: - - obj = super().__getattribute__(item) - if obj is not None: - self.slots_.add(item) - return obj + return super().__getattribute__(item) except AttributeError: return None @@ -52,11 +46,9 @@ def __deepcopy__(self, memo) -> 'ToolsMixin': cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result - self.slots_.clear() - values = [self.get_(slot) for slot in self.__slots__ if - self.get_(slot) is not None] - items = it.zip_longest(self.slots_, values) - for k, v in items: + dct = {slot: self.get_(slot) for slot in self.__slots__ if + self.get_(slot) is not None} + for k, v in dct.items(): if k == '_parser': v.session = None setattr(result, k, copy.deepcopy(v, memo)) From da05eeb1959548486a5e3d2a5bb95082f5f0dd81 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 14:19:23 +0300 Subject: [PATCH 06/14] add QIWI Maps coverage --- README.md | 27 ++-- examples/usage_in_bots.py | 9 +- glQiwiApi/__init__.py | 3 +- glQiwiApi/core/__init__.py | 21 ++++ glQiwiApi/{ => core}/abstracts.py | 4 +- glQiwiApi/{ => core}/aiohttp_custom_api.py | 23 +++- glQiwiApi/{ => core}/basic_requests_api.py | 3 +- glQiwiApi/{ => core}/mixins.py | 6 +- glQiwiApi/qiwi/__init__.py | 3 +- glQiwiApi/qiwi/qiwi_api.py | 30 ++--- glQiwiApi/qiwi/qiwi_maps.py | 86 +++++++++++++ glQiwiApi/types/__init__.py | 19 +-- glQiwiApi/types/particular.py | 2 +- glQiwiApi/types/qiwi_types/__init__.py | 25 +++- glQiwiApi/types/qiwi_types/bill.py | 2 +- glQiwiApi/types/qiwi_types/partner.py | 14 +++ glQiwiApi/types/qiwi_types/polygon.py | 18 +++ glQiwiApi/types/qiwi_types/terminal.py | 32 +++++ glQiwiApi/types/yoomoney_types/types.py | 139 ++++++++++++++++----- glQiwiApi/utils/basics.py | 46 ++----- glQiwiApi/utils/basics.pyi | 12 +- glQiwiApi/yoo_money/yoomoney_api.py | 115 +++++++++-------- setup.py | 4 +- 23 files changed, 453 insertions(+), 190 deletions(-) create mode 100644 glQiwiApi/core/__init__.py rename glQiwiApi/{ => core}/abstracts.py (97%) rename glQiwiApi/{ => core}/aiohttp_custom_api.py (81%) rename glQiwiApi/{ => core}/basic_requests_api.py (99%) rename glQiwiApi/{ => core}/mixins.py (91%) create mode 100644 glQiwiApi/qiwi/qiwi_maps.py create mode 100644 glQiwiApi/types/qiwi_types/partner.py create mode 100644 glQiwiApi/types/qiwi_types/polygon.py create mode 100644 glQiwiApi/types/qiwi_types/terminal.py diff --git a/README.md b/README.md index 51ba4373..413e3375 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,16 @@ [![PyPI version](https://img.shields.io/pypi/v/glQiwiApi.svg)](https://pypi.org/project/glQiwiApi/) [![Python](https://img.shields.io/badge/Python-3.7+-blue)](https://www.python.org/downloads/) [![Code Quality Score](https://www.code-inspector.com/project/20780/score/svg)](https://frontend.code-inspector.com/public/project/20780/glQiwiApi/dashboard) ![Code Grade](https://www.code-inspector.com/project/20780/status/svg) ![Downloads](https://img.shields.io/pypi/dm/glQiwiApi) ![docs](https://readthedocs.org/projects/pip/badge/?version=latest) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/GLEF1X/glQiwiApi.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/GLEF1X/glQiwiApi/context:python) + + ## :globe_with_meridians:Official api resources: - * :mortar_board:__Docs: [here](https://glqiwiapi.readthedocs.io/en/master/index.html)__ + * 🎓__Docs: [here](https://glqiwiapi.readthedocs.io/en/master/index.html)__ * 🖱️ __Developer contacts: [![Dev-Telegram](https://img.shields.io/badge/Telegram-blue.svg?style=flat-square&logo=telegram)](https://t.me/GLEF1X)__ -### :loudspeaker:New feature. Add YooMoney support and `pydantic` models to library! -### :floppy_disk:Installation +### 📣New feature. Add YooMoney support and `pydantic` models to library! +### 💾Installation ```bash pip install glQiwiApi @@ -20,7 +22,7 @@ pip install glQiwiApi --- -## :bulb:Dependencies +## 🐦Dependencies | Library | Description | |:-------:|:----------------------------------------------:| @@ -32,7 +34,7 @@ pip install glQiwiApi --- -## :pencil2:Dive-in Examples +## 🧸Dive-in Examples ```python import asyncio @@ -75,7 +77,7 @@ asyncio.run(main()) --- -## :scroll:Checking transactions +## 🌀Checking transactions ```python import asyncio @@ -99,7 +101,7 @@ async def main(): asyncio.run(main()) ``` -## :city_sunset:Create & check p2p bills +## 🌱Create & check p2p bills ```python import asyncio @@ -135,7 +137,7 @@ asyncio.run(main()) ![form](https://i.ibb.co/T0C5RYz/2021-03-21-14-58-33.png) -## :rocket:Send to another wallet & get receipt(получение чека транзакции) +## ⛏Send to another wallet & get receipt(получение чека транзакции) ```python import asyncio @@ -165,7 +167,7 @@ asyncio.run(main()) ``` -## Send to card & check commission +## 💳Send to card & check commission ```python import asyncio @@ -239,7 +241,7 @@ asyncio.run(cache_test()) ``` -## :warning:Handling exceptions +## ⚠️Handling exceptions ```python import asyncio @@ -259,6 +261,11 @@ async def main(): asyncio.run(main()) ``` +--- +## 🗺QIWI terminals +__glQiwiApi covers qiwi's MAPS api in QiwiMaps class__ + +--- # YooMoney API --- diff --git a/examples/usage_in_bots.py b/examples/usage_in_bots.py index 5dbae1a7..63b064c9 100644 --- a/examples/usage_in_bots.py +++ b/examples/usage_in_bots.py @@ -5,7 +5,7 @@ from aiogram.dispatcher import FSMContext from aiogram.utils import executor -from glQiwiApi import QiwiWrapper, types as qiwi_types, sync +from glQiwiApi import QiwiWrapper, types as qiwi_types wallet = QiwiWrapper( secret_p2p='YOUR_SECRET_P2P_TOKEN', @@ -19,13 +19,14 @@ dp = Dispatcher(bot, storage=storage) -def create_payment(amount: Union[float, int] = 1) -> qiwi_types.Bill: - return sync(wallet.create_p2p_bill, amount) +async def create_payment(amount: Union[float, int] = 1) -> qiwi_types.Bill: + async with wallet as w: + return await w.create_p2p_bill(amount=amount) @dp.message_handler(text='Хочу оплатить') async def payment(message: types.Message, state: FSMContext): - bill = create_payment() + bill = await create_payment() await message.answer( text=f'Хорошо, вот ваш счёт для оплаты:\n {bill.pay_url}' ) diff --git a/glQiwiApi/__init__.py b/glQiwiApi/__init__.py index 927aa33c..5f684d9f 100644 --- a/glQiwiApi/__init__.py +++ b/glQiwiApi/__init__.py @@ -1,6 +1,6 @@ import sys -from .qiwi import QiwiWrapper # NOQA +from .qiwi import QiwiWrapper, QiwiMaps # NOQA from .utils.basics import sync, to_datetime # NOQA from .utils.exceptions import * # NOQA from .yoo_money import YooMoneyAPI # NOQA @@ -9,6 +9,7 @@ ( 'QiwiWrapper', 'YooMoneyAPI', + 'QiwiMaps', 'RequestError', 'to_datetime', 'sync' diff --git a/glQiwiApi/core/__init__.py b/glQiwiApi/core/__init__.py new file mode 100644 index 00000000..0c8bfd87 --- /dev/null +++ b/glQiwiApi/core/__init__.py @@ -0,0 +1,21 @@ +from .abstracts import ( + AbstractParser, + AbstractCacheController, + AbstractPaymentWrapper, + AioTestCase +) +from .aiohttp_custom_api import CustomParser +from .basic_requests_api import HttpXParser, SimpleCache +from .mixins import BillMixin, ToolsMixin + +__all__ = ( + 'HttpXParser', + 'SimpleCache', + 'AbstractParser', + 'AbstractPaymentWrapper', + 'AbstractCacheController', + 'AioTestCase', + 'BillMixin', + 'ToolsMixin', + 'CustomParser' +) diff --git a/glQiwiApi/abstracts.py b/glQiwiApi/core/abstracts.py similarity index 97% rename from glQiwiApi/abstracts.py rename to glQiwiApi/core/abstracts.py index 509071d7..1aa62fd3 100644 --- a/glQiwiApi/abstracts.py +++ b/glQiwiApi/core/abstracts.py @@ -95,7 +95,6 @@ async def _request( set_timeout: bool, cookies: Optional[LooseCookies], json: Optional[dict], - skip_exceptions: bool, proxy: Optional[ProxyService], data: Optional[Dict[str, Union[ str, int, List[ @@ -122,7 +121,8 @@ async def fetch( def raise_exception( self, status_code: Union[str, int], - json_info: Optional[Dict[str, Any]] = None + json_info: Optional[Dict[str, Any]] = None, + message: Optional[str] = None ) -> None: """Метод для обработки исключений и лучшего логирования""" diff --git a/glQiwiApi/aiohttp_custom_api.py b/glQiwiApi/core/aiohttp_custom_api.py similarity index 81% rename from glQiwiApi/aiohttp_custom_api.py rename to glQiwiApi/core/aiohttp_custom_api.py index a6c82d70..c2072627 100644 --- a/glQiwiApi/aiohttp_custom_api.py +++ b/glQiwiApi/core/aiohttp_custom_api.py @@ -3,7 +3,7 @@ import aiohttp import glQiwiApi -from glQiwiApi.basic_requests_api import HttpXParser, SimpleCache +from glQiwiApi.core.basic_requests_api import SimpleCache, HttpXParser from glQiwiApi.types import Response from glQiwiApi.types.basics import CachedResponse from glQiwiApi.utils.exceptions import RequestError @@ -19,9 +19,9 @@ class CustomParser(HttpXParser): def __init__( self, - without_context: bool, - messages: Dict[str, str], - cache_time: Union[float, int] + without_context: bool = False, + messages: Optional[Dict[str, str]] = None, + cache_time: Union[float, int] = 0 ) -> None: super(CustomParser, self).__init__() self._without_context = without_context @@ -72,9 +72,11 @@ async def _request(self, *args, **kwargs) -> Response: def raise_exception( self, status_code: Union[str, int], - json_info: Optional[Dict[str, Any]] = None + json_info: Optional[Dict[str, Any]] = None, + message: Optional[str] = None ) -> None: - message = self.messages.get(str(status_code), "Unknown") + if not isinstance(message, str): + message = self.messages.get(str(status_code), "Unknown") raise RequestError( message, status_code, @@ -100,3 +102,12 @@ def check_session(session: Any) -> bool: def create_session(self, **kwargs) -> None: self.set_cached_session() super().create_session(**kwargs) + + @classmethod + def filter_dict(cls, dictionary: dict): + """ + Pop NoneType values and convert everything to str, designed?for=params + :param dictionary: source dict + :return: filtered dict + """ + return {k: str(v) for k, v in dictionary.items() if v is not None} diff --git a/glQiwiApi/basic_requests_api.py b/glQiwiApi/core/basic_requests_api.py similarity index 99% rename from glQiwiApi/basic_requests_api.py rename to glQiwiApi/core/basic_requests_api.py index 428af3b5..d829bc98 100644 --- a/glQiwiApi/basic_requests_api.py +++ b/glQiwiApi/core/basic_requests_api.py @@ -1,4 +1,3 @@ -import abc import asyncio import time from itertools import repeat @@ -21,7 +20,7 @@ from aiosocksy import SocksError from aiosocksy.connector import ProxyConnector, ProxyClientRequest -from glQiwiApi.abstracts import AbstractParser, AbstractCacheController +from glQiwiApi.core import AbstractParser, AbstractCacheController from glQiwiApi.types import ProxyService, Response from glQiwiApi.types.basics import CachedResponse, Attributes from glQiwiApi.utils.exceptions import InvalidData diff --git a/glQiwiApi/mixins.py b/glQiwiApi/core/mixins.py similarity index 91% rename from glQiwiApi/mixins.py rename to glQiwiApi/core/mixins.py index 86a53814..5f1f8c23 100644 --- a/glQiwiApi/mixins.py +++ b/glQiwiApi/core/mixins.py @@ -36,7 +36,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): await self._parser.session.close() self._parser.clear_cache() - def get_(self, item: Any) -> Any: + def get(self, item: Any) -> Any: try: return super().__getattribute__(item) except AttributeError: @@ -46,8 +46,8 @@ def __deepcopy__(self, memo) -> 'ToolsMixin': cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result - dct = {slot: self.get_(slot) for slot in self.__slots__ if - self.get_(slot) is not None} + dct = {slot: self.get(slot) for slot in self.__slots__ if + self.get(slot) is not None} for k, v in dct.items(): if k == '_parser': v.session = None diff --git a/glQiwiApi/qiwi/__init__.py b/glQiwiApi/qiwi/__init__.py index 24054f66..13c7ffaa 100644 --- a/glQiwiApi/qiwi/__init__.py +++ b/glQiwiApi/qiwi/__init__.py @@ -1,3 +1,4 @@ from .qiwi_api import QiwiWrapper +from .qiwi_maps import QiwiMaps -__all__ = ('QiwiWrapper',) +__all__ = ('QiwiWrapper', 'QiwiMaps') diff --git a/glQiwiApi/qiwi/qiwi_api.py b/glQiwiApi/qiwi/qiwi_api.py index 1b25569d..a95783c2 100644 --- a/glQiwiApi/qiwi/qiwi_api.py +++ b/glQiwiApi/qiwi/qiwi_api.py @@ -7,9 +7,10 @@ import aiohttp import glQiwiApi.utils.basics as api_helper -from glQiwiApi.abstracts import AbstractPaymentWrapper -from glQiwiApi.aiohttp_custom_api import CustomParser -from glQiwiApi.mixins import ToolsMixin +from glQiwiApi.core import ( + AbstractPaymentWrapper +) +from glQiwiApi.core import CustomParser, ToolsMixin from glQiwiApi.qiwi.basic_qiwi_config import ( BASE_QIWI_URL, ERROR_CODE_NUMBERS, QIWI_TO_CARD, DEFAULT_QIWI_HEADERS, @@ -371,7 +372,7 @@ async def check_transaction( self, amount: Union[int, float], transaction_type: str = 'IN', - sender_number: Optional[str] = None, + sender: Optional[str] = None, rows_num: int = 50, comment: Optional[str] = None ) -> bool: @@ -389,7 +390,7 @@ async def check_transaction( :param amount: сумма платежа :param transaction_type: тип платежа - :param sender_number: номер получателя + :param sender: номер получателя :param rows_num: кол-во платежей, которое будет проверяться :param comment: комментарий, по которому будет проверяться транзакция :return: bool, есть ли такая транзакция в истории платежей @@ -401,19 +402,18 @@ async def check_transaction( raise InvalidData('Можно проверять не более 50 транзакций') transactions = await self.transactions(rows_num=rows_num) - for transaction in transactions: - if float(transaction.sum.amount) >= amount: - if transaction.type == transaction_type: - if transaction.comment == comment: - if transaction.to_account == sender_number: - return True - elif comment and sender_number: + for txn in transactions: + if float(txn.sum.amount) >= amount: + if txn.type == transaction_type: + if txn.comment == comment and txn.to_account == sender: + return True + elif comment and sender: continue - elif transaction.to_account == sender_number: + elif txn.to_account == sender: return True - elif sender_number: + elif sender: continue - elif transaction.comment == comment: + elif txn.comment == comment: return True return False diff --git a/glQiwiApi/qiwi/qiwi_maps.py b/glQiwiApi/qiwi/qiwi_maps.py new file mode 100644 index 00000000..170fae55 --- /dev/null +++ b/glQiwiApi/qiwi/qiwi_maps.py @@ -0,0 +1,86 @@ +import typing + +import glQiwiApi.utils.basics as api_helper +from glQiwiApi import types +from glQiwiApi.core import CustomParser + + +class QiwiMaps: + """ + API Карты терминалов QIWI позволяет установить местонахождение + терминалов QIWI на территории РФ + + """ + + def __init__(self) -> None: + self.parser = CustomParser() + + async def terminals( + self, + polygon: types.Polygon, + zoom: int = None, + pop_if_inactive_x_mins: int = 30, + include_partners: bool = None, + partners_ids: list = None, + cache_terminals: bool = None, + card_terminals: bool = None, + identification_types: int = None, + terminal_groups: list = None, + ) -> typing.List[types.Terminal]: + """ + Get map of terminals sent for passed polygon with additional params + + :param polygon: glQiwiApi.types.Polygon object + :param zoom: + https://tech.yandex.ru/maps/doc/staticapi/1.x/dg/concepts/map_scale-docpage/ + :param pop_if_inactive_x_mins: do not show if terminal + was inactive for X minutes default 0.5 hours + :param include_partners: result will include/exclude partner terminals + :param partners_ids: Not fixed IDS array look at docs + :param cache_terminals: result will include or exclude + cache-acceptable terminals + :param card_terminals: result will include or + exclude card-acceptable terminals + :param identification_types: `0` - not identified, `1` - + partly identified, `2` - fully identified + :param terminal_groups: look at QiwiMaps.partners + :return: list of Terminal instances + """ + params = self.parser.filter_dict( + { + **polygon.dict, + "zoom": zoom, + "activeWithinMinutes": pop_if_inactive_x_mins, + "withRefillWallet": include_partners, + "ttpIds": partners_ids, + "cacheAllowed": cache_terminals, + "cardAllowed": card_terminals, + "identificationTypes": identification_types, + "ttpGroups": terminal_groups, + } + ) + url = "http://edge.qiwi.com/locator/v3/nearest/clusters?parameters" + async for response in self.parser.fast().fetch( + url=url, + method='GET', + params=params + ): + return api_helper.multiply_objects_parse( + lst_of_objects=response.response_data, + model=types.Terminal + ) + + async def partners(self) -> typing.List[types.Partner]: + """ + Get terminal partners for ttpGroups + :return: list of TTPGroups + """ + async for response in self.parser.fast().fetch( + url='http://edge.qiwi.com/locator/v3/ttp-groups', + method='GET', + headers={"Content-type": "text/json"} + ): + return api_helper.multiply_objects_parse( + lst_of_objects=response.response_data, + model=types.Partner + ) diff --git a/glQiwiApi/types/__init__.py b/glQiwiApi/types/__init__.py index 4aa9df21..9ac2ff1b 100644 --- a/glQiwiApi/types/__init__.py +++ b/glQiwiApi/types/__init__.py @@ -25,7 +25,10 @@ Transaction, PaymentInfo, OrderDetails, - RefundBill + RefundBill, + Polygon, + Terminal, + Partner ) from .yoomoney_types import ( OperationType, @@ -37,14 +40,12 @@ AccountInfo ) -BasicTypes = Union[ - AccountInfo, Operation, OperationDetails, - PreProcessPaymentResponse, Payment, IncomingTransaction -] - PydanticTypes = Union[ Transaction, QiwiAccountInfo, Account, Limit, - Bill, BillError, Statistic, Balance, Identification + Bill, BillError, Statistic, Balance, Identification, + AccountInfo, Operation, OperationDetails, + PreProcessPaymentResponse, Payment, IncomingTransaction, + Terminal, Partner ] E_ = TypeVar( @@ -81,11 +82,13 @@ 'Type', 'OptionalSum', 'Commission', - 'BasicTypes', 'PydanticTypes', 'PaymentInfo', 'OrderDetails', 'RefundBill', + 'Polygon', + 'Terminal', + 'Partner', 'E_', 'FuncT' ] diff --git a/glQiwiApi/types/particular.py b/glQiwiApi/types/particular.py index c84362fe..665af104 100644 --- a/glQiwiApi/types/particular.py +++ b/glQiwiApi/types/particular.py @@ -7,7 +7,7 @@ from glQiwiApi.utils.exceptions import ProxyError -response_type = Union[dict, str, bytes, bytearray, Exception] +response_type = Union[dict, str, bytes, bytearray, Exception, list] @dataclass diff --git a/glQiwiApi/types/qiwi_types/__init__.py b/glQiwiApi/types/qiwi_types/__init__.py index 21c7e46a..accaa91a 100644 --- a/glQiwiApi/types/qiwi_types/__init__.py +++ b/glQiwiApi/types/qiwi_types/__init__.py @@ -4,13 +4,28 @@ from .bill import Bill, BillError, RefundBill from .identification import Identification from .limit import Limit +from .partner import Partner from .payment_info import PaymentInfo +from .polygon import Polygon from .qiwi_master import OrderDetails from .stats import Statistic +from .terminal import Terminal from .transaction import Transaction -__all__ = [ - 'QiwiAccountInfo', 'Transaction', 'Bill', 'BillError', - 'Statistic', 'Limit', 'Account', 'Balance', 'Identification', - 'PaymentInfo', 'OrderDetails', 'RefundBill' -] +__all__ = ( + 'QiwiAccountInfo', + 'Transaction', + 'Bill', + 'BillError', + 'Statistic', + 'Limit', + 'Account', + 'Balance', + 'Identification', + 'PaymentInfo', + 'OrderDetails', + 'RefundBill', + 'Polygon', + 'Terminal', + 'Partner' +) diff --git a/glQiwiApi/types/qiwi_types/bill.py b/glQiwiApi/types/qiwi_types/bill.py index 8eadd06d..4dee2a68 100644 --- a/glQiwiApi/types/qiwi_types/bill.py +++ b/glQiwiApi/types/qiwi_types/bill.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field -from glQiwiApi.mixins import BillMixin +from glQiwiApi.core.mixins import BillMixin from glQiwiApi.types.basics import OptionalSum from glQiwiApi.utils.basics import custom_load diff --git a/glQiwiApi/types/qiwi_types/partner.py b/glQiwiApi/types/qiwi_types/partner.py new file mode 100644 index 00000000..887f2999 --- /dev/null +++ b/glQiwiApi/types/qiwi_types/partner.py @@ -0,0 +1,14 @@ +"""Main model: Partner""" +from typing import List, Optional + +from pydantic import BaseModel + + +class Partner(BaseModel): + title: str + id: int + + maps: Optional[List[str]] = None + + +__all__ = ("Partner",) diff --git a/glQiwiApi/types/qiwi_types/polygon.py b/glQiwiApi/types/qiwi_types/polygon.py new file mode 100644 index 00000000..9af2712e --- /dev/null +++ b/glQiwiApi/types/qiwi_types/polygon.py @@ -0,0 +1,18 @@ +class Polygon: + def __init__(self, lat_lon_pair_nw: tuple, lat_lon_pair_se: tuple): + self.lat_nw, self.lon_nw = lat_lon_pair_nw + self.lat_se, self.lon_se = lat_lon_pair_se + + self._dict = { + "latNW": self.lat_nw, + "lngNW": self.lon_nw, + "latSE": self.lat_se, + "lngSE": self.lon_se, + } + + @property + def dict(self): + return {k: str(double) for k, double in self._dict.items()} + + +__all__ = ("Polygon",) diff --git a/glQiwiApi/types/qiwi_types/terminal.py b/glQiwiApi/types/qiwi_types/terminal.py new file mode 100644 index 00000000..6549057d --- /dev/null +++ b/glQiwiApi/types/qiwi_types/terminal.py @@ -0,0 +1,32 @@ +"""Main model: Terminal""" +from typing import Optional + +from pydantic import Field, BaseModel + + +class Coordinate(BaseModel): + """Object: coordinate""" + + latitude: float = Field(..., alias="latitude") + longitude: float = Field(..., alias="longitude") + precision: int = Field(..., alias="precision") + + +class Terminal(BaseModel): + """Object: Terminal""" + + terminal_id: int = Field(..., alias="terminalId") + ttp_id: int = Field(..., alias="ttpId") + last_active: str = Field(..., alias="lastActive") + count: int = Field(..., alias="count") + address: str = Field(..., alias="address") + verified: bool = Field(..., alias="verified") + label: str = Field(..., alias="label") + description: Optional[str] = Field(type(None), alias="description") + cash_allowed: bool = Field(..., alias="cashAllowed") + card_allowed: bool = Field(..., alias="cardAllowed") + identification_type: int = Field(..., alias="identificationType") + coordinate: Coordinate = Field(..., alias="coordinate") + + +__all__ = ("Terminal",) diff --git a/glQiwiApi/types/yoomoney_types/types.py b/glQiwiApi/types/yoomoney_types/types.py index b61e2206..3ba27702 100644 --- a/glQiwiApi/types/yoomoney_types/types.py +++ b/glQiwiApi/types/yoomoney_types/types.py @@ -1,10 +1,13 @@ -from dataclasses import dataclass +from datetime import datetime from enum import Enum from typing import Optional, Union, Dict, List, Any +from pydantic import BaseModel, Field, Extra -@dataclass -class BalanceDetails: +from glQiwiApi.utils.basics import custom_load + + +class BalanceDetails(BaseModel): total: float available: float deposition_pending: Optional[float] = None @@ -13,8 +16,12 @@ class BalanceDetails: hold: Optional[float] = None -@dataclass -class AccountInfo: +class CardsLinked(BaseModel): + pan_fragment: str + type: str + + +class AccountInfo(BaseModel): account: str """Номер счета""" @@ -53,11 +60,11 @@ class AccountInfo: задолженности, блокировки средств. """ - cards_linked: Optional[List[Dict[str, str]]] = None + cards_linked: Optional[List[CardsLinked]] = None """ Информация о привязанных банковских картах. Если к счету не привязано ни одной карты, параметр отсутствует. - Если к счету привязана хотя бы одна карта, + Если к счету привязана хотя бы одна карта, параметр содержит список данных о привязанных картах. pan_fragment - string Маскированный номер карты. type: string @@ -68,6 +75,9 @@ class AccountInfo: - JCB. """ + class Config: + json_loads = custom_load + class OperationType(Enum): """ @@ -85,12 +95,23 @@ class OperationType(Enum): PAYMENT = 'payment' """Платежи со счета (расход)""" - INCOMING = 'incoming-transfers-unaccepted' + TRANSFERS = 'incoming-transfers-unaccepted' """непринятые входящие P2P-переводы любого типа.""" -@dataclass(frozen=True) -class Operation: +class DigitalGoods(BaseModel): + """ + Данные о цифровом товаре (пин-коды и бонусы игр, iTunes, Xbox, etc.) + Поле присутствует при успешном платеже в магазины цифровых товаров. + Описание формата: + https://yoomoney.ru/docs/wallet/process-payments/process-payment#digital-goods + """ + article_id: str = Field(..., alias="merchantArticleId") + serial: str + secret: str + + +class Operation(BaseModel): operation_id: str """Идентификатор операции.""" @@ -103,7 +124,7 @@ class Operation: перевод не принят получателем или ожидает ввода кода протекции. """ - operation_date: str + operation_date: datetime = Field(..., alias="datetime") """ Дата и время совершения операции в формате строки в ISO формате с часовым поясом UTC. @@ -124,7 +145,7 @@ class Operation: amount: Union[int, float] """Сумма операции.""" - operation_type: str + operation_type: str = Field(..., alias="type") """Тип операции. Возможные значения: payment-shop — исходящий платеж в магазин; outgoing-transfer — исходящий P2P-перевод любого типа; @@ -135,7 +156,7 @@ class Operation: label: Optional[str] = None """Метка платежа. - Присутствует для входящих и исходящих переводов другим пользователям ЮMoney + Присутствует для входящих и исходящих переводов другим пользователям у которых был указан параметр label вызова send() """ @@ -147,9 +168,11 @@ class Operation: details: Optional[Any] = None + class Config: + json_loads = custom_load + -@dataclass(frozen=True) -class OperationDetails: +class OperationDetails(BaseModel): operation_id: Optional[str] = None """Идентификатор операции. Можно получить при вызове метода history()""" @@ -159,13 +182,13 @@ class OperationDetails: amount: Optional[float] = None """Сумма операции (сумма списания со счета).""" - operation_date: Optional[str] = None + operation_date: Optional[str] = Field(..., alias="datetime") """ Дата и время совершения операции в формате строки в ISO формате с часовым поясом UTC. """ - operation_type: Optional[str] = None + operation_type: Optional[str] = Field(..., alias="type") """Тип операции. Возможные значения: payment-shop — исходящий платеж в магазин; outgoing-transfer — исходящий P2P-перевод любого типа; @@ -187,7 +210,7 @@ class OperationDetails: Присутствует в истории отправителя перевода или получателя пополнения. """ - digital_goods: Optional[Dict[str, Dict[str, List[Dict[str, str]]]]] = None + digital_goods: Optional[Dict[str, DigitalGoods]] = None """ Данные о цифровом товаре (пин-коды и бонусы игр, iTunes, Xbox, etc.) Поле присутствует при успешном платеже в магазины цифровых товаров. @@ -197,8 +220,8 @@ class OperationDetails: details: Optional[str] = None """ - Детальное описание платежа. - Строка произвольного формата, может содержать любые символы и переводы строк + Детальное описание платежа. Строка произвольного формата, + может содержать любые символы и переводы строк Необязательный параметр. """ @@ -291,9 +314,55 @@ class OperationDetails: Все прочие значения - техническая ошибка, повторите вызов метода позднее. """ + class Config: + json_loads = custom_load -@dataclass -class PreProcessPaymentResponse: + +class Wallet(BaseModel): + allowed: bool + + +class Item(BaseModel): + item_id: str = Field(..., alias="id") + """ + Идентификатор привязанной к счету банковской карты. + Его необходимо указать в методе process-payment для + совершения платежа выбранной картой. + """ + pan_fragment: str + """ + Фрагмент номера банковской карты. + Поле присутствует только для привязанной банковской карты. + Может отсутствовать, если неизвестен. + """ + item_type: str = Field(..., alias="type") + """ + Тип карты. Может отсутствовать, если неизвестен. Возможные значения: + Visa; + MasterCard; + American Express; + JCB. + """ + + +class Card(BaseModel): + allowed: bool + csc_required: bool + items: List[Item] + + +class MoneySource(BaseModel): + """ + Список доступных методов для проведения данного платежа. + Каждый метод содержит набор атрибутов. + """ + wallet: Wallet + """платеж со счета пользователя""" + cards: Optional[Card] = None + """платеж с банковских карт, привязанных к счету""" + + +class PreProcessPaymentResponse(BaseModel): """ Объект, который вы получаете при вызове _pre_process_payment. При вызове данного метода вы не списываете деньги со своего счёта, @@ -311,23 +380,25 @@ class PreProcessPaymentResponse: multiple_recipients_found: Optional[str] = None contract_amount: Optional[float] = None error: Optional[str] = None - money_source: Optional[Dict[str, Dict[str, Union[str, bool, list]]]] = None + money_source: Optional[MoneySource] = None protection_code: Optional[str] = None account_unblock_uri: Optional[str] = None ext_action_uri: Optional[str] = None + class Config: + json_loads = custom_load + -@dataclass -class Payment: +class Payment(BaseModel): status: str """ Код результата выполнения операции. Возможные значения: success — успешное выполнение (платеж проведен). Это конечное состояние платежа. - refused — отказ в проведении платежа. + refused — отказ в проведении платежа. Причина отказа возвращается в поле error. Это конечное состояние платежа. in_progress — авторизация платежа не завершена. - Приложению следует повторить запрос + Приложению следует повторить запрос с теми же параметрами спустя некоторое время. ext_auth_required — для завершения авторизации платежа с использованием банковской карты @@ -388,7 +459,7 @@ class Payment: """ account_unblock_uri: Optional[str] = None """ - Адрес, на который необходимо отправить пользователя для разблокировки счета. + Адрес, на который необходимо отправить пользователя для разблокировки счета Поле присутствует в случае ошибки account_blocked. """ @@ -403,7 +474,7 @@ class Payment: Поле присутствует при status=in_progress. """ - digital_goods: Optional[Dict[str, Dict[str, List[Dict[str, str]]]]] = None + digital_goods: Optional[Dict[str, Dict[str, List[DigitalGoods]]]] = None protection_code: Optional[str] = None """ @@ -416,14 +487,20 @@ def initialize(self, protection_code: str) -> 'Payment': self.protection_code = protection_code return self + class Config: + json_loads = custom_load + extra = Extra.allow -@dataclass(frozen=True) -class IncomingTransaction: + +class IncomingTransaction(BaseModel): status: str protection_code_attempts_available: int ext_action_uri: Optional[str] = None error: Optional[str] = None + class Config: + json_loads = custom_load + __all__ = ( 'AccountInfo', 'OperationType', 'Operation', diff --git a/glQiwiApi/utils/basics.py b/glQiwiApi/utils/basics.py index 08aacfd2..e178c9cf 100644 --- a/glQiwiApi/utils/basics.py +++ b/glQiwiApi/utils/basics.py @@ -6,10 +6,8 @@ import time import warnings from copy import deepcopy -from dataclasses import is_dataclass from datetime import datetime from json import JSONDecodeError -from typing import Optional, Union, Type import pytz from pydantic import ValidationError, BaseModel @@ -120,37 +118,6 @@ def set_data_to_wallet(data, to_number, trans_sum, comment, currency): return data -def format_objects(iterable_obj, obj, transfers=None): - """ - Функция для форматирования объектов, которые приходят от апи - """ - kwargs = {} - objects = [] - for transaction in iterable_obj: - for key, value in transaction.items(): - if key in obj.__dict__.get( - '__annotations__').keys() or key in transfers.keys(): - try: - fill_key = key if not transfers.get( - key) else transfers.get( - key) - except AttributeError: - fill_key = key - sp_obj: Optional[Union[str, Type]] = obj.__dict__.get( - '__annotations__').get(fill_key) - if is_dataclass(sp_obj): - try: - kwargs.update({fill_key: sp_obj(**value)}) - except TypeError: - kwargs.update({fill_key: sp_obj( - **format_objects_for_fill(value, transfers))}) - continue - kwargs.update({fill_key: value}) - objects.append(obj(**kwargs)) - kwargs = {} - return objects - - def set_data_p2p_create(wrapped_data, amount, life_time, comment, theme_code, pay_source_filter): wrapped_data.json['amount']['value'] = str(amount) @@ -275,6 +242,16 @@ def wrapper(*args, **kwargs): return wrapper +def parse_amount(txn_type, txn): + if txn_type == 'in': + amount = txn.amount + comment = txn.comment + else: + amount = txn.amount_due + comment = txn.message + return amount, comment + + def _run_forever_safe(loop) -> None: """ run a loop for ever and clean up after being stopped """ @@ -325,9 +302,11 @@ def _stop_loop(loop) -> None: def sync(func, *args, **kwargs): + """ Function to use async functions(libraries) synchronously """ loop = asyncio.new_event_loop() # construct a new event loop executor = futures.ThreadPoolExecutor(2, 'sync_connector_') + # Run our coroutine thread safe my_task = asyncio.run_coroutine_threadsafe( func(*args, **kwargs), loop=loop @@ -338,4 +317,5 @@ def sync(func, *args, **kwargs): result = _await_sync(my_task, executor=executor, loop=loop) # close created early loop loop.call_soon_threadsafe(_stop_loop, loop) + executor.shutdown(wait=True) return result diff --git a/glQiwiApi/utils/basics.pyi b/glQiwiApi/utils/basics.pyi index b628ddc4..7b127e2f 100644 --- a/glQiwiApi/utils/basics.pyi +++ b/glQiwiApi/utils/basics.pyi @@ -23,11 +23,6 @@ def parse_headers(content_json: bool = False, auth: bool = False) -> Dict[ Any, Any]: ... -def format_objects(iterable_obj: Union[list, tuple], obj: Type, - transfers: Optional[Dict[str, str]] = None) -> Optional[ - List[types.BasicTypes]]: ... - - def set_data_to_wallet(data: types.WrapperData, to_number: str, trans_sum: Union[str, int, float], @@ -80,6 +75,10 @@ def sync_measure_time(func: Any) -> Any: ... def _run_forever_safe(loop: asyncio.AbstractEventLoop) -> None: ... +def parse_amount(txn_type: str, txn: types.OperationDetails) -> Tuple[ + Union[int, float], str]: ... + + def _await_sync(future: asyncio.Future, executor: types.E_, loop: asyncio.AbstractEventLoop) -> Any: ... @@ -400,7 +399,8 @@ def sync( @overload def sync( func: Callable[ - [Optional[None]], Coroutine[Any, Any, Optional[Union[float, int]]]], + [Optional[None]], Coroutine[ + Any, Any, Optional[Union[float, int]]]], *args: Any, **kwargs: Any ) -> Optional[Union[float, int]]: ... diff --git a/glQiwiApi/yoo_money/yoomoney_api.py b/glQiwiApi/yoo_money/yoomoney_api.py index fcb903f0..e7d1fa33 100644 --- a/glQiwiApi/yoo_money/yoomoney_api.py +++ b/glQiwiApi/yoo_money/yoomoney_api.py @@ -1,10 +1,14 @@ from datetime import datetime from typing import List, Dict, Any, Union, Optional, Tuple +from pydantic import ValidationError + import glQiwiApi.utils.basics as api_helper -from glQiwiApi.abstracts import AbstractPaymentWrapper -from glQiwiApi.aiohttp_custom_api import CustomParser -from glQiwiApi.mixins import ToolsMixin +from glQiwiApi.core import ( + AbstractPaymentWrapper, + CustomParser, + ToolsMixin +) from glQiwiApi.types import ( AccountInfo, OperationType, @@ -19,8 +23,7 @@ from glQiwiApi.yoo_money.basic_yoomoney_config import ( BASE_YOOMONEY_URL, ERROR_CODE_NUMBERS, - content_and_auth, - OPERATION_TRANSFER + content_and_auth ) @@ -30,7 +33,7 @@ class YooMoneyAPI(AbstractPaymentWrapper, ToolsMixin): Удобен он тем, что не просто отдает json подобные объекты, а всё это конвертирует в python dataclasses. Для работы с данным классом, необходимо зарегистрировать токен, - используя гайд на официальном гитхабе проекта + используя гайд на официальном github проекта """ @@ -184,10 +187,7 @@ async def account_info(self) -> AccountInfo: headers=headers, method='POST' ): - return api_helper.format_objects( - iterable_obj=(response.response_data,), - obj=AccountInfo, - )[0] + return AccountInfo.parse_raw(response.response_data) async def transactions( self, @@ -205,8 +205,8 @@ async def transactions( https://yoomoney.ru/docs/wallet/user-account/operation-history\n Метод позволяет просматривать историю операций (полностью или частично) в постраничном режиме.\n - Записи истории выдаются в обратном хронологическом порядке: от последних - к более ранним.\n + Записи истории выдаются в обратном хронологическом порядке: + от последних к более ранним.\n Перечень типов операций, которые требуется отобразить.\n Возможные значения: DEPOSITION — пополнение счета (приход);\n @@ -291,10 +291,9 @@ async def transactions( data=data, get_json=True ): - return api_helper.format_objects( - iterable_obj=response.response_data.get('operations'), - obj=Operation, - transfers=OPERATION_TRANSFER + return api_helper.multiply_objects_parse( + lst_of_objects=response.response_data.get('operations'), + model=Operation ) async def transaction_info(self, operation_id: str) -> OperationDetails: @@ -317,11 +316,7 @@ async def transaction_info(self, operation_id: str) -> OperationDetails: headers=headers, data=payload ): - return api_helper.format_objects( - iterable_obj=(response.response_data,), - obj=OperationDetails, - transfers=OPERATION_TRANSFER - )[0] + return OperationDetails.parse_raw(response.response_data) async def _pre_process_payment( self, @@ -379,17 +374,14 @@ async def _pre_process_payment( data=payload ): try: - obj = api_helper.format_objects( - iterable_obj=(response.response_data,), - obj=PreProcessPaymentResponse - )[0] - if obj.status != 'success': - raise IndexError() - return obj - except (IndexError, AttributeError): - raise ConnectionError( - 'Не удалось создать pre_payment запрос, ' - 'проверьте переданные параметры и попробуйте ещё раз' + return PreProcessPaymentResponse.parse_raw( + response.response_data + ) + except ValidationError: + msg = "Недостаточно денег для перевода или ошибка сервиса" + self._parser.raise_exception( + status_code=400, + message=msg ) async def send( @@ -456,6 +448,7 @@ async def send( protect=protect, pattern_id=pattern_id ) + headers = self._auth_token( api_helper.parse_headers(**content_and_auth)) @@ -465,20 +458,20 @@ async def send( } expression = isinstance(pre_payment, PreProcessPaymentResponse) if money_source == 'card' and expression: - if pre_payment.money_source.get('cards').get('allowed') == 'true': + if pre_payment.money_source.cards.allowed == 'true': if not isinstance(card_type, str): - cards = pre_payment.money_source['cards'] + cards = pre_payment.money_source.cards payload.update({ - 'money_source': cards['items'][0]['id'], + 'money_source': cards.items[0].item_id, 'csc': cvv2_code }) else: - cards = pre_payment.money_source.get('cards').get('items') + cards = pre_payment.money_source.cards.items for card in cards: - if card.get('type') == card_type: + if card.item_type == card_type: payload.update( { - 'money_source': card.get('id'), + 'money_source': card.item_id, 'csc': cvv2_code }, ) @@ -488,10 +481,9 @@ async def send( headers=headers, data=payload ): - return api_helper.format_objects( - iterable_obj=(response.response_data,), - obj=Payment - )[0].initialize(pre_payment.protection_code) + return Payment.parse_raw( + response.response_data + ).initialize(pre_payment.protection_code) async def get_balance(self) -> Optional[Union[float, int]]: """Метод для получения баланса на кошельке yoomoney""" @@ -531,10 +523,9 @@ async def accept_incoming_transaction( method='POST', get_json=True ): - return api_helper.format_objects( - iterable_obj=(response.response_data,), - obj=IncomingTransaction - )[0] + return IncomingTransaction.parse_raw( + response.response_data + ) async def reject_transaction(self, operation_id: str) -> Dict[str, str]: """ @@ -584,30 +575,36 @@ async def check_transaction( :param comment: комментарий, по которому будет проверяться транзакция :return: bool, есть ли такая транзакция в истории платежей """ + # Generate types of transactions for request types = [ - OperationType.DEPOSITION if transaction_type == 'in' else OperationType.PAYMENT - ] + OperationType.DEPOSITION if transaction_type == 'in' else OperationType.PAYMENT] + # Get transactions by params transactions = await self.transactions( operation_types=types, records=rows_num ) - for transaction in transactions: - details_transaction = await self.transaction_info( - transaction.operation_id + for txn in transactions: + # Get details of transaction to check it later + txn_detail = await self.transaction_info(txn.operation_id) + + # Parse amount and comment, + # because the parameters depend on the type of transaction + amount_, comment_ = api_helper.parse_amount( + transaction_type, + txn_detail ) - detail_amount = details_transaction.amount if transaction_type == 'in' else details_transaction.amount_due - detail_comment = details_transaction.comment if transaction_type == 'in' else details_transaction.message - if detail_amount >= amount: - if details_transaction.direction == transaction_type: - if transaction.status == 'success': - if detail_comment == comment: - if details_transaction.sender == sender_number: + + if amount_ >= amount: + if txn_detail.direction == transaction_type: + if txn.status == 'success': + if comment_ == comment: + if txn_detail.sender == sender_number: return True elif isinstance(comment, str) and isinstance( sender_number, str): continue - elif detail_comment == comment: + elif comment_ == comment: return True return False diff --git a/setup.py b/setup.py index 2a43af87..6a9921f8 100644 --- a/setup.py +++ b/setup.py @@ -7,10 +7,10 @@ packages=setuptools.find_packages(), include_package_data=True, name="glQiwiApi", # Replace with your own username - version="0.2.12", + version="0.2.13", author="GLEF1X", author_email="glebgar567@gmail.com", - description="Parser for post and get requests", + description="Light and fast wrapper for qiwi and yoomoney", # Длинное описание, которое будет отображаться на странице PyPi. # Использует README.md репозитория для заполнения. long_description=long_description, From 56880430834f5a0c24f88a2c19d7e7bc8089d553 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 14:25:51 +0300 Subject: [PATCH 07/14] change README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 413e3375..9ae9c656 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,6 @@ from glQiwiApi import QiwiWrapper async def main(): # You can pass on only p2p tokens, if you want to use only p2p api async with QiwiWrapper( - public_p2p="your_p2p", secret_p2p="your_secret_p2p" ) as w: # Таким образом можно создать p2p счет From 1dae72fa96cb2aa79f885fac418a87419099fda8 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 14:30:08 +0300 Subject: [PATCH 08/14] change README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9ae9c656..5e399f49 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ [![PyPI version](https://img.shields.io/pypi/v/glQiwiApi.svg)](https://pypi.org/project/glQiwiApi/) [![Python](https://img.shields.io/badge/Python-3.7+-blue)](https://www.python.org/downloads/) [![Code Quality Score](https://www.code-inspector.com/project/20780/score/svg)](https://frontend.code-inspector.com/public/project/20780/glQiwiApi/dashboard) ![Code Grade](https://www.code-inspector.com/project/20780/status/svg) ![Downloads](https://img.shields.io/pypi/dm/glQiwiApi) ![docs](https://readthedocs.org/projects/pip/badge/?version=latest) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/GLEF1X/glQiwiApi.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/GLEF1X/glQiwiApi/context:python) - - -## :globe_with_meridians:Official api resources: - * 🎓__Docs: [here](https://glqiwiapi.readthedocs.io/en/master/index.html)__ - * 🖱️ __Developer contacts: [![Dev-Telegram](https://img.shields.io/badge/Telegram-blue.svg?style=flat-square&logo=telegram)](https://t.me/GLEF1X)__ + +## 🌎Official api resources: + +* 🎓__Docs: [here](https://glqiwiapi.readthedocs.io/en/master/index.html)__ +* 🖱️ __Developer contacts: [![Dev-Telegram](https://img.shields.io/badge/Telegram-blue.svg?style=flat-square&logo=telegram)](https://t.me/GLEF1X)__ ### 📣New feature. Add YooMoney support and `pydantic` models to library! ### 💾Installation From 7f0b62776b52544efc62a886005749e882e9ac82 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 14:36:35 +0300 Subject: [PATCH 09/14] some fixes --- README.md | 1 - glQiwiApi/core/mixins.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5e399f49..cf6e4d82 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,6 @@ __glQiwiApi covers qiwi's MAPS api in QiwiMaps class__ --- # YooMoney API ---- ## Important. How to get YooMoney access token diff --git a/glQiwiApi/core/mixins.py b/glQiwiApi/core/mixins.py index 5f1f8c23..26f22c7b 100644 --- a/glQiwiApi/core/mixins.py +++ b/glQiwiApi/core/mixins.py @@ -36,7 +36,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): await self._parser.session.close() self._parser.clear_cache() - def get(self, item: Any) -> Any: + def _get(self, item: Any) -> Any: try: return super().__getattribute__(item) except AttributeError: @@ -46,8 +46,8 @@ def __deepcopy__(self, memo) -> 'ToolsMixin': cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result - dct = {slot: self.get(slot) for slot in self.__slots__ if - self.get(slot) is not None} + dct = {slot: self._get(slot) for slot in self.__slots__ if + self._get(slot) is not None} for k, v in dct.items(): if k == '_parser': v.session = None From 318facdd510ef9573efdba0c64a3d40648862f3d Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 15:11:26 +0300 Subject: [PATCH 10/14] refactor changes --- examples/usage_in_bots.py | 4 +-- glQiwiApi/__init__.py | 3 +- glQiwiApi/types/__init__.py | 6 ++-- glQiwiApi/types/basics.py | 8 +++++ glQiwiApi/types/qiwi_types/account.py | 7 ++++ glQiwiApi/types/qiwi_types/account_info.py | 7 ++++ glQiwiApi/types/qiwi_types/balance.py | 7 ++++ glQiwiApi/types/qiwi_types/bill.py | 14 ++++++++ glQiwiApi/types/qiwi_types/identification.py | 7 ++++ glQiwiApi/types/qiwi_types/limit.py | 7 ++++ glQiwiApi/types/qiwi_types/partner.py | 12 +++++++ glQiwiApi/types/qiwi_types/payment_info.py | 11 ++++++ glQiwiApi/types/qiwi_types/stats.py | 7 ++++ glQiwiApi/types/qiwi_types/transaction.py | 7 ++++ glQiwiApi/types/yoomoney_types/types.py | 36 ++++++++++++++++++++ glQiwiApi/utils/basics.py | 2 +- 16 files changed, 138 insertions(+), 7 deletions(-) diff --git a/examples/usage_in_bots.py b/examples/usage_in_bots.py index 63b064c9..b63d2c2f 100644 --- a/examples/usage_in_bots.py +++ b/examples/usage_in_bots.py @@ -20,8 +20,8 @@ async def create_payment(amount: Union[float, int] = 1) -> qiwi_types.Bill: - async with wallet as w: - return await w.create_p2p_bill(amount=amount) + async with wallet: + return await wallet.create_p2p_bill(amount=amount) @dp.message_handler(text='Хочу оплатить') diff --git a/glQiwiApi/__init__.py b/glQiwiApi/__init__.py index 5f684d9f..ecef454d 100644 --- a/glQiwiApi/__init__.py +++ b/glQiwiApi/__init__.py @@ -1,5 +1,6 @@ import sys +from glQiwiApi.utils import exceptions from .qiwi import QiwiWrapper, QiwiMaps # NOQA from .utils.basics import sync, to_datetime # NOQA from .utils.exceptions import * # NOQA @@ -13,7 +14,7 @@ 'RequestError', 'to_datetime', 'sync' - ) + utils.exceptions.__all__ # NOQA + ) + exceptions.__all__ # NOQA ) diff --git a/glQiwiApi/types/__init__.py b/glQiwiApi/types/__init__.py index 9ac2ff1b..394c8ea0 100644 --- a/glQiwiApi/types/__init__.py +++ b/glQiwiApi/types/__init__.py @@ -48,8 +48,8 @@ Terminal, Partner ] -E_ = TypeVar( - 'E_', +E = TypeVar( + 'E', futures.ThreadPoolExecutor, futures.ProcessPoolExecutor, Optional[None] @@ -89,6 +89,6 @@ 'Polygon', 'Terminal', 'Partner', - 'E_', + 'E', 'FuncT' ] diff --git a/glQiwiApi/types/basics.py b/glQiwiApi/types/basics.py index 0b51def9..484e1c88 100644 --- a/glQiwiApi/types/basics.py +++ b/glQiwiApi/types/basics.py @@ -18,8 +18,15 @@ class Sum(BaseModel): currency: str class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + class OptionalSum(BaseModel): value: Union[int, float] @@ -38,6 +45,7 @@ class Commission(BaseModel): withdraw_to_enrollment_rate: int = Field(alias="withdrawToEnrollmentRate") class Config: + """ Pydantic config """ json_loads = custom_load diff --git a/glQiwiApi/types/qiwi_types/account.py b/glQiwiApi/types/qiwi_types/account.py index c5e3b9c0..6cfb94b5 100644 --- a/glQiwiApi/types/qiwi_types/account.py +++ b/glQiwiApi/types/qiwi_types/account.py @@ -18,8 +18,15 @@ class Account(BaseModel): is_default_account: bool = Field(alias="defaultAccount") class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + __all__ = [ 'Account' diff --git a/glQiwiApi/types/qiwi_types/account_info.py b/glQiwiApi/types/qiwi_types/account_info.py index ce9dfe36..efea78f6 100644 --- a/glQiwiApi/types/qiwi_types/account_info.py +++ b/glQiwiApi/types/qiwi_types/account_info.py @@ -106,8 +106,15 @@ class QiwiAccountInfo(BaseModel): user_info: Optional[UserInfo] = Field(alias="userInfo", const=None) class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + __all__ = [ 'QiwiAccountInfo' diff --git a/glQiwiApi/types/qiwi_types/balance.py b/glQiwiApi/types/qiwi_types/balance.py index bbf0dc57..53c2b573 100644 --- a/glQiwiApi/types/qiwi_types/balance.py +++ b/glQiwiApi/types/qiwi_types/balance.py @@ -8,4 +8,11 @@ class Balance(BaseModel): currency: int class Config: + """ Pydantic config """ json_loads = custom_load + + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() diff --git a/glQiwiApi/types/qiwi_types/bill.py b/glQiwiApi/types/qiwi_types/bill.py index 4dee2a68..241aa06c 100644 --- a/glQiwiApi/types/qiwi_types/bill.py +++ b/glQiwiApi/types/qiwi_types/bill.py @@ -33,8 +33,15 @@ class BillError(BaseModel): trace_id: str = Field(alias="traceId") class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + class Bill(BaseModel, BillMixin): site_id: str = Field(alias="siteId") @@ -50,10 +57,17 @@ class Bill(BaseModel, BillMixin): customer: Optional[Customer] = None class Config: + """ Pydantic config """ extra = 'allow' json_loads = custom_load allow_mutation = True + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + class RefundBill(BaseModel): """ diff --git a/glQiwiApi/types/qiwi_types/identification.py b/glQiwiApi/types/qiwi_types/identification.py index 29787c93..263bbcca 100644 --- a/glQiwiApi/types/qiwi_types/identification.py +++ b/glQiwiApi/types/qiwi_types/identification.py @@ -19,4 +19,11 @@ class Identification(BaseModel): type: str class Config: + """ Pydantic config """ json_loads = custom_load + + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() diff --git a/glQiwiApi/types/qiwi_types/limit.py b/glQiwiApi/types/qiwi_types/limit.py index fd2cb926..a8084556 100644 --- a/glQiwiApi/types/qiwi_types/limit.py +++ b/glQiwiApi/types/qiwi_types/limit.py @@ -20,8 +20,15 @@ class Limit(BaseModel): limit_type: str = Field(alias="type") class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + __all__ = [ 'Limit' diff --git a/glQiwiApi/types/qiwi_types/partner.py b/glQiwiApi/types/qiwi_types/partner.py index 887f2999..4b86af2d 100644 --- a/glQiwiApi/types/qiwi_types/partner.py +++ b/glQiwiApi/types/qiwi_types/partner.py @@ -3,6 +3,8 @@ from pydantic import BaseModel +from glQiwiApi.utils.basics import custom_load + class Partner(BaseModel): title: str @@ -10,5 +12,15 @@ class Partner(BaseModel): maps: Optional[List[str]] = None + class Config: + """ Pydantic config """ + json_loads = custom_load + + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + __all__ = ("Partner",) diff --git a/glQiwiApi/types/qiwi_types/payment_info.py b/glQiwiApi/types/qiwi_types/payment_info.py index ebe73c57..731fffa3 100644 --- a/glQiwiApi/types/qiwi_types/payment_info.py +++ b/glQiwiApi/types/qiwi_types/payment_info.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, Field from glQiwiApi.types import Sum +from glQiwiApi.utils.basics import custom_load class Fields(BaseModel): @@ -27,6 +28,16 @@ class PaymentInfo(BaseModel): transaction: Optional[TransactionInfo] = None comment: Optional[str] = None + class Config: + """ Pydantic config """ + json_loads = custom_load + + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + __all__ = [ 'PaymentInfo' diff --git a/glQiwiApi/types/qiwi_types/stats.py b/glQiwiApi/types/qiwi_types/stats.py index 395a71fc..b38ebf3f 100644 --- a/glQiwiApi/types/qiwi_types/stats.py +++ b/glQiwiApi/types/qiwi_types/stats.py @@ -11,8 +11,15 @@ class Statistic(BaseModel): out: List[Sum] = Field(alias="outgoingTotal") class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + __all__ = [ 'Statistic' diff --git a/glQiwiApi/types/qiwi_types/transaction.py b/glQiwiApi/types/qiwi_types/transaction.py index c6135fcc..1201d3dc 100644 --- a/glQiwiApi/types/qiwi_types/transaction.py +++ b/glQiwiApi/types/qiwi_types/transaction.py @@ -101,8 +101,15 @@ class Transaction(BaseModel): """Курс конвертации (если применяется в транзакции)""" class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + def as_str(self): return f"""Статус транзакции: {self.status} Дата проведения платежа: {self.date} diff --git a/glQiwiApi/types/yoomoney_types/types.py b/glQiwiApi/types/yoomoney_types/types.py index 3ba27702..86886ba2 100644 --- a/glQiwiApi/types/yoomoney_types/types.py +++ b/glQiwiApi/types/yoomoney_types/types.py @@ -76,8 +76,15 @@ class AccountInfo(BaseModel): """ class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + class OperationType(Enum): """ @@ -169,8 +176,15 @@ class Operation(BaseModel): details: Optional[Any] = None class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + class OperationDetails(BaseModel): operation_id: Optional[str] = None @@ -315,8 +329,15 @@ class OperationDetails(BaseModel): """ class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + class Wallet(BaseModel): allowed: bool @@ -386,8 +407,15 @@ class PreProcessPaymentResponse(BaseModel): ext_action_uri: Optional[str] = None class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + class Payment(BaseModel): status: str @@ -488,6 +516,7 @@ def initialize(self, protection_code: str) -> 'Payment': return self class Config: + """ Pydantic config """ json_loads = custom_load extra = Extra.allow @@ -499,8 +528,15 @@ class IncomingTransaction(BaseModel): error: Optional[str] = None class Config: + """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return super().__str__() + + def __repr__(self) -> str: + return super().__repr__() + __all__ = ( 'AccountInfo', 'OperationType', 'Operation', diff --git a/glQiwiApi/utils/basics.py b/glQiwiApi/utils/basics.py index e178c9cf..417c7d33 100644 --- a/glQiwiApi/utils/basics.py +++ b/glQiwiApi/utils/basics.py @@ -173,8 +173,8 @@ def custom_load(data): return orjson.loads(json.dumps(data)) -# Модель pydantic для перевода строки в datetime class Parser(BaseModel): + """ Модель pydantic для перевода строки в datetime """ dt: datetime From 74ccdd3dc958b57cd320aaddd6305a041ff7a99f Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 15:26:07 +0300 Subject: [PATCH 11/14] refactor changes --- glQiwiApi/types/basics.py | 6 +++++ glQiwiApi/types/qiwi_types/account.py | 13 +++++---- glQiwiApi/types/qiwi_types/account_info.py | 4 +-- glQiwiApi/types/qiwi_types/balance.py | 4 +-- glQiwiApi/types/qiwi_types/bill.py | 4 +-- glQiwiApi/types/qiwi_types/identification.py | 4 +-- glQiwiApi/types/qiwi_types/limit.py | 4 +-- glQiwiApi/types/qiwi_types/partner.py | 4 +-- glQiwiApi/types/qiwi_types/payment_info.py | 4 +-- glQiwiApi/types/qiwi_types/polygon.py | 5 +++- glQiwiApi/types/qiwi_types/stats.py | 4 +-- glQiwiApi/types/qiwi_types/terminal.py | 12 +++++++++ glQiwiApi/types/qiwi_types/transaction.py | 4 +-- glQiwiApi/types/yoomoney_types/types.py | 14 +++++++--- glQiwiApi/utils/basics.py | 7 +++++ glQiwiApi/utils/basics.pyi | 8 ++++-- glQiwiApi/yoo_money/yoomoney_api.py | 28 +++++++++++--------- 17 files changed, 85 insertions(+), 44 deletions(-) diff --git a/glQiwiApi/types/basics.py b/glQiwiApi/types/basics.py index 484e1c88..f0695974 100644 --- a/glQiwiApi/types/basics.py +++ b/glQiwiApi/types/basics.py @@ -48,6 +48,12 @@ class Config: """ Pydantic config """ json_loads = custom_load + def __str__(self) -> str: + return f'Config class with loads={self.json_loads}' + + def __repr__(self) -> str: + return self.__str__() + class Type(BaseModel): """ diff --git a/glQiwiApi/types/qiwi_types/account.py b/glQiwiApi/types/qiwi_types/account.py index 6cfb94b5..e707bdb2 100644 --- a/glQiwiApi/types/qiwi_types/account.py +++ b/glQiwiApi/types/qiwi_types/account.py @@ -17,15 +17,14 @@ class Account(BaseModel): account_type: Optional[Type] = Field(None, alias="type") is_default_account: bool = Field(alias="defaultAccount") - class Config: - """ Pydantic config """ - json_loads = custom_load + """ Pydantic config """ + json_loads = custom_load - def __str__(self) -> str: - return super().__str__() + def __str__(self) -> str: + return super().__str__() - def __repr__(self) -> str: - return super().__repr__() + def __repr__(self) -> str: + return super().__repr__() __all__ = [ diff --git a/glQiwiApi/types/qiwi_types/account_info.py b/glQiwiApi/types/qiwi_types/account_info.py index efea78f6..e89955ce 100644 --- a/glQiwiApi/types/qiwi_types/account_info.py +++ b/glQiwiApi/types/qiwi_types/account_info.py @@ -110,10 +110,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() __all__ = [ diff --git a/glQiwiApi/types/qiwi_types/balance.py b/glQiwiApi/types/qiwi_types/balance.py index 53c2b573..025d5734 100644 --- a/glQiwiApi/types/qiwi_types/balance.py +++ b/glQiwiApi/types/qiwi_types/balance.py @@ -12,7 +12,7 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() diff --git a/glQiwiApi/types/qiwi_types/bill.py b/glQiwiApi/types/qiwi_types/bill.py index 241aa06c..6600b017 100644 --- a/glQiwiApi/types/qiwi_types/bill.py +++ b/glQiwiApi/types/qiwi_types/bill.py @@ -37,10 +37,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() class Bill(BaseModel, BillMixin): diff --git a/glQiwiApi/types/qiwi_types/identification.py b/glQiwiApi/types/qiwi_types/identification.py index 263bbcca..0d609e43 100644 --- a/glQiwiApi/types/qiwi_types/identification.py +++ b/glQiwiApi/types/qiwi_types/identification.py @@ -23,7 +23,7 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() diff --git a/glQiwiApi/types/qiwi_types/limit.py b/glQiwiApi/types/qiwi_types/limit.py index a8084556..147b9297 100644 --- a/glQiwiApi/types/qiwi_types/limit.py +++ b/glQiwiApi/types/qiwi_types/limit.py @@ -24,10 +24,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() __all__ = [ diff --git a/glQiwiApi/types/qiwi_types/partner.py b/glQiwiApi/types/qiwi_types/partner.py index 4b86af2d..5f071e3e 100644 --- a/glQiwiApi/types/qiwi_types/partner.py +++ b/glQiwiApi/types/qiwi_types/partner.py @@ -17,10 +17,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() __all__ = ("Partner",) diff --git a/glQiwiApi/types/qiwi_types/payment_info.py b/glQiwiApi/types/qiwi_types/payment_info.py index 731fffa3..75a6eea1 100644 --- a/glQiwiApi/types/qiwi_types/payment_info.py +++ b/glQiwiApi/types/qiwi_types/payment_info.py @@ -33,10 +33,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() __all__ = [ diff --git a/glQiwiApi/types/qiwi_types/polygon.py b/glQiwiApi/types/qiwi_types/polygon.py index 9af2712e..3199f205 100644 --- a/glQiwiApi/types/qiwi_types/polygon.py +++ b/glQiwiApi/types/qiwi_types/polygon.py @@ -12,7 +12,10 @@ def __init__(self, lat_lon_pair_nw: tuple, lat_lon_pair_se: tuple): @property def dict(self): - return {k: str(double) for k, double in self._dict.items()} + return {k: str(double) for k, double in self._get_items()} + + def _get_items(self): + return self._dict.items() __all__ = ("Polygon",) diff --git a/glQiwiApi/types/qiwi_types/stats.py b/glQiwiApi/types/qiwi_types/stats.py index b38ebf3f..4bbb0e37 100644 --- a/glQiwiApi/types/qiwi_types/stats.py +++ b/glQiwiApi/types/qiwi_types/stats.py @@ -15,10 +15,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() __all__ = [ diff --git a/glQiwiApi/types/qiwi_types/terminal.py b/glQiwiApi/types/qiwi_types/terminal.py index 6549057d..2498d975 100644 --- a/glQiwiApi/types/qiwi_types/terminal.py +++ b/glQiwiApi/types/qiwi_types/terminal.py @@ -3,6 +3,8 @@ from pydantic import Field, BaseModel +from glQiwiApi.utils.basics import custom_load + class Coordinate(BaseModel): """Object: coordinate""" @@ -28,5 +30,15 @@ class Terminal(BaseModel): identification_type: int = Field(..., alias="identificationType") coordinate: Coordinate = Field(..., alias="coordinate") + class Config: + """ Pydantic config """ + json_loads = custom_load + + def __str__(self) -> str: + return f'Config class with loads={self.json_loads}' + + def __repr__(self) -> str: + return self.__str__() + __all__ = ("Terminal",) diff --git a/glQiwiApi/types/qiwi_types/transaction.py b/glQiwiApi/types/qiwi_types/transaction.py index 1201d3dc..f16518ab 100644 --- a/glQiwiApi/types/qiwi_types/transaction.py +++ b/glQiwiApi/types/qiwi_types/transaction.py @@ -105,10 +105,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() def as_str(self): return f"""Статус транзакции: {self.status} diff --git a/glQiwiApi/types/yoomoney_types/types.py b/glQiwiApi/types/yoomoney_types/types.py index 86886ba2..1eb74c90 100644 --- a/glQiwiApi/types/yoomoney_types/types.py +++ b/glQiwiApi/types/yoomoney_types/types.py @@ -80,10 +80,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() class OperationType(Enum): @@ -333,10 +333,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() class Wallet(BaseModel): @@ -520,6 +520,12 @@ class Config: json_loads = custom_load extra = Extra.allow + def __str__(self) -> str: + return f'Config class with loads={self.json_loads}' + + def __repr__(self) -> str: + return self.__str__() + class IncomingTransaction(BaseModel): status: str diff --git a/glQiwiApi/utils/basics.py b/glQiwiApi/utils/basics.py index 417c7d33..8d77beec 100644 --- a/glQiwiApi/utils/basics.py +++ b/glQiwiApi/utils/basics.py @@ -290,6 +290,13 @@ def _await_sync(future, executor, loop): return future.result() +def check_params(amount_, amount, txn, transaction_type): + if amount_ >= amount: + if txn.direction == transaction_type: + return True + return False + + def _cancel_future(loop, future, executor) -> None: """ cancels future if any exception occurred """ executor.submit(loop.call_soon_threadsafe, future.cancel) diff --git a/glQiwiApi/utils/basics.pyi b/glQiwiApi/utils/basics.pyi index 7b127e2f..a97e0c55 100644 --- a/glQiwiApi/utils/basics.pyi +++ b/glQiwiApi/utils/basics.pyi @@ -57,6 +57,10 @@ def simple_multiply_parse(lst_of_objects: Union[List[Union[dict, str]], dict], def custom_load(data: Dict[Any, Any]) -> Dict[str, Any]: ... +def check_params(amount_: Union[int, float], amount: Union[int, float], + txn: types.OperationDetails, transaction_type: str) -> bool: ... + + def allow_response_code(status_code: Union[str, int]) -> Any: ... @@ -79,12 +83,12 @@ def parse_amount(txn_type: str, txn: types.OperationDetails) -> Tuple[ Union[int, float], str]: ... -def _await_sync(future: asyncio.Future, executor: types.E_, +def _await_sync(future: asyncio.Future, executor: types.E, loop: asyncio.AbstractEventLoop) -> Any: ... def _cancel_future(loop: asyncio.AbstractEventLoop, future: asyncio.Future, - executor: types.E_) -> None: ... + executor: types.E) -> None: ... def _stop_loop(loop: asyncio.AbstractEventLoop) -> None: ... diff --git a/glQiwiApi/yoo_money/yoomoney_api.py b/glQiwiApi/yoo_money/yoomoney_api.py index e7d1fa33..bc6305df 100644 --- a/glQiwiApi/yoo_money/yoomoney_api.py +++ b/glQiwiApi/yoo_money/yoomoney_api.py @@ -593,18 +593,22 @@ async def check_transaction( transaction_type, txn_detail ) - - if amount_ >= amount: - if txn_detail.direction == transaction_type: - if txn.status == 'success': - if comment_ == comment: - if txn_detail.sender == sender_number: - return True - elif isinstance(comment, str) and isinstance( - sender_number, - str): - continue - elif comment_ == comment: + checked = api_helper.check_params( + amount=amount, + transaction_type=transaction_type, + amount_=amount_, + txn=txn_detail + ) + if checked: + if txn.status == 'success': + if comment_ == comment: + if txn_detail.sender == sender_number: return True + elif isinstance(comment, str) and isinstance( + sender_number, + str): + continue + elif comment_ == comment: + return True return False From 51f4caa139038f6a39e819f7821e698dfdcffe95 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 15:42:12 +0300 Subject: [PATCH 12/14] refactor changes --- glQiwiApi/core/basic_requests_api.py | 6 +++--- glQiwiApi/types/__init__.py | 6 +++--- glQiwiApi/types/basics.py | 4 ++-- glQiwiApi/types/qiwi_types/account.py | 13 +++++++------ glQiwiApi/types/qiwi_types/bill.py | 6 ++---- glQiwiApi/types/qiwi_types/partner.py | 1 + glQiwiApi/types/qiwi_types/payment_info.py | 4 ++++ glQiwiApi/types/qiwi_types/polygon.py | 7 ++++++- glQiwiApi/types/yoomoney_types/types.py | 12 ++++++------ glQiwiApi/utils/basics.py | 3 +-- glQiwiApi/utils/basics.pyi | 4 ++-- glQiwiApi/yoo_money/yoomoney_api.py | 11 +++++------ 12 files changed, 42 insertions(+), 35 deletions(-) diff --git a/glQiwiApi/core/basic_requests_api.py b/glQiwiApi/core/basic_requests_api.py index d829bc98..a427bdb1 100644 --- a/glQiwiApi/core/basic_requests_api.py +++ b/glQiwiApi/core/basic_requests_api.py @@ -8,13 +8,13 @@ ) from typing import Optional, List, Any, Union -import aiohttp from aiohttp import ( ClientTimeout, ClientRequest, ClientProxyConnectionError, ServerDisconnectedError, - ContentTypeError + ContentTypeError, + ClientSession ) from aiohttp.typedefs import LooseCookies from aiosocksy import SocksError @@ -254,7 +254,7 @@ def __setitem__(self, key, value) -> None: ) def __getitem__(self, item) -> Union[ - CachedResponse, aiohttp.ClientSession + CachedResponse, ClientSession ]: return self.tmp_data.get(item) diff --git a/glQiwiApi/types/__init__.py b/glQiwiApi/types/__init__.py index 394c8ea0..094a3098 100644 --- a/glQiwiApi/types/__init__.py +++ b/glQiwiApi/types/__init__.py @@ -48,8 +48,8 @@ Terminal, Partner ] -E = TypeVar( - 'E', +Executors = TypeVar( + 'Executors', futures.ThreadPoolExecutor, futures.ProcessPoolExecutor, Optional[None] @@ -89,6 +89,6 @@ 'Polygon', 'Terminal', 'Partner', - 'E', + 'Executors', 'FuncT' ] diff --git a/glQiwiApi/types/basics.py b/glQiwiApi/types/basics.py index f0695974..396d2b8e 100644 --- a/glQiwiApi/types/basics.py +++ b/glQiwiApi/types/basics.py @@ -22,10 +22,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() class OptionalSum(BaseModel): diff --git a/glQiwiApi/types/qiwi_types/account.py b/glQiwiApi/types/qiwi_types/account.py index e707bdb2..9c675e59 100644 --- a/glQiwiApi/types/qiwi_types/account.py +++ b/glQiwiApi/types/qiwi_types/account.py @@ -17,14 +17,15 @@ class Account(BaseModel): account_type: Optional[Type] = Field(None, alias="type") is_default_account: bool = Field(alias="defaultAccount") - """ Pydantic config """ - json_loads = custom_load + class Config: + """ Pydantic config """ + json_loads = custom_load - def __str__(self) -> str: - return super().__str__() + def __str__(self) -> str: + return f'Config class with loads={self.json_loads}' - def __repr__(self) -> str: - return super().__repr__() + def __repr__(self) -> str: + return self.__str__() __all__ = [ diff --git a/glQiwiApi/types/qiwi_types/bill.py b/glQiwiApi/types/qiwi_types/bill.py index 6600b017..2f237e84 100644 --- a/glQiwiApi/types/qiwi_types/bill.py +++ b/glQiwiApi/types/qiwi_types/bill.py @@ -58,15 +58,13 @@ class Bill(BaseModel, BillMixin): class Config: """ Pydantic config """ - extra = 'allow' json_loads = custom_load - allow_mutation = True def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() class RefundBill(BaseModel): diff --git a/glQiwiApi/types/qiwi_types/partner.py b/glQiwiApi/types/qiwi_types/partner.py index 5f071e3e..1d8c3a7b 100644 --- a/glQiwiApi/types/qiwi_types/partner.py +++ b/glQiwiApi/types/qiwi_types/partner.py @@ -7,6 +7,7 @@ class Partner(BaseModel): + """ Base partner class """ title: str id: int diff --git a/glQiwiApi/types/qiwi_types/payment_info.py b/glQiwiApi/types/qiwi_types/payment_info.py index 75a6eea1..de464acc 100644 --- a/glQiwiApi/types/qiwi_types/payment_info.py +++ b/glQiwiApi/types/qiwi_types/payment_info.py @@ -7,19 +7,23 @@ class Fields(BaseModel): + """ Специальные поля """ account: str class State(BaseModel): + """ State """ code: str class TransactionInfo(BaseModel): + """ Информация о транзакции """ txn_id: int = Field(..., alias="id") state: State class PaymentInfo(BaseModel): + """Информация о платеже""" payment_id: int = Field(..., alias="id") terms: str fields: Fields diff --git a/glQiwiApi/types/qiwi_types/polygon.py b/glQiwiApi/types/qiwi_types/polygon.py index 3199f205..5277fcfb 100644 --- a/glQiwiApi/types/qiwi_types/polygon.py +++ b/glQiwiApi/types/qiwi_types/polygon.py @@ -1,4 +1,9 @@ +from collections import ItemsView + + class Polygon: + """ Polygon class for QiwiMaps class """ + def __init__(self, lat_lon_pair_nw: tuple, lat_lon_pair_se: tuple): self.lat_nw, self.lon_nw = lat_lon_pair_nw self.lat_se, self.lon_se = lat_lon_pair_se @@ -14,7 +19,7 @@ def __init__(self, lat_lon_pair_nw: tuple, lat_lon_pair_se: tuple): def dict(self): return {k: str(double) for k, double in self._get_items()} - def _get_items(self): + def _get_items(self) -> ItemsView: return self._dict.items() diff --git a/glQiwiApi/types/yoomoney_types/types.py b/glQiwiApi/types/yoomoney_types/types.py index 1eb74c90..dbc9238c 100644 --- a/glQiwiApi/types/yoomoney_types/types.py +++ b/glQiwiApi/types/yoomoney_types/types.py @@ -180,10 +180,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() class OperationDetails(BaseModel): @@ -411,10 +411,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() class Payment(BaseModel): @@ -538,10 +538,10 @@ class Config: json_loads = custom_load def __str__(self) -> str: - return super().__str__() + return f'Config class with loads={self.json_loads}' def __repr__(self) -> str: - return super().__repr__() + return self.__str__() __all__ = ( diff --git a/glQiwiApi/utils/basics.py b/glQiwiApi/utils/basics.py index 8d77beec..ccfa35c2 100644 --- a/glQiwiApi/utils/basics.py +++ b/glQiwiApi/utils/basics.py @@ -1,7 +1,6 @@ import asyncio import concurrent.futures as futures import functools as ft -import json import re import time import warnings @@ -170,7 +169,7 @@ async def wrapper(*args, **kwargs): def custom_load(data): - return orjson.loads(json.dumps(data)) + return orjson.loads(orjson.dumps(data)) class Parser(BaseModel): diff --git a/glQiwiApi/utils/basics.pyi b/glQiwiApi/utils/basics.pyi index a97e0c55..7fee78f6 100644 --- a/glQiwiApi/utils/basics.pyi +++ b/glQiwiApi/utils/basics.pyi @@ -83,12 +83,12 @@ def parse_amount(txn_type: str, txn: types.OperationDetails) -> Tuple[ Union[int, float], str]: ... -def _await_sync(future: asyncio.Future, executor: types.E, +def _await_sync(future: asyncio.Future, executor: types.Executors, loop: asyncio.AbstractEventLoop) -> Any: ... def _cancel_future(loop: asyncio.AbstractEventLoop, future: asyncio.Future, - executor: types.E) -> None: ... + executor: types.Executors) -> None: ... def _stop_loop(loop: asyncio.AbstractEventLoop) -> None: ... diff --git a/glQiwiApi/yoo_money/yoomoney_api.py b/glQiwiApi/yoo_money/yoomoney_api.py index bc6305df..7be89adf 100644 --- a/glQiwiApi/yoo_money/yoomoney_api.py +++ b/glQiwiApi/yoo_money/yoomoney_api.py @@ -585,25 +585,24 @@ async def check_transaction( ) for txn in transactions: # Get details of transaction to check it later - txn_detail = await self.transaction_info(txn.operation_id) + detail = await self.transaction_info(txn.operation_id) # Parse amount and comment, # because the parameters depend on the type of transaction amount_, comment_ = api_helper.parse_amount( transaction_type, - txn_detail + detail ) checked = api_helper.check_params( amount=amount, transaction_type=transaction_type, amount_=amount_, - txn=txn_detail + txn=detail ) if checked: if txn.status == 'success': - if comment_ == comment: - if txn_detail.sender == sender_number: - return True + if comment_ == comment and detail.sender == sender_number: + return True elif isinstance(comment, str) and isinstance( sender_number, str): From 573ef65817dca3deda9715a7df879641f5c65bd3 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 15:42:24 +0300 Subject: [PATCH 13/14] refactor changes --- glQiwiApi/utils/basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glQiwiApi/utils/basics.py b/glQiwiApi/utils/basics.py index ccfa35c2..32498def 100644 --- a/glQiwiApi/utils/basics.py +++ b/glQiwiApi/utils/basics.py @@ -211,7 +211,7 @@ def to_datetime(string_representation): :return: datetime representation """ try: - parsed = json.dumps( + parsed = orjson.dumps( {'dt': string_representation} ) return Parser.parse_raw(parsed).dt From 294b477e95ce05172d4e74e61674333b7722b8f9 Mon Sep 17 00:00:00 2001 From: gleb Date: Tue, 20 Apr 2021 15:59:25 +0300 Subject: [PATCH 14/14] refactor changes --- glQiwiApi/types/qiwi_types/polygon.py | 5 +---- glQiwiApi/types/yoomoney_types/types.py | 2 +- glQiwiApi/utils/basics.py | 3 +-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/glQiwiApi/types/qiwi_types/polygon.py b/glQiwiApi/types/qiwi_types/polygon.py index 5277fcfb..a76ce57f 100644 --- a/glQiwiApi/types/qiwi_types/polygon.py +++ b/glQiwiApi/types/qiwi_types/polygon.py @@ -1,6 +1,3 @@ -from collections import ItemsView - - class Polygon: """ Polygon class for QiwiMaps class """ @@ -19,7 +16,7 @@ def __init__(self, lat_lon_pair_nw: tuple, lat_lon_pair_se: tuple): def dict(self): return {k: str(double) for k, double in self._get_items()} - def _get_items(self) -> ItemsView: + def _get_items(self): return self._dict.items() diff --git a/glQiwiApi/types/yoomoney_types/types.py b/glQiwiApi/types/yoomoney_types/types.py index dbc9238c..333d7c27 100644 --- a/glQiwiApi/types/yoomoney_types/types.py +++ b/glQiwiApi/types/yoomoney_types/types.py @@ -417,7 +417,7 @@ def __repr__(self) -> str: return self.__str__() -class Payment(BaseModel): +class Payment(BaseModel): # lgtm [py/missing-equals # status: str """ Код результата выполнения операции. Возможные значения: diff --git a/glQiwiApi/utils/basics.py b/glQiwiApi/utils/basics.py index 32498def..c0dbfcca 100644 --- a/glQiwiApi/utils/basics.py +++ b/glQiwiApi/utils/basics.py @@ -6,7 +6,6 @@ import warnings from copy import deepcopy from datetime import datetime -from json import JSONDecodeError import pytz from pydantic import ValidationError, BaseModel @@ -215,7 +214,7 @@ def to_datetime(string_representation): {'dt': string_representation} ) return Parser.parse_raw(parsed).dt - except (ValidationError, JSONDecodeError) as ex: + except (ValidationError, orjson.JSONDecodeError) as ex: return ex.json(indent=4)