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 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.2',
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(Exception):
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(type='{self.error_type}', error_code='{self.error_code}', detail='{self.detail}', "
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'],
@@ -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
- raise exception_class(error)
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"api_error.{resource}ApiError"
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')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: paddle-python-sdk
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Paddle's Python SDK for Paddle Billing
5
5
  Home-page: https://developer.paddle.com/api-reference/overview
6
6
  Author: Paddle and contributors
@@ -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=faWJ9xEnZEhJjr3_lhoDIHzKooMMi92U2_1MAloX-yg,8317
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=NEuuF5FAPJ_L87nTbM1pq2IEtjAe2cudUlGrdbqq18o,1871
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=pCtnJRNPNybprR-qumCrkEszV1b7y0RUc8wH64kTdQU,951
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=UimpNZ4A7Y8FR96g1FF4tUDloKM528njNrsiKOngwOg,12380
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=Y13roXRdkDp7r73O1p030deWZNeDhDxHtkvKxNG935M,1158
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.2.dist-info/LICENSE,sha256=e5Sn9mzCdpjnqCLxXpGHNKdYB6JlRSTLhN_CD4WSO6w,10770
514
- paddle_python_sdk-0.1.2.dist-info/METADATA,sha256=aq5Uof1fMhvGWGF78S7agNTcpPf513VDdDe3LEnZCds,7247
515
- paddle_python_sdk-0.1.2.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
516
- paddle_python_sdk-0.1.2.dist-info/top_level.txt,sha256=9a07VPng7o5wK8VAXkl6NC30xrNbW9zff_02jHyHi4c,30
517
- paddle_python_sdk-0.1.2.dist-info/RECORD,,
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
@@ -0,0 +1,2 @@
1
+ from tests.Utils.TestClient import mock_requests, test_client
2
+ from tests.Utils.TestLogger import test_log_handler, test_logger