From 541a40f74fddb4a378f48d85b953d17157cc9223 Mon Sep 17 00:00:00 2001 From: Alexeev Nickolay Date: Thu, 7 Apr 2022 21:56:08 +0300 Subject: [PATCH 1/2] (feat) async --- README.md | 4 ++- requirements.txt | 1 + src/yookassa/client.py | 61 +++++++++++++++++++++++++++++++--------- src/yookassa/deal.py | 12 ++++---- src/yookassa/payment.py | 20 ++++++------- src/yookassa/payout.py | 8 +++--- src/yookassa/receipt.py | 12 ++++---- src/yookassa/refund.py | 12 ++++---- src/yookassa/settings.py | 4 +-- src/yookassa/webhook.py | 12 ++++---- 10 files changed, 91 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 6d5d42d..550d940 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,11 @@ Russian | [English](https://github.com/yoomoney/yookassa-sdk-python/blob/master/ Клиент для работы с платежами по [API ЮKassa](https://yookassa.ru/developers/api) Подходит тем, у кого способ подключения к ЮKassa называется API. +Асинхронная версия. + ## Требования -1. Python 2.7 or Python 3.x +1. Python 3.x 2. pip ## Установка diff --git a/requirements.txt b/requirements.txt index 0e808d3..c0f9091 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +httpx requests setuptools urllib3 diff --git a/src/yookassa/client.py b/src/yookassa/client.py index 358c939..d3d7cd4 100644 --- a/src/yookassa/client.py +++ b/src/yookassa/client.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -import requests -from requests.adapters import HTTPAdapter +import typing +import httpx +import time from requests.auth import _basic_auth_str -from urllib3 import Retry from yookassa import Configuration from yookassa.domain.common import RequestObject, UserAgent @@ -10,6 +10,41 @@ ResponseProcessingError, TooManyRequestsError, UnauthorizedError +class RetryTransport(httpx.HTTPTransport): + """ Адаптация urllib3.Retry для HTTPX """ + + def __init__(self, *args, total: int = 3, backoff_factor: float = 1, method_whitelist: typing.Optional[list] = None, status_forcelist: typing.Optional[list] = None, **kwargs): + super(RetryTransport, self).__init__(*args, **kwargs) + self.total = total + self.backoff_factor = backoff_factor + self.method_whitelist = method_whitelist + self.status_forcelist = status_forcelist + + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + retry = 0 + resp = None + retry_active = not self.method_whitelist or request.method in self.method_whitelist + while retry < self.total: + retry += 1 + if retry > 2: + time.sleep(self.backoff_factor) + try: + if resp is not None: + resp.close() + resp = super().handle_request(request) + except Exception: + if not retry_active: + raise + continue + if self.status_forcelist and resp.status_code in self.status_forcelist: + continue + break + return resp + + class ApiClient: endpoint = Configuration.api_endpoint() @@ -29,35 +64,33 @@ def __init__(self): if self.configuration.agent_module: self.user_agent.module = self.configuration.agent_module - def request(self, method="", path="", query_params=None, headers=None, body=None): + async def request(self, method="", path="", query_params=None, headers=None, body=None): if isinstance(body, RequestObject): body.validate() body = dict(body) request_headers = self.prepare_request_headers(headers) - raw_response = self.execute(body, method, path, query_params, request_headers) + raw_response = await self.execute(body, method, path, query_params, request_headers) if raw_response.status_code != 200: self.__handle_error(raw_response) return raw_response.json() - def execute(self, body, method, path, query_params, request_headers): + async def execute(self, body, method, path, query_params, request_headers): session = self.get_session() raw_response = session.request(method, - self.endpoint + path, + f'{self.endpoint}{path}', params=query_params, headers=request_headers, json=body) return raw_response - def get_session(self): - session = requests.Session() - retries = Retry(total=self.max_attempts, - backoff_factor=self.timeout / 1000, - method_whitelist=['POST'], - status_forcelist=[202]) - session.mount('https://', HTTPAdapter(max_retries=retries)) + def get_session(self) -> httpx.Client: + session = httpx.Client( + timeout=httpx.Timeout(self.timeout / 1000, connect=self.timeout / 1000), + transport=RetryTransport() + ) return session def prepare_request_headers(self, headers): diff --git a/src/yookassa/deal.py b/src/yookassa/deal.py index bef433c..ac73d80 100644 --- a/src/yookassa/deal.py +++ b/src/yookassa/deal.py @@ -14,7 +14,7 @@ def __init__(self): self.client = ApiClient() @classmethod - def find_one(cls, deal_id): + async def find_one(cls, deal_id): """ Get receipt information @@ -26,11 +26,11 @@ def find_one(cls, deal_id): raise ValueError('Invalid payment_id value') path = instance.base_path + '/' + deal_id - response = instance.client.request(HttpVerb.GET, path) + response = await instance.client.request(HttpVerb.GET, path) return DealResponse(response) @classmethod - def create(cls, params, idempotency_key=None): + async def create(cls, params, idempotency_key=None): """ Create receipt @@ -55,13 +55,13 @@ def create(cls, params, idempotency_key=None): else: raise TypeError('Invalid params value type') - response = instance.client.request(HttpVerb.POST, path, None, headers, params_object) + response = await instance.client.request(HttpVerb.POST, path, None, headers, params_object) return DealResponse(response) @classmethod - def list(cls, params): + async def list(cls, params): instance = cls() path = cls.base_path - response = instance.client.request(HttpVerb.GET, path, params) + response = await instance.client.request(HttpVerb.GET, path, params) return DealListResponse(response) diff --git a/src/yookassa/payment.py b/src/yookassa/payment.py index f81d0cb..33ba739 100644 --- a/src/yookassa/payment.py +++ b/src/yookassa/payment.py @@ -14,7 +14,7 @@ def __init__(self): self.client = ApiClient() @classmethod - def find_one(cls, payment_id): + async def find_one(cls, payment_id): """ Get information about payment @@ -26,11 +26,11 @@ def find_one(cls, payment_id): raise ValueError('Invalid payment_id value') path = instance.base_path + '/' + payment_id - response = instance.client.request(HttpVerb.GET, path) + response = await instance.client.request(HttpVerb.GET, path) return PaymentResponse(response) @classmethod - def create(cls, params, idempotency_key=None): + async def create(cls, params, idempotency_key=None): """ Create payment @@ -55,11 +55,11 @@ def create(cls, params, idempotency_key=None): else: raise TypeError('Invalid params value type') - response = instance.client.request(HttpVerb.POST, path, None, headers, params_object) + response = await instance.client.request(HttpVerb.POST, path, None, headers, params_object) return PaymentResponse(response) @classmethod - def capture(cls, payment_id, params=None, idempotency_key=None): + async def capture(cls, payment_id, params=None, idempotency_key=None): """ Capture payment @@ -88,11 +88,11 @@ def capture(cls, payment_id, params=None, idempotency_key=None): else: params_object = None - response = instance.client.request(HttpVerb.POST, path, None, headers, params_object) + response = await instance.client.request(HttpVerb.POST, path, None, headers, params_object) return PaymentResponse(response) @classmethod - def cancel(cls, payment_id, idempotency_key=None): + async def cancel(cls, payment_id, idempotency_key=None): """ Cancel payment @@ -111,13 +111,13 @@ def cancel(cls, payment_id, idempotency_key=None): headers = { 'Idempotence-Key': str(idempotency_key) } - response = instance.client.request(HttpVerb.POST, path, None, headers) + response = await instance.client.request(HttpVerb.POST, path, None, headers) return PaymentResponse(response) @classmethod - def list(cls, params): + async def list(cls, params): instance = cls() path = cls.base_path - response = instance.client.request(HttpVerb.GET, path, params) + response = await instance.client.request(HttpVerb.GET, path, params) return PaymentListResponse(response) diff --git a/src/yookassa/payout.py b/src/yookassa/payout.py index ce5027a..d4563f8 100644 --- a/src/yookassa/payout.py +++ b/src/yookassa/payout.py @@ -14,7 +14,7 @@ def __init__(self): self.client = ApiClient() @classmethod - def find_one(cls, payout_id): + async def find_one(cls, payout_id): """ Get receipt information @@ -26,11 +26,11 @@ def find_one(cls, payout_id): raise ValueError('Invalid payment_id value') path = instance.base_path + '/' + payout_id - response = instance.client.request(HttpVerb.GET, path) + response = await instance.client.request(HttpVerb.GET, path) return PayoutResponse(response) @classmethod - def create(cls, params, idempotency_key=None): + async def create(cls, params, idempotency_key=None): """ Create receipt @@ -55,5 +55,5 @@ def create(cls, params, idempotency_key=None): else: raise TypeError('Invalid params value type') - response = instance.client.request(HttpVerb.POST, path, None, headers, params_object) + response = await instance.client.request(HttpVerb.POST, path, None, headers, params_object) return PayoutResponse(response) diff --git a/src/yookassa/receipt.py b/src/yookassa/receipt.py index 3cfb414..f26af4c 100644 --- a/src/yookassa/receipt.py +++ b/src/yookassa/receipt.py @@ -15,7 +15,7 @@ def __init__(self): self.client = ApiClient() @classmethod - def find_one(cls, receipt_id): + async def find_one(cls, receipt_id): """ Get receipt information @@ -27,11 +27,11 @@ def find_one(cls, receipt_id): raise ValueError('Invalid payment_id value') path = instance.base_path + '/' + receipt_id - response = instance.client.request(HttpVerb.GET, path) + response = await instance.client.request(HttpVerb.GET, path) return ReceiptResponse(response) @classmethod - def create(cls, params, idempotency_key=None): + async def create(cls, params, idempotency_key=None): """ Create receipt @@ -56,13 +56,13 @@ def create(cls, params, idempotency_key=None): else: raise TypeError('Invalid params value type') - response = instance.client.request(HttpVerb.POST, path, None, headers, params_object) + response = await instance.client.request(HttpVerb.POST, path, None, headers, params_object) return ReceiptResponse(response) @classmethod - def list(cls, params): + async def list(cls, params): instance = cls() path = cls.base_path - response = instance.client.request(HttpVerb.GET, path, params) + response = await instance.client.request(HttpVerb.GET, path, params) return ReceiptListResponse(response) diff --git a/src/yookassa/refund.py b/src/yookassa/refund.py index 7879d0e..dcc130f 100644 --- a/src/yookassa/refund.py +++ b/src/yookassa/refund.py @@ -15,7 +15,7 @@ def __init__(self): self.client = ApiClient() @classmethod - def create(cls, params, idempotency_key=None): + async def create(cls, params, idempotency_key=None): """ Create refund @@ -38,11 +38,11 @@ def create(cls, params, idempotency_key=None): else: raise TypeError('Invalid params value type') - response = instance.client.request(HttpVerb.POST, path, None, headers, params_object) + response = await instance.client.request(HttpVerb.POST, path, None, headers, params_object) return RefundResponse(response) @classmethod - def find_one(cls, refund_id): + async def find_one(cls, refund_id): """ Get refund information @@ -53,13 +53,13 @@ def find_one(cls, refund_id): if not isinstance(refund_id, str) or not refund_id: raise ValueError('Invalid payment_id value') path = instance.base_path + '/' + refund_id - response = instance.client.request(HttpVerb.GET, path) + response = await instance.client.request(HttpVerb.GET, path) return RefundResponse(response) @classmethod - def list(cls, params): + async def list(cls, params): instance = cls() path = cls.base_path - response = instance.client.request(HttpVerb.GET, path, params) + response = await instance.client.request(HttpVerb.GET, path, params) return RefundListResponse(response) diff --git a/src/yookassa/settings.py b/src/yookassa/settings.py index 2e86e9b..453dd68 100644 --- a/src/yookassa/settings.py +++ b/src/yookassa/settings.py @@ -10,7 +10,7 @@ def __init__(self): self.client = ApiClient() @classmethod - def get_account_settings(cls, params=None): + async def get_account_settings(cls, params=None): """ Shop Info @@ -21,5 +21,5 @@ def get_account_settings(cls, params=None): instance = cls() path = cls.base_path - response = instance.client.request(HttpVerb.GET, path, params) + response = await instance.client.request(HttpVerb.GET, path, params) return response diff --git a/src/yookassa/webhook.py b/src/yookassa/webhook.py index c99a285..ee3a883 100644 --- a/src/yookassa/webhook.py +++ b/src/yookassa/webhook.py @@ -19,11 +19,11 @@ def __init__(self): :return: WebhookList """ @classmethod - def list(cls): + async def list(cls): instance = cls() path = cls.base_path - response = instance.client.request(HttpVerb.GET, path) + response = await instance.client.request(HttpVerb.GET, path) return WebhookList(response) """ @@ -34,7 +34,7 @@ def list(cls): :return: WebhookResponse """ @classmethod - def add(cls, params, idempotency_key=None): + async def add(cls, params, idempotency_key=None): instance = cls() path = cls.base_path if not idempotency_key: @@ -50,7 +50,7 @@ def add(cls, params, idempotency_key=None): else: raise TypeError('Invalid params value type') - response = instance.client.request(HttpVerb.POST, path, None, headers, params_object) + response = await instance.client.request(HttpVerb.POST, path, None, headers, params_object) return WebhookResponse(response) """ @@ -61,7 +61,7 @@ def add(cls, params, idempotency_key=None): :return: WebhookResponse """ @classmethod - def remove(cls, webhook_id, idempotency_key=None): + async def remove(cls, webhook_id, idempotency_key=None): instance = cls() path = cls.base_path + '/' + webhook_id if not idempotency_key: @@ -70,5 +70,5 @@ def remove(cls, webhook_id, idempotency_key=None): 'Idempotence-Key': str(idempotency_key) } - response = instance.client.request(HttpVerb.DELETE, path, None, headers) + response = await instance.client.request(HttpVerb.DELETE, path, None, headers) return WebhookResponse(response) From 9da37b063f4f35955f82cbbcd3d7c427ebe7ceaf Mon Sep 17 00:00:00 2001 From: Alexeev Nickolay Date: Thu, 7 Apr 2022 22:06:24 +0300 Subject: [PATCH 2/2] (fix) issue #2: docs - webhook --- docs/examples/01-configuration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/examples/01-configuration.md b/docs/examples/01-configuration.md index 4e2ab9c..f2a2331 100644 --- a/docs/examples/01-configuration.md +++ b/docs/examples/01-configuration.md @@ -87,8 +87,7 @@ var_dump.var_dump(me) ```python import var_dump as var_dump -from yookassa import Webhook -from yookassa.domain.notification import WebhookNotificationEventType +from yookassa.domain.notification import WebhookNotification, WebhookNotificationEventType whUrl = 'https://merchant-site.ru/payment-notification' needWebhookList = [