mangopay4-python-sdk 4.0.0__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.
mangopay/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ client_id = None
2
+ apikey = None
3
+ api_url = 'https://api.mangopay.com/v2.01/'
4
+ api_sandbox_url = 'https://api.sandbox.mangopay.com/v2.01/'
5
+ temp_dir = None
6
+ api_version = 2.01
7
+ sandbox = True
8
+ uk_header_flag = False
9
+ mtls_cert = None # Path to client certificate file for mTLS
10
+ mtls_private_key = None # Path to client private key file for mTLS
11
+
12
+ package_version = None
13
+ try:
14
+ # try importlib.metadata first
15
+ from importlib.metadata import version
16
+ package_version = version('mangopay4-python-sdk')
17
+ except Exception:
18
+ # fallback for development/editable installs
19
+ try:
20
+ with open('./setup.py', 'r') as f:
21
+ for line in f:
22
+ if line.startswith(' version'):
23
+ package_version = line.split('=')[1].replace("'", "").replace(",", "").replace("\n", "").strip()
24
+ except:
25
+ None
26
+
27
+
28
+ from .api import APIRequest # noqa
29
+ from .utils import memoize
30
+
31
+
32
+ def _get_default_handler():
33
+ return APIRequest()
34
+
35
+ get_default_handler = memoize(_get_default_handler, {}, 0)
mangopay/api.py ADDED
@@ -0,0 +1,390 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import print_function
3
+ from __future__ import unicode_literals
4
+
5
+ import logging
6
+ import platform
7
+ import time
8
+
9
+ import requests
10
+ import six
11
+ from requests.exceptions import ConnectionError, Timeout
12
+
13
+ import mangopay
14
+ from mangopay.auth import AuthorizationTokenManager
15
+ from mangopay.ratelimit import RateLimit
16
+ from .exceptions import APIError, DecodeError
17
+ from .signals import request_finished, request_started, request_error
18
+ from .utils import reraise_as
19
+
20
+ try:
21
+ import urllib.parse as urlrequest
22
+ except ImportError:
23
+ import urllib as urlrequest
24
+
25
+ try:
26
+ import simplejson as json
27
+ except ImportError:
28
+ import json
29
+
30
+
31
+ logger = logging.getLogger('mangopay')
32
+
33
+ rate_limits = None
34
+
35
+
36
+ class APIRequest(object):
37
+
38
+ def __init__(self, client_id=None, apikey=None, api_url=None, api_sandbox_url=None, sandbox=None,
39
+ timeout=30.0, storage_strategy=None, proxies=None, uk_header_flag=False,
40
+ cert=None, key=None):
41
+ global rate_limits
42
+ rate_limits = None
43
+ if (sandbox is None and mangopay.sandbox) or sandbox:
44
+ self.api_url = api_sandbox_url or mangopay.api_sandbox_url
45
+ else:
46
+ self.api_url = api_url or mangopay.api_url
47
+
48
+ self.client_id = client_id or mangopay.client_id
49
+ self.apikey = apikey or mangopay.apikey
50
+ self.auth_manager = AuthorizationTokenManager(self, storage_strategy)
51
+ self.timeout = timeout
52
+ self.proxies = proxies
53
+ self.uk_header_flag = uk_header_flag or mangopay.uk_header_flag
54
+
55
+ self._session = requests.Session()
56
+
57
+ _cert = cert or mangopay.mtls_cert
58
+ _key = key or mangopay.mtls_private_key
59
+ if _cert and _key:
60
+ self._session.cert = (_cert, _key)
61
+ elif _cert:
62
+ self._session.cert = _cert
63
+
64
+ def set_rate_limit(self, rate_limit):
65
+ global rate_limits
66
+ rate_limits = rate_limit
67
+
68
+
69
+ def get_rate_limits(self):
70
+ return rate_limits
71
+
72
+ def request(self, method, url, data=None, idempotency_key=None, oauth_request=False, without_client_id=False,
73
+ is_v3=False, **params):
74
+ return self.custom_request(method, url, data, idempotency_key, oauth_request, True,
75
+ without_client_id, is_v3, **params)
76
+
77
+ def multipart_request(self, method, url, file, file_name, idempotency_key=None, oauth_request=False, is_v3=False):
78
+ return self.custom_multipart_request(method, url, file, file_name, idempotency_key, oauth_request, is_v3)
79
+
80
+ def custom_request(self, method, url, data=None, idempotency_key=None, oauth_request=False,
81
+ is_mangopay_request=False, without_client_id=False, is_v3=False, **params):
82
+ params = params or {}
83
+
84
+ headers = {}
85
+
86
+ if is_mangopay_request:
87
+ headers['User-Agent'] = 'Mangopay-SDK/' + str(
88
+ mangopay.package_version) + ' (Python/' + platform.python_version() + ')'
89
+ if oauth_request:
90
+ headers['Authorization'] = self.auth_manager.basic_token()
91
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
92
+ else:
93
+ headers['Authorization'] = self.auth_manager.get_token()
94
+ headers['Content-Type'] = 'application/json'
95
+
96
+ if idempotency_key:
97
+ headers['Idempotency-Key'] = idempotency_key
98
+
99
+ if self.uk_header_flag:
100
+ logger.warning('uk_header_flag is deprecated and will be removed in future versions')
101
+ headers['x-tenant-id'] = 'uk'
102
+ else:
103
+ if "data_XXX" in params:
104
+ params[str("data")] = params[str("data_XXX")]
105
+ params.__delitem__(str("data_XXX"))
106
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
107
+
108
+ # truncated_data = None
109
+
110
+ encoded_params = urlrequest.urlencode(params)
111
+
112
+ if is_mangopay_request:
113
+ if oauth_request or without_client_id:
114
+ url = self.api_url + url
115
+ else:
116
+ url = self._absolute_url(url, encoded_params)
117
+ else:
118
+ url = '%s?%s' % (url, encoded_params)
119
+
120
+ if is_v3:
121
+ url = url.replace('v2.01', 'V3.0')
122
+
123
+ if data or data == {}:
124
+ # truncated_data = truncatechars(copy.copy(data))
125
+
126
+ data = json.dumps(data, default=lambda x: x.to_api_json())
127
+
128
+ logger.debug('DATA[IN -> %s]\n\t- headers: %s\n\t- content: %s', url, headers, data)
129
+
130
+ ts = time.time()
131
+
132
+ # signal:
133
+ request_started.send(url=url, data=data, headers=headers, method=method)
134
+
135
+ try:
136
+ result = self._session.request(method, url,
137
+ data=data,
138
+ headers=headers,
139
+ timeout=self.timeout,
140
+ proxies=self.proxies)
141
+ except ConnectionError as e:
142
+ msg = '{}'.format(e)
143
+
144
+ if msg:
145
+ msg = '%s: %s' % (type(e).__name__, msg)
146
+ else:
147
+ msg = type(e).__name__
148
+
149
+ reraise_as(APIError(msg))
150
+
151
+ except Timeout as e:
152
+ msg = '{}'.format(e)
153
+
154
+ if msg:
155
+ msg = '%s: %s' % (type(e).__name__, msg)
156
+ else:
157
+ msg = type(e).__name__
158
+
159
+ reraise_as(APIError(msg))
160
+ laps = time.time() - ts
161
+
162
+ # signal:
163
+ request_finished.send(url=url,
164
+ data=data,
165
+ headers=headers,
166
+ method=method,
167
+ result=result,
168
+ laps=laps)
169
+
170
+ logger.debug('DATA[OUT -> %s][%2.3f seconds]\n\t- status_code: %s\n\t- headers: %s\n\t- content: %s',
171
+ url,
172
+ laps,
173
+ result.status_code,
174
+ result.headers,
175
+ result.text if hasattr(result, 'text') else result.content
176
+ )
177
+
178
+ self.read_response_headers(result.headers)
179
+
180
+ if result.status_code not in (requests.codes.ok, requests.codes.not_found,
181
+ requests.codes.created, requests.codes.accepted,
182
+ requests.codes.no_content):
183
+ self._create_apierror(result, url=url, data=data, method=method)
184
+ elif result.status_code == requests.codes.no_content or (result.status_code == requests.codes.ok and result.content == b''):
185
+ return result, None
186
+ else:
187
+ if result.content:
188
+ try:
189
+ content = result.content
190
+
191
+ if six.PY3:
192
+ content = content.decode('utf-8')
193
+
194
+ return result, json.loads(content)
195
+ except ValueError:
196
+ if result.content.startswith(b'data='):
197
+ return result.content
198
+ self._create_decodeerror(result, url=url)
199
+ else:
200
+ self._create_decodeerror(result, url=url)
201
+
202
+ def custom_multipart_request(self, method, url, file, file_name, idempotency_key=None, oauth_request=False, is_v3=False):
203
+ if not isinstance(file, bytes):
204
+ raise TypeError("The 'file' parameter must be of type 'bytes'.")
205
+
206
+ headers = {'User-Agent': 'Mangopay-SDK/' + str(
207
+ mangopay.package_version) + ' (Python/' + platform.python_version() + ')'}
208
+
209
+ if oauth_request:
210
+ headers['Authorization'] = self.auth_manager.basic_token()
211
+ else:
212
+ headers['Authorization'] = self.auth_manager.get_token()
213
+
214
+ if idempotency_key:
215
+ headers['Idempotency-Key'] = idempotency_key
216
+
217
+ if self.uk_header_flag:
218
+ logger.warning('uk_header_flag is deprecated and will be removed in future versions')
219
+ headers['x-tenant-id'] = 'uk'
220
+
221
+ if oauth_request:
222
+ url = self.api_url + url
223
+ else:
224
+ url = self._absolute_url(url, '')
225
+
226
+ if is_v3:
227
+ url = url.replace('v2.01', 'V3.0')
228
+
229
+ files = {'file': (file_name, file)}
230
+
231
+ logger.debug('DATA[IN -> %s]\n\t- headers: %s\n\t- file: %s', url, headers, 'provided' if file else 'none')
232
+ ts = time.time()
233
+
234
+ request_started.send(url=url, data=None, headers=headers, method=method)
235
+
236
+ try:
237
+ result = self._session.request(method, url,
238
+ files=files,
239
+ headers=headers,
240
+ timeout=self.timeout,
241
+ proxies=self.proxies)
242
+ except ConnectionError as e:
243
+ msg = f'{type(e).__name__}: {e}' if str(e) else type(e).__name__
244
+ reraise_as(APIError(msg))
245
+
246
+ except Timeout as e:
247
+ msg = f'{type(e).__name__}: {e}' if str(e) else type(e).__name__
248
+ reraise_as(APIError(msg))
249
+
250
+ laps = time.time() - ts
251
+
252
+ request_finished.send(url=url,
253
+ data=None,
254
+ headers=headers,
255
+ method=method,
256
+ result=result,
257
+ laps=laps)
258
+
259
+ logger.debug('DATA[OUT -> %s][%2.3f seconds]\n\t- status_code: %s\n\t- headers: %s\n\t- content: %s',
260
+ url,
261
+ laps,
262
+ result.status_code,
263
+ result.headers,
264
+ result.text if hasattr(result, 'text') else result.content
265
+ )
266
+
267
+ self.read_response_headers(result.headers)
268
+
269
+ if result.status_code not in (requests.codes.ok, requests.codes.not_found,
270
+ requests.codes.created, requests.codes.accepted,
271
+ requests.codes.no_content):
272
+ self._create_apierror(result, url=url, data=None, method=method)
273
+ elif result.status_code == requests.codes.no_content or (
274
+ result.status_code == requests.codes.ok and result.content == b''):
275
+ return result, None
276
+ else:
277
+ if result.content:
278
+ try:
279
+ content = result.content
280
+ if six.PY3:
281
+ content = content.decode('utf-8')
282
+ return result, json.loads(content)
283
+ except ValueError:
284
+ if result.content.startswith(b'data='):
285
+ return result.content
286
+ self._create_decodeerror(result, url=url)
287
+ else:
288
+ self._create_decodeerror(result, url=url)
289
+
290
+ def read_response_headers(self, headers):
291
+ rate_limit_reset = headers.get('x-ratelimit-reset')
292
+ rate_limit_remaining = headers.get('x-ratelimit-remaining')
293
+ rate_limit_made = headers.get('x-ratelimit')
294
+
295
+ if rate_limit_reset is not None and rate_limit_remaining is not None and rate_limit_made is not None:
296
+ rate_limit_reset = rate_limit_reset.split(',')
297
+ rate_limit_remaining = rate_limit_remaining.split(',')
298
+ rate_limit_made = rate_limit_made.split(',')
299
+
300
+ if len(rate_limit_reset) == len(rate_limit_remaining) and len(rate_limit_reset) == len(rate_limit_made):
301
+ current_time = int(time.time())
302
+ updated_rate_limits = []
303
+
304
+ for i, rlr in enumerate(rate_limit_reset):
305
+ rate_limit = RateLimit()
306
+ number_of_minutes = (int(rate_limit_reset[i].strip()) - current_time) / 60
307
+
308
+ if number_of_minutes <= 15:
309
+ rate_limit.interval_minutes = 15
310
+ elif number_of_minutes <= 30:
311
+ rate_limit.interval_minutes = 30
312
+ elif number_of_minutes <= 60:
313
+ rate_limit.interval_minutes = 60
314
+ elif number_of_minutes <= 60 * 24:
315
+ rate_limit.interval_minutes = 60 * 24
316
+
317
+ rate_limit.reset_time_millis = int(rate_limit_reset[i].strip())
318
+ rate_limit.calls_remaining = int(rate_limit_remaining[i].strip())
319
+ rate_limit.calls_made = int(rate_limit_made[i].strip())
320
+ updated_rate_limits.append(rate_limit)
321
+
322
+ if len(updated_rate_limits) > 0:
323
+ self.set_rate_limit(updated_rate_limits)
324
+
325
+ else:
326
+ logger.debug("Could not set rate limits: headers length should be the same")
327
+ else:
328
+ logger.debug("Could not set rate limits: missing headers")
329
+
330
+ def _absolute_url(self, url, encoded_params):
331
+ pattern = '%s%s%s'
332
+
333
+ if encoded_params:
334
+ pattern = '%s%s?%s'
335
+
336
+ return pattern % (self.api_url, self._construct_api_url(url), encoded_params)
337
+
338
+ def _construct_api_url(self, relative_url):
339
+ return '%s%s' % (self.client_id, relative_url)
340
+
341
+ def _create_apierror(self, result, url=None, data=None, method=None):
342
+ text = result.text if hasattr(result, 'text') else result.content
343
+
344
+ status_code = result.status_code
345
+
346
+ headers = result.headers
347
+
348
+ logger.error('API ERROR: status_code: %s | url: %s | method: %s | data: %r | headers: %s | content: %s',
349
+ status_code,
350
+ url,
351
+ method,
352
+ data,
353
+ headers,
354
+ text,
355
+ )
356
+
357
+ request_error.send(url=url, status_code=status_code, headers=headers)
358
+
359
+ try:
360
+ content = result.json()
361
+ except ValueError:
362
+ content = None
363
+
364
+ raise APIError(text, code=status_code, content=content, headers=headers)
365
+
366
+ def _create_decodeerror(self, result, url=None):
367
+
368
+ text = result.text if hasattr(result, 'text') else result.content
369
+
370
+ status_code = result.status_code
371
+
372
+ headers = result.headers
373
+
374
+ logger.error('DECODE ERROR: status_code: %s | headers: %s | content: %s',
375
+ status_code,
376
+ headers,
377
+ text,
378
+ )
379
+
380
+ request_error.send(url=url, status_code=status_code, headers=headers)
381
+
382
+ try:
383
+ content = result.json()
384
+ except ValueError:
385
+ content = None
386
+
387
+ raise DecodeError(text,
388
+ code=status_code,
389
+ headers=headers,
390
+ content=content)
mangopay/auth.py ADDED
@@ -0,0 +1,130 @@
1
+ import base64
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import stat
6
+ import tempfile
7
+ import time
8
+
9
+ import six
10
+
11
+ import mangopay
12
+ from mangopay.exceptions import AuthenticationError
13
+
14
+
15
+ class StorageStrategyBase(object):
16
+
17
+ def get(self, env_key):
18
+ pass
19
+
20
+ def store(self, token, env_key):
21
+ pass
22
+
23
+
24
+ class StaticStorageStrategy(StorageStrategyBase):
25
+
26
+ _dict = dict()
27
+
28
+ def get(self, env_key):
29
+ return StaticStorageStrategy._dict[env_key]
30
+
31
+ def store(self, token, env_key):
32
+ StaticStorageStrategy._dict[env_key] = token
33
+
34
+
35
+ class FileStorageStrategy(StorageStrategyBase):
36
+
37
+ def __init__(self):
38
+ if mangopay.temp_dir is None:
39
+ mangopay.temp_dir = tempfile.gettempdir()
40
+
41
+ def get(self, env_key):
42
+ cache_path = os.path.join(mangopay.temp_dir, "cached-data." + env_key + ".py")
43
+
44
+ if not os.path.exists(cache_path):
45
+ return None
46
+
47
+ with open(cache_path, 'rb') as f:
48
+ serialized_obj = f.read().decode('UTF-8')
49
+ try:
50
+ cached = json.loads(serialized_obj[1:])
51
+ except:
52
+ cached = None
53
+
54
+ return cached
55
+
56
+ def store(self, token, env_key):
57
+ cache_path = os.path.join(mangopay.temp_dir, "cached-data." + env_key + ".py")
58
+ if token is None:
59
+ return
60
+
61
+ with open(cache_path, 'w') as f:
62
+ os.chmod(cache_path, stat.S_IRUSR | stat.S_IWUSR)
63
+ serialized_obj = "#" + json.dumps(token)
64
+ f.write(serialized_obj)
65
+
66
+
67
+ class AuthorizationTokenManager(object):
68
+
69
+ def __init__(self, handler, storage_strategy):
70
+ self._handler = handler
71
+ self.authorization = ApiAuthorization(handler)
72
+ if storage_strategy:
73
+ self.storage_strategy = storage_strategy
74
+ else:
75
+ self.storage_strategy = StaticStorageStrategy()
76
+
77
+ def basic_token(self):
78
+ return self.authorization.basic_token()
79
+
80
+ def get_token(self):
81
+ try:
82
+ token = self.storage_strategy.get(self.get_evn_key())
83
+ except KeyError:
84
+ token = None
85
+
86
+ if not token or not token['timestamp'] or token['timestamp'] <= time.time():
87
+ auth_result = self.authorization.oauth_token()
88
+ if auth_result[0].status_code == 200:
89
+ token = auth_result[1]
90
+ else:
91
+ return None
92
+ token['timestamp'] = time.time() + (int(token['expires_in']) - 30)
93
+ self.set_token(token)
94
+
95
+ return token['token_type'] + ' ' + token['access_token']
96
+
97
+ def set_token(self, token):
98
+ self.storage_strategy.store(token, self.get_evn_key())
99
+
100
+ def get_evn_key(self):
101
+ return hashlib.md5((self._handler.client_id + self._handler.api_url +
102
+ self._handler.apikey).encode('utf-8')).hexdigest()
103
+
104
+
105
+ class ApiAuthorization(object):
106
+
107
+ def __init__(self, handler):
108
+ self._handler = handler
109
+
110
+ def oauth_token(self):
111
+ result = self._handler.request(method='POST',
112
+ url='oauth/token',
113
+ data={'grant_type': 'client_credentials'},
114
+ oauth_request=True)
115
+ return result
116
+
117
+ def basic_token(self):
118
+ if self._handler.client_id is None or self._handler.apikey is None:
119
+ raise AuthenticationError(
120
+ 'Authentication failed. (Please set your Mangopay API username '
121
+ 'and password using "mangopay.client_id = CLIENT_ID" '
122
+ 'and "mangopay.apikey = APIKEY").')
123
+
124
+ credentials = '%s:%s' % (self._handler.client_id, self._handler.apikey)
125
+ credentials = base64.b64encode(credentials.encode('ascii'))
126
+
127
+ if six.PY3:
128
+ credentials = credentials.decode('utf-8')
129
+
130
+ return 'Basic %s' % credentials