paddle-python-sdk 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paddle_billing/Client.py +12 -2
- paddle_billing/Exceptions/ApiError.py +7 -5
- paddle_billing/ResponseParser.py +11 -5
- {paddle_python_sdk-0.1.2.dist-info → paddle_python_sdk-0.1.3.dist-info}/METADATA +1 -1
- {paddle_python_sdk-0.1.2.dist-info → paddle_python_sdk-0.1.3.dist-info}/RECORD +14 -10
- tests/Functional/Client/__init__.py +0 -0
- tests/Functional/Client/test_Client.py +138 -0
- tests/Functional/Resources/Addresses/test_AddressesClient.py +99 -2
- tests/Utils/TestClient.py +10 -5
- tests/Utils/TestLogger.py +28 -0
- tests/conftest.py +2 -0
- {paddle_python_sdk-0.1.2.dist-info → paddle_python_sdk-0.1.3.dist-info}/LICENSE +0 -0
- {paddle_python_sdk-0.1.2.dist-info → paddle_python_sdk-0.1.3.dist-info}/WHEEL +0 -0
- {paddle_python_sdk-0.1.2.dist-info → paddle_python_sdk-0.1.3.dist-info}/top_level.txt +0 -0
paddle_billing/Client.py
CHANGED
|
@@ -9,6 +9,7 @@ from uuid import uuid4
|
|
|
9
9
|
from paddle_billing.FiltersUndefined import FiltersUndefined
|
|
10
10
|
from paddle_billing.HasParameters import HasParameters
|
|
11
11
|
from paddle_billing.Options import Options
|
|
12
|
+
from paddle_billing.ResponseParser import ResponseParser
|
|
12
13
|
|
|
13
14
|
from paddle_billing.Logger.NullHandler import NullHandler
|
|
14
15
|
|
|
@@ -133,8 +134,17 @@ class Client:
|
|
|
133
134
|
except RequestException as e:
|
|
134
135
|
self.status_code = e.response.status_code
|
|
135
136
|
|
|
137
|
+
api_error = None
|
|
138
|
+
if e.response is not None:
|
|
139
|
+
response_parser = ResponseParser(e.response)
|
|
140
|
+
api_error = response_parser.get_error()
|
|
141
|
+
|
|
136
142
|
if self.log:
|
|
137
|
-
self.log.error(f"Request failed: {e}")
|
|
143
|
+
self.log.error(f"Request failed: {e}.{' ' + api_error.detail if api_error is not None else ''}")
|
|
144
|
+
|
|
145
|
+
if api_error is not None:
|
|
146
|
+
raise api_error
|
|
147
|
+
|
|
138
148
|
raise
|
|
139
149
|
|
|
140
150
|
|
|
@@ -187,7 +197,7 @@ class Client:
|
|
|
187
197
|
'Authorization': f"Bearer {self.__api_key}",
|
|
188
198
|
'Content-Type': 'application/json',
|
|
189
199
|
'Paddle-Version': str(self.use_api_version),
|
|
190
|
-
'User-Agent': 'PaddleSDK/python 0.1.
|
|
200
|
+
'User-Agent': 'PaddleSDK/python 0.1.3',
|
|
191
201
|
})
|
|
192
202
|
|
|
193
203
|
# Configure retries
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
from paddle_billing.Exceptions.FieldError import FieldError
|
|
2
2
|
|
|
3
|
+
from requests import HTTPError
|
|
3
4
|
|
|
4
|
-
class ApiError(
|
|
5
|
-
def __init__(self, error_type, error_code, detail, docs_url, *field_errors):
|
|
5
|
+
class ApiError(HTTPError):
|
|
6
|
+
def __init__(self, response, error_type, error_code, detail, docs_url, *field_errors):
|
|
7
|
+
super().__init__(detail, response=response)
|
|
6
8
|
self.error_type = error_type
|
|
7
9
|
self.error_code = error_code
|
|
8
10
|
self.detail = detail
|
|
9
11
|
self.docs_url = docs_url
|
|
10
12
|
self.field_errors = field_errors
|
|
11
|
-
super().__init__(self.detail)
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def __repr__(self):
|
|
15
|
-
return (f"ApiError(
|
|
16
|
+
return (f"ApiError(error_type='{self.error_type}', error_code='{self.error_code}', detail='{self.detail}', "
|
|
16
17
|
f"docs_url='{self.docs_url}', field_errors={self.field_errors})")
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@classmethod
|
|
20
|
-
def from_error_data(cls, error):
|
|
21
|
+
def from_error_data(cls, response, error):
|
|
21
22
|
field_errors = [FieldError(fe['field'], fe['message']) for fe in error.get('errors', [])]
|
|
22
23
|
return cls(
|
|
24
|
+
response,
|
|
23
25
|
error['type'],
|
|
24
26
|
error['code'],
|
|
25
27
|
error['detail'],
|
paddle_billing/ResponseParser.py
CHANGED
|
@@ -9,6 +9,8 @@ from paddle_billing.Exceptions.ApiError import ApiError
|
|
|
9
9
|
class ResponseParser:
|
|
10
10
|
def __init__(self, response: Response):
|
|
11
11
|
self.body = None
|
|
12
|
+
self.error = None
|
|
13
|
+
self.response = response
|
|
12
14
|
|
|
13
15
|
if not hasattr(response, 'text'):
|
|
14
16
|
return
|
|
@@ -19,13 +21,17 @@ class ResponseParser:
|
|
|
19
21
|
self.body = None
|
|
20
22
|
|
|
21
23
|
if self.body and 'error' in self.body:
|
|
22
|
-
self.parse_errors()
|
|
24
|
+
self.error = self.parse_errors()
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def get_data(self) -> list | dict:
|
|
26
28
|
return self.body.get('data', []) if self.body else []
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def get_error(self) -> ApiError | None:
|
|
32
|
+
return self.error
|
|
33
|
+
|
|
34
|
+
|
|
29
35
|
def get_pagination(self) -> Pagination:
|
|
30
36
|
meta = self.body.get('meta', {}) if self.body else {}
|
|
31
37
|
pagination = meta.get('pagination', {}) if meta else {}
|
|
@@ -38,15 +44,15 @@ class ResponseParser:
|
|
|
38
44
|
)
|
|
39
45
|
|
|
40
46
|
|
|
41
|
-
def parse_errors(self):
|
|
47
|
+
def parse_errors(self) -> ApiError | None:
|
|
42
48
|
if not self.body or 'error' not in self.body:
|
|
43
|
-
return
|
|
49
|
+
return None
|
|
44
50
|
|
|
45
51
|
error = self.body['error']
|
|
46
52
|
code = error.get('code', 'shared_error')
|
|
47
53
|
exception_class = self.find_exception_class_from_code(code)
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
return exception_class.from_error_data(self.response, error)
|
|
50
56
|
|
|
51
57
|
|
|
52
58
|
@staticmethod
|
|
@@ -57,7 +63,7 @@ class ResponseParser:
|
|
|
57
63
|
if not resource:
|
|
58
64
|
return ApiError
|
|
59
65
|
|
|
60
|
-
class_name = f"
|
|
66
|
+
class_name = f"paddle_billing.Exceptions.ApiErrors.{resource}ApiError"
|
|
61
67
|
try:
|
|
62
68
|
module = __import__(class_name, fromlist=[''])
|
|
63
69
|
return getattr(module, f'{resource}ApiError')
|
|
@@ -3,7 +3,7 @@ examples/catalog_management.py,sha256=3osNPpYD5Dh3uq3Uf7Be9li806IkLbr8W7iRJMHMJJ
|
|
|
3
3
|
examples/event_polling.py,sha256=XIpNzSfFIF_V72a-Fujal7y43PV4K28CCt0K29goz_Y,2407
|
|
4
4
|
examples/get_products.py,sha256=LqA92ykNCJJmkTQIpnqrun7HaXozh3h0BpQ4S2JnsR0,1858
|
|
5
5
|
examples/logger.py,sha256=mZuSjhKSqUYpAGtvcqmazkU8Eb7jzl_MS2zNUPFNvY0,1065
|
|
6
|
-
paddle_billing/Client.py,sha256=
|
|
6
|
+
paddle_billing/Client.py,sha256=emyh0fXgRezMTAeRQ2hrQuxI7ApPfDKDXpFl_wKEaGY,8692
|
|
7
7
|
paddle_billing/ConditionallyRemoveImportMeta.py,sha256=tyCEj29dCDO3s_28NwBITRzU0ms2o9x0uToDpsufaEo,652
|
|
8
8
|
paddle_billing/EnumStringify.py,sha256=iDvKULZat3jx9A-_PhvoN6CbFGf2pyxrQeYZxqX9n8M,48
|
|
9
9
|
paddle_billing/Environment.py,sha256=IV5eVH131bhNkM9mAGpkziERsSsWPu-xWxGkdCQEb5c,346
|
|
@@ -13,7 +13,7 @@ paddle_billing/HasParameters.py,sha256=EOJNJLI6Yg7PZtyjr4ioEJidOZYOTumeoQcdCxjap
|
|
|
13
13
|
paddle_billing/Options.py,sha256=Lpaw_VaKjMrxCFM7Sf5KnkLVVr_WjREiEmMZKMPKEZ8,237
|
|
14
14
|
paddle_billing/PaddleStrEnum.py,sha256=SSpayqkGn_ZbB1wAiN2Qdqo17GBgTFxB5GbfLiwJuwI,1139
|
|
15
15
|
paddle_billing/RecursivelyRemoveKey.py,sha256=_y1NIa9yC0jaDgzv-rfoCQ4Z4-KvorlurAh4eUp3i6s,474
|
|
16
|
-
paddle_billing/ResponseParser.py,sha256=
|
|
16
|
+
paddle_billing/ResponseParser.py,sha256=V0_hkWUjisMa6EQQ0p489ggSFxk-hgCREWVKdqQqtTM,2097
|
|
17
17
|
paddle_billing/Undefined.py,sha256=NJKVAEGbTBDfnfaAS6Vn_pSHmHDGbIS277gVScuB2A8,26
|
|
18
18
|
paddle_billing/__init__.py,sha256=IZyNx_kWTth8sU76pk34esblJFfzr_DWla_mlv4q9VA,187
|
|
19
19
|
paddle_billing/Entities/Address.py,sha256=MEFULxsE86lTUruNhX5NWM0Mw4gD3r7b2128QdFNWKI,1631
|
|
@@ -194,7 +194,7 @@ paddle_billing/Entities/Transactions/TransactionProration.py,sha256=DQTbnO8R9tVN
|
|
|
194
194
|
paddle_billing/Entities/Transactions/TransactionTimePeriod.py,sha256=wBnOcZ9zRwkIly5WrdvuSo729WJ5zWAOirP8uEeyLpU,449
|
|
195
195
|
paddle_billing/Entities/Transactions/TransactionUpdateTransactionItem.py,sha256=GwfqJUcfoi3auMndxT63uRnm_qE__LWmXH0IgeECnIo,159
|
|
196
196
|
paddle_billing/Entities/Transactions/__init__.py,sha256=EuAhpqyHrcGbVnzbSOMK43L3VNy6cqGK_zTsgz7YvYY,2044
|
|
197
|
-
paddle_billing/Exceptions/ApiError.py,sha256=
|
|
197
|
+
paddle_billing/Exceptions/ApiError.py,sha256=qT4TFN8oi-xWV10qIwQS0a-2avcnJ0x9rmk-uuw_Amk,1044
|
|
198
198
|
paddle_billing/Exceptions/FieldError.py,sha256=ucSg3nDZ2bJCbuOU51ivqGRaRIS07CR_G--iTs3lgBQ,209
|
|
199
199
|
paddle_billing/Exceptions/SdkException.py,sha256=JKKNX6OUCoxwzz1XLV7qGP3ih9f4Nj2mHZOws6IRy7o,40
|
|
200
200
|
paddle_billing/Exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -465,10 +465,13 @@ paddle_billing/Resources/Transactions/Operations/List/Includes.py,sha256=mOSfkRx
|
|
|
465
465
|
paddle_billing/Resources/Transactions/Operations/List/Origin.py,sha256=FedgNN1r7gDHUmqgdzOa3T1MYlWfmMC7bgmHqLkSles,432
|
|
466
466
|
paddle_billing/Resources/Transactions/Operations/List/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
467
467
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
468
|
+
tests/conftest.py,sha256=K_rWFm4jOBGVpoQP9llOwQwyYrXThbF3CBvood_hNRU,127
|
|
468
469
|
tests/Functional/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
470
|
+
tests/Functional/Client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
471
|
+
tests/Functional/Client/test_Client.py,sha256=e4b7wt2hpAVxE4s0gR1qeETYoYvBPakkIiVRmG4jllk,5645
|
|
469
472
|
tests/Functional/Resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
470
473
|
tests/Functional/Resources/Addresses/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
471
|
-
tests/Functional/Resources/Addresses/test_AddressesClient.py,sha256=
|
|
474
|
+
tests/Functional/Resources/Addresses/test_AddressesClient.py,sha256=hnSEvZS6mbkFQkFr52w2OfTWVl6uP5LnAFC0jiXf0bY,16313
|
|
472
475
|
tests/Functional/Resources/Adjustments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
473
476
|
tests/Functional/Resources/Adjustments/test_AdjustmentsClient.py,sha256=TQIT6t5fwxeDziCNPHHA_deb14ws1y6DVeXOlJk_eR0,8869
|
|
474
477
|
tests/Functional/Resources/Businesses/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -508,10 +511,11 @@ tests/Unit/PaddleStrEnum/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
508
511
|
tests/Unit/PaddleStrEnum/test_PaddleStrEnum.py,sha256=OUMMRhyQdMwJQ4xNKZEshvh08USOlCIjkngJylY5JdQ,1879
|
|
509
512
|
tests/Utils/FakeResponse.py,sha256=naT8IgXDtbAxAAj-T3FoJl9Ze4kXb-O-Mf6BarWU6oM,305
|
|
510
513
|
tests/Utils/ReadsFixture.py,sha256=VEIEST29lorMZHA11XYn7aqtEYry2TDUqeWd63IRLbM,1153
|
|
511
|
-
tests/Utils/TestClient.py,sha256=
|
|
514
|
+
tests/Utils/TestClient.py,sha256=fZb0yzgw9Wnn0JGTw8bJyaUnpR3lRz-AL3N_7O42O7k,1381
|
|
515
|
+
tests/Utils/TestLogger.py,sha256=h6PJbVrgxyi_H0FlSoSKwONOqBC35Hdmqp8_TZ5qt_w,557
|
|
512
516
|
tests/Utils/__init__.py,sha256=fkxfGYArVZPD7GQlBEwT6FY052mTD83VnDerzsTK7hA,39
|
|
513
|
-
paddle_python_sdk-0.1.
|
|
514
|
-
paddle_python_sdk-0.1.
|
|
515
|
-
paddle_python_sdk-0.1.
|
|
516
|
-
paddle_python_sdk-0.1.
|
|
517
|
-
paddle_python_sdk-0.1.
|
|
517
|
+
paddle_python_sdk-0.1.3.dist-info/LICENSE,sha256=e5Sn9mzCdpjnqCLxXpGHNKdYB6JlRSTLhN_CD4WSO6w,10770
|
|
518
|
+
paddle_python_sdk-0.1.3.dist-info/METADATA,sha256=nf_AUi0MjFRCT40bYveCrUXDm5Sb1l4SXbqQMsZ3ipg,7247
|
|
519
|
+
paddle_python_sdk-0.1.3.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
|
520
|
+
paddle_python_sdk-0.1.3.dist-info/top_level.txt,sha256=9a07VPng7o5wK8VAXkl6NC30xrNbW9zff_02jHyHi4c,30
|
|
521
|
+
paddle_python_sdk-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from json import loads, dumps
|
|
2
|
+
from pytest import mark, raises
|
|
3
|
+
from urllib.parse import unquote
|
|
4
|
+
|
|
5
|
+
from paddle_billing.Exceptions.ApiError import ApiError
|
|
6
|
+
from paddle_billing.Exceptions.ApiErrors.AddressApiError import AddressApiError
|
|
7
|
+
|
|
8
|
+
from requests.exceptions import RequestException, HTTPError
|
|
9
|
+
|
|
10
|
+
from tests.Utils.TestLogger import test_log_handler, LogHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestClient:
|
|
14
|
+
@mark.parametrize(
|
|
15
|
+
[
|
|
16
|
+
'expected_response_status',
|
|
17
|
+
'expected_reason',
|
|
18
|
+
'expected_response_body',
|
|
19
|
+
'expected_exception',
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
(
|
|
23
|
+
400,
|
|
24
|
+
'Bad Request',
|
|
25
|
+
{
|
|
26
|
+
"error": {
|
|
27
|
+
"type": "request_error",
|
|
28
|
+
"code": "bad_request",
|
|
29
|
+
"detail": "Invalid request",
|
|
30
|
+
"documentation_url": "https://developer.paddle.com/v1/errors/shared/bad_request",
|
|
31
|
+
"errors": [
|
|
32
|
+
{
|
|
33
|
+
"field": "some_field",
|
|
34
|
+
"message": "Some error message"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"meta": {
|
|
39
|
+
"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
ApiError
|
|
43
|
+
),
|
|
44
|
+
(
|
|
45
|
+
404,
|
|
46
|
+
'Not Found',
|
|
47
|
+
{
|
|
48
|
+
"error": {
|
|
49
|
+
"type": "request_error",
|
|
50
|
+
"code": "not_found",
|
|
51
|
+
"detail": "Entity not found",
|
|
52
|
+
"documentation_url": "https://developer.paddle.com/v1/errors/shared/not_found"
|
|
53
|
+
},
|
|
54
|
+
"meta": {
|
|
55
|
+
"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
ApiError
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
400,
|
|
62
|
+
'Bad Request',
|
|
63
|
+
{
|
|
64
|
+
"error": {
|
|
65
|
+
"type": "request_error",
|
|
66
|
+
"code": "address_location_not_allowed",
|
|
67
|
+
"detail": "Cannot create address with an unsupported location",
|
|
68
|
+
"documentation_url": "https://developer.paddle.com/errors/addresses/address_location_not_allowed"
|
|
69
|
+
},
|
|
70
|
+
"meta": {
|
|
71
|
+
"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
AddressApiError
|
|
75
|
+
),
|
|
76
|
+
],
|
|
77
|
+
ids=[
|
|
78
|
+
"Returns bad_request response",
|
|
79
|
+
"Returns not_found response",
|
|
80
|
+
"Returns address_location_not_allowed response",
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
def test_post_raw_returns_error_response(
|
|
84
|
+
self,
|
|
85
|
+
test_client,
|
|
86
|
+
test_log_handler: LogHandler,
|
|
87
|
+
mock_requests,
|
|
88
|
+
expected_response_status,
|
|
89
|
+
expected_reason,
|
|
90
|
+
expected_response_body,
|
|
91
|
+
expected_exception
|
|
92
|
+
):
|
|
93
|
+
expected_request_url = f"{test_client.base_url}/some/url"
|
|
94
|
+
expected_request_body = { "some_property": "some value" }
|
|
95
|
+
mock_requests.post(expected_request_url, status_code=expected_response_status, text=dumps(expected_response_body), reason=expected_reason)
|
|
96
|
+
|
|
97
|
+
with raises(expected_exception) as exception_info:
|
|
98
|
+
test_client.client.post_raw(expected_request_url, expected_request_body)
|
|
99
|
+
|
|
100
|
+
request_json = test_client.client.payload
|
|
101
|
+
last_request = mock_requests.last_request
|
|
102
|
+
api_error = exception_info.value
|
|
103
|
+
|
|
104
|
+
assert unquote(last_request.url) == expected_request_url, \
|
|
105
|
+
"The URL does not match the expected URL, verify the query string is correct"
|
|
106
|
+
|
|
107
|
+
assert loads(request_json) == expected_request_body, \
|
|
108
|
+
"The request JSON doesn't match the expected fixture JSON"
|
|
109
|
+
|
|
110
|
+
assert isinstance(api_error, ApiError)
|
|
111
|
+
assert isinstance(api_error, HTTPError)
|
|
112
|
+
assert isinstance(api_error, RequestException)
|
|
113
|
+
assert api_error.response.status_code == expected_response_status, 'Unexpected status code'
|
|
114
|
+
assert api_error.error_type == expected_response_body['error']['type']
|
|
115
|
+
assert api_error.error_code == expected_response_body['error']['code']
|
|
116
|
+
assert api_error.detail == expected_response_body['error']['detail']
|
|
117
|
+
assert api_error.docs_url == expected_response_body['error']['documentation_url']
|
|
118
|
+
|
|
119
|
+
if 'errors' in expected_response_body['error']:
|
|
120
|
+
assert len(api_error.field_errors) == len(expected_response_body['error']['errors'])
|
|
121
|
+
|
|
122
|
+
for i, expected_field_error in enumerate(expected_response_body['error']['errors']):
|
|
123
|
+
assert api_error.field_errors[i].field == expected_field_error['field']
|
|
124
|
+
assert api_error.field_errors[i].error == expected_field_error['message']
|
|
125
|
+
else:
|
|
126
|
+
assert len(api_error.field_errors) == 0
|
|
127
|
+
|
|
128
|
+
assert str(api_error) == expected_response_body['error']['detail']
|
|
129
|
+
assert repr(api_error) == ("ApiError("
|
|
130
|
+
f"error_type='{api_error.error_type}', "
|
|
131
|
+
f"error_code='{api_error.error_code}', "
|
|
132
|
+
f"detail='{api_error.detail}', "
|
|
133
|
+
f"docs_url='{api_error.docs_url}', "
|
|
134
|
+
f"field_errors={api_error.field_errors}"
|
|
135
|
+
")")
|
|
136
|
+
|
|
137
|
+
assert test_log_handler.get_logs()[0].message == \
|
|
138
|
+
f"Request failed: {expected_response_status} Client Error: {expected_reason} for url: {expected_request_url}. {api_error.detail}"
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
from json import loads
|
|
2
|
-
from pytest import mark
|
|
1
|
+
from json import loads, dumps
|
|
2
|
+
from pytest import mark, raises
|
|
3
3
|
from urllib.parse import unquote
|
|
4
4
|
|
|
5
5
|
from paddle_billing.Entities.Address import Address
|
|
6
6
|
from paddle_billing.Entities.Collections import AddressCollection
|
|
7
7
|
from paddle_billing.Entities.Shared import CountryCode, CustomData, Status
|
|
8
|
+
from paddle_billing.Exceptions.ApiError import ApiError
|
|
8
9
|
|
|
9
10
|
from paddle_billing.Resources.Addresses.Operations import CreateAddress, ListAddresses, UpdateAddress
|
|
10
11
|
from paddle_billing.Resources.Shared.Operations import Pager
|
|
11
12
|
|
|
13
|
+
from requests.exceptions import RequestException, HTTPError
|
|
14
|
+
|
|
12
15
|
from tests.Utils.TestClient import mock_requests, test_client
|
|
13
16
|
from tests.Utils.ReadsFixture import ReadsFixtures
|
|
14
17
|
|
|
@@ -78,6 +81,100 @@ class TestAddressesClient:
|
|
|
78
81
|
"The response JSON doesn't match the expected fixture JSON"
|
|
79
82
|
|
|
80
83
|
|
|
84
|
+
@mark.parametrize(
|
|
85
|
+
[
|
|
86
|
+
'customer_id',
|
|
87
|
+
'operation',
|
|
88
|
+
'expected_request_body',
|
|
89
|
+
'expected_response_status',
|
|
90
|
+
'expected_reason',
|
|
91
|
+
'expected_response_body',
|
|
92
|
+
'expected_url_path',
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
(
|
|
96
|
+
'ctm_01h844p3h41s12zs5mn4axja51',
|
|
97
|
+
CreateAddress(CountryCode.AG),
|
|
98
|
+
ReadsFixtures.read_raw_json_fixture('request/create_basic'),
|
|
99
|
+
400,
|
|
100
|
+
'Bad Request',
|
|
101
|
+
{
|
|
102
|
+
"error": {
|
|
103
|
+
"type": "request_error",
|
|
104
|
+
"code": "bad_request",
|
|
105
|
+
"detail": "Invalid request",
|
|
106
|
+
"documentation_url": "https://developer.paddle.com/v1/errors/shared/bad_request",
|
|
107
|
+
"errors": [
|
|
108
|
+
{
|
|
109
|
+
"field": "postal_code",
|
|
110
|
+
"message": "US postal_code must be 5 digits only"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"meta": {
|
|
115
|
+
"request_id": "f00bb3ca-399d-4686-889c-50b028f4c911"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
'/customers/ctm_01h844p3h41s12zs5mn4axja51/addresses'
|
|
119
|
+
),
|
|
120
|
+
],
|
|
121
|
+
ids=[
|
|
122
|
+
"Returns 400 response for postal code",
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
def test_create_address_returns_error_response(
|
|
126
|
+
self,
|
|
127
|
+
test_client,
|
|
128
|
+
mock_requests,
|
|
129
|
+
customer_id,
|
|
130
|
+
operation,
|
|
131
|
+
expected_request_body,
|
|
132
|
+
expected_response_status,
|
|
133
|
+
expected_reason,
|
|
134
|
+
expected_response_body,
|
|
135
|
+
expected_url_path,
|
|
136
|
+
):
|
|
137
|
+
expected_url = f"{test_client.base_url}{expected_url_path}"
|
|
138
|
+
mock_requests.post(expected_url, status_code=expected_response_status, text=dumps(expected_response_body), reason=expected_reason)
|
|
139
|
+
|
|
140
|
+
with raises(ApiError) as exception_info:
|
|
141
|
+
test_client.client.addresses.create(customer_id, operation)
|
|
142
|
+
|
|
143
|
+
request_json = test_client.client.payload
|
|
144
|
+
last_request = mock_requests.last_request
|
|
145
|
+
api_error = exception_info.value
|
|
146
|
+
|
|
147
|
+
assert unquote(last_request.url) == expected_url, \
|
|
148
|
+
"The URL does not match the expected URL, verify the query string is correct"
|
|
149
|
+
|
|
150
|
+
assert loads(request_json) == loads(expected_request_body), \
|
|
151
|
+
"The request JSON doesn't match the expected fixture JSON"
|
|
152
|
+
|
|
153
|
+
assert isinstance(api_error, ApiError)
|
|
154
|
+
assert isinstance(api_error, HTTPError)
|
|
155
|
+
assert isinstance(api_error, RequestException)
|
|
156
|
+
assert api_error.response.status_code == expected_response_status, 'Unexpected status code'
|
|
157
|
+
assert api_error.error_type == expected_response_body['error']['type']
|
|
158
|
+
assert api_error.error_code == expected_response_body['error']['code']
|
|
159
|
+
assert api_error.detail == expected_response_body['error']['detail']
|
|
160
|
+
assert api_error.docs_url == expected_response_body['error']['documentation_url']
|
|
161
|
+
|
|
162
|
+
assert len(api_error.field_errors) == len(expected_response_body['error']['errors'])
|
|
163
|
+
|
|
164
|
+
for i, expected_field_error in enumerate(expected_response_body['error']['errors']):
|
|
165
|
+
assert api_error.field_errors[i].field == expected_field_error['field']
|
|
166
|
+
assert api_error.field_errors[i].error == expected_field_error['message']
|
|
167
|
+
|
|
168
|
+
assert str(api_error) == api_error.detail
|
|
169
|
+
assert repr(api_error) == ("ApiError("
|
|
170
|
+
f"error_type='{api_error.error_type}', "
|
|
171
|
+
f"error_code='{api_error.error_code}', "
|
|
172
|
+
f"detail='{api_error.detail}', "
|
|
173
|
+
f"docs_url='{api_error.docs_url}', "
|
|
174
|
+
f"field_errors={api_error.field_errors}"
|
|
175
|
+
")")
|
|
176
|
+
|
|
177
|
+
|
|
81
178
|
@mark.parametrize(
|
|
82
179
|
'customer_id, address_id, operation, expected_request_body, expected_response_status, expected_response_body, expected_url',
|
|
83
180
|
[
|
tests/Utils/TestClient.py
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
from os import getenv
|
|
2
2
|
from pytest import fixture
|
|
3
3
|
from requests_mock import Mocker
|
|
4
|
+
from logging import Logger
|
|
4
5
|
|
|
5
6
|
from paddle_billing import Client, Environment, Options
|
|
6
7
|
|
|
8
|
+
from tests.Utils.TestLogger import test_logger
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class TestClient:
|
|
9
12
|
def __init__(
|
|
10
13
|
self,
|
|
11
14
|
client: Client | None = None,
|
|
12
15
|
api_secret_key: str | None = None,
|
|
13
|
-
environment: Environment = Environment.SANDBOX
|
|
16
|
+
environment: Environment = Environment.SANDBOX,
|
|
17
|
+
logger: Logger | None = None,
|
|
14
18
|
):
|
|
15
19
|
self._environment = environment
|
|
16
20
|
self._base_url = self._environment.base_url
|
|
17
|
-
self._client = client or self.create_client(api_secret_key)
|
|
21
|
+
self._client = client or self.create_client(api_secret_key, logger)
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
@property
|
|
@@ -30,16 +34,17 @@ class TestClient:
|
|
|
30
34
|
return self._environment
|
|
31
35
|
|
|
32
36
|
|
|
33
|
-
def create_client(self, api_secret_key: str | None = None):
|
|
37
|
+
def create_client(self, api_secret_key: str | None = None, logger: Logger | None = None):
|
|
34
38
|
return Client(
|
|
35
39
|
getenv('PADDLE_API_SECRET_KEY') if api_secret_key is None else api_secret_key,
|
|
36
40
|
options = Options(self._environment),
|
|
41
|
+
logger=logger
|
|
37
42
|
)
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
@fixture(autouse=True)
|
|
41
|
-
def test_client():
|
|
42
|
-
return TestClient(environment=Environment.SANDBOX)
|
|
46
|
+
def test_client(test_logger):
|
|
47
|
+
return TestClient(environment=Environment.SANDBOX, logger=test_logger)
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
@fixture
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from pytest import fixture
|
|
2
|
+
from logging import getLogger, Logger, Handler, LogRecord
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LogHandler(Handler):
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__()
|
|
8
|
+
self._logs = []
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def emit(self, record):
|
|
12
|
+
self._logs.append(record)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_logs(self) -> list[LogRecord]:
|
|
16
|
+
return self._logs
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@fixture
|
|
20
|
+
def test_log_handler() -> LogHandler:
|
|
21
|
+
return LogHandler()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@fixture
|
|
25
|
+
def test_logger(test_log_handler) -> Logger:
|
|
26
|
+
logger = getLogger('paddle_test_logger')
|
|
27
|
+
logger.addHandler(test_log_handler)
|
|
28
|
+
return logger
|
tests/conftest.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|