amazon-pay-v2 0.0.12__tar.gz
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.
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: amazon-pay-v2
|
|
3
|
+
Version: 0.0.12
|
|
4
|
+
Project-URL: Repository, https://github.com/Hatraco-GmbH/amazon-pay-v2
|
|
5
|
+
Project-URL: Homepage, https://github.com/Hatraco-GmbH/amazon-pay-v2
|
|
6
|
+
Author-email: Hatraco GmbH <webdev@hatraco.de>
|
|
7
|
+
Requires-Python: >=3.7
|
|
8
|
+
Requires-Dist: cryptography
|
|
9
|
+
Requires-Dist: requests
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Amazon Pay V2 API
|
|
13
|
+
A simple implementation of the amazon pay api for python.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
```python
|
|
17
|
+
# environment = "sandbox"
|
|
18
|
+
environment = "live"
|
|
19
|
+
api = AmazonPayAPIV2("/certs/amazon_pay/amazon-pay-private-key.cert", "SANDBOX-<your-sandbox-or-live-public-key>", region="eu", environment=environment)
|
|
20
|
+
payload = {
|
|
21
|
+
"webCheckoutDetails": {"checkoutReviewReturnUrl": "https://amazon-pay-create-or-update-order", "checkoutMode": "ProcessOrder"},
|
|
22
|
+
"storeId": "<your-store-id>",
|
|
23
|
+
"deliverySpecifications": {"specialRestrictions": ["RestrictPOBoxes"], "addressRestrictions": {"type": "Allowed", "restrictions": {"DE": {}, "CH": {}}}},
|
|
24
|
+
"paymentDetails": {
|
|
25
|
+
"paymentIntent": "AuthorizeWithCapture",
|
|
26
|
+
"chargeAmount": {"amount": 100, "currencyCode": "EUR"},
|
|
27
|
+
"presentmentCurrency": "EUR",
|
|
28
|
+
},
|
|
29
|
+
"merchantMetadata": {"merchantStoreName": "my store"},
|
|
30
|
+
}
|
|
31
|
+
signature = api.generate_button_signature(payload)
|
|
32
|
+
# ...
|
|
33
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Amazon Pay V2 API
|
|
2
|
+
A simple implementation of the amazon pay api for python.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
```python
|
|
6
|
+
# environment = "sandbox"
|
|
7
|
+
environment = "live"
|
|
8
|
+
api = AmazonPayAPIV2("/certs/amazon_pay/amazon-pay-private-key.cert", "SANDBOX-<your-sandbox-or-live-public-key>", region="eu", environment=environment)
|
|
9
|
+
payload = {
|
|
10
|
+
"webCheckoutDetails": {"checkoutReviewReturnUrl": "https://amazon-pay-create-or-update-order", "checkoutMode": "ProcessOrder"},
|
|
11
|
+
"storeId": "<your-store-id>",
|
|
12
|
+
"deliverySpecifications": {"specialRestrictions": ["RestrictPOBoxes"], "addressRestrictions": {"type": "Allowed", "restrictions": {"DE": {}, "CH": {}}}},
|
|
13
|
+
"paymentDetails": {
|
|
14
|
+
"paymentIntent": "AuthorizeWithCapture",
|
|
15
|
+
"chargeAmount": {"amount": 100, "currencyCode": "EUR"},
|
|
16
|
+
"presentmentCurrency": "EUR",
|
|
17
|
+
},
|
|
18
|
+
"merchantMetadata": {"merchantStoreName": "my store"},
|
|
19
|
+
}
|
|
20
|
+
signature = api.generate_button_signature(payload)
|
|
21
|
+
# ...
|
|
22
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import collections
|
|
3
|
+
import datetime
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from urllib import parse
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from cryptography.hazmat.primitives import hashes
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
13
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_idempotency_key():
|
|
18
|
+
return str(uuid.uuid4()).replace('-', '')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AmazonPayAPIV2:
|
|
22
|
+
HASH_ALGORITHM = 'sha256'
|
|
23
|
+
AMAZON_SIGNATURE_ALGORITHM = 'AMZN-PAY-RSASSA-PSS'
|
|
24
|
+
API_VERSION = 'v2'
|
|
25
|
+
USER_AGENT = 'AmazonPayAPIV2/HatracoGmbH/Python3'
|
|
26
|
+
|
|
27
|
+
host = None
|
|
28
|
+
environment = None
|
|
29
|
+
version = None
|
|
30
|
+
|
|
31
|
+
region = None
|
|
32
|
+
|
|
33
|
+
service_hosts = {
|
|
34
|
+
'eu': 'pay-api.amazon.eu',
|
|
35
|
+
'na': 'pay-api.amazon.com',
|
|
36
|
+
'jp': 'pay-api.amazon.jp'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
region_map = {
|
|
40
|
+
'eu': 'eu',
|
|
41
|
+
'de': 'eu',
|
|
42
|
+
'uk': 'eu',
|
|
43
|
+
'us': 'na',
|
|
44
|
+
'na': 'na',
|
|
45
|
+
'jp': 'jp'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def __init__(self, private_key_path, public_key, region=None, environment=None):
|
|
49
|
+
"""
|
|
50
|
+
Args:
|
|
51
|
+
private_key_path: Path to your private key file
|
|
52
|
+
region: Possible values: 'eu', 'de', 'uk', 'us', 'na', 'jp'
|
|
53
|
+
environment: "live" or "sandbox"
|
|
54
|
+
"""
|
|
55
|
+
self.public_key = public_key
|
|
56
|
+
|
|
57
|
+
self.environment = environment if environment else 'live'
|
|
58
|
+
|
|
59
|
+
self.region = region
|
|
60
|
+
|
|
61
|
+
if not region or region not in list(self.region_map.keys()):
|
|
62
|
+
raise Exception(f"The 'region' argument is a required parameter and must have on of the following values: {','.join(list(self.region_map.keys()))}")
|
|
63
|
+
|
|
64
|
+
self.host = self.service_hosts[self.region_map[region]]
|
|
65
|
+
|
|
66
|
+
with open(private_key_path, 'rb') as f:
|
|
67
|
+
self.private_key = load_pem_private_key(f.read(), password=None)
|
|
68
|
+
|
|
69
|
+
def _check_for_critical_data_api(self, url, method, payload):
|
|
70
|
+
payment_critical_data_apis = [f'/live/account-management/{self.API_VERSION}/accounts', f'/sandbox/account-management/{self.API_VERSION}/accounts']
|
|
71
|
+
allowed_methods = ['POST', 'PUT', 'PATCH']
|
|
72
|
+
|
|
73
|
+
for api in payment_critical_data_apis:
|
|
74
|
+
if api in url and method in allowed_methods:
|
|
75
|
+
return ''
|
|
76
|
+
|
|
77
|
+
return payload
|
|
78
|
+
|
|
79
|
+
def _get_post_signed_headers(self, method, url, request_parameters, payload, headers):
|
|
80
|
+
payload = self._check_for_critical_data_api(url, method, payload)
|
|
81
|
+
|
|
82
|
+
pre_signed_headers = {
|
|
83
|
+
'accept': 'application/json',
|
|
84
|
+
'content-type': 'application/json',
|
|
85
|
+
'x-amz-pay-region': self.region,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if headers:
|
|
89
|
+
for key, value in headers.items():
|
|
90
|
+
if key.lower() == 'x-amz-pay-idempotency-key':
|
|
91
|
+
if value:
|
|
92
|
+
pre_signed_headers['x-amz-pay-idempotency-key'] = value
|
|
93
|
+
|
|
94
|
+
ts = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')
|
|
95
|
+
|
|
96
|
+
signature = self._create_signature(method, url, request_parameters, pre_signed_headers, payload, ts)
|
|
97
|
+
|
|
98
|
+
canonical_headers = self._get_canonical_headers(pre_signed_headers)
|
|
99
|
+
canonical_headers['X-Amz-Pay-Date'] = ts
|
|
100
|
+
canonical_headers['X-Amz-Pay-Host'] = self.host
|
|
101
|
+
|
|
102
|
+
signed_headers = f'SignedHeaders={self._get_canonical_header_names(canonical_headers)}, Signature={signature}'
|
|
103
|
+
|
|
104
|
+
final_headers = {
|
|
105
|
+
'accept': pre_signed_headers['accept'],
|
|
106
|
+
'content-type': pre_signed_headers['content-type'],
|
|
107
|
+
'x-amz-pay-host': self.host,
|
|
108
|
+
'x-amz-pay-date': ts,
|
|
109
|
+
'x-amz-pay-region': self.region,
|
|
110
|
+
'authorization': f'{self.AMAZON_SIGNATURE_ALGORITHM} PublicKeyId={self.public_key}, {signed_headers}',
|
|
111
|
+
'user-agent': self.USER_AGENT
|
|
112
|
+
}
|
|
113
|
+
final_headers_sorted = collections.OrderedDict()
|
|
114
|
+
for key in sorted(final_headers.keys()):
|
|
115
|
+
final_headers_sorted[key] = final_headers[key]
|
|
116
|
+
return final_headers_sorted
|
|
117
|
+
|
|
118
|
+
def _call(self, method, endpoint, payload=None, headers=None, query_parameters=None):
|
|
119
|
+
if not endpoint.startswith('/'):
|
|
120
|
+
endpoint = f'/{endpoint}'
|
|
121
|
+
|
|
122
|
+
if payload:
|
|
123
|
+
json_payload = json.dumps(payload, separators=(',', ':'))
|
|
124
|
+
else:
|
|
125
|
+
json_payload = ''
|
|
126
|
+
|
|
127
|
+
url = f'https://{self.host}/{self.environment}/{self.API_VERSION}{endpoint}'
|
|
128
|
+
|
|
129
|
+
if query_parameters:
|
|
130
|
+
if type(query_parameters) != dict:
|
|
131
|
+
raise Exception("query_parameters must be a dictionary; e.g. {'accountId': 'ABCD1234XYZIJK'}")
|
|
132
|
+
request_parameters = query_parameters
|
|
133
|
+
url = f'{url}?{self._get_canonical_query_string(query_parameters)}'
|
|
134
|
+
else:
|
|
135
|
+
request_parameters = {}
|
|
136
|
+
|
|
137
|
+
post_signed_headers = self._get_post_signed_headers(method, url, request_parameters, json_payload, headers)
|
|
138
|
+
|
|
139
|
+
if headers:
|
|
140
|
+
if type(headers) != dict:
|
|
141
|
+
raise Exception("headers must be a dictionary; e.g. {'x-amz-pay-authtoken': 'abcd1234xyzIJK'}")
|
|
142
|
+
for key, value in headers.items():
|
|
143
|
+
post_signed_headers[key] = value
|
|
144
|
+
|
|
145
|
+
if not json_payload:
|
|
146
|
+
json_payload = None
|
|
147
|
+
|
|
148
|
+
response = requests.request(method.lower(), url, data=json_payload, headers=post_signed_headers)
|
|
149
|
+
return response
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _get_canonical_url(url):
|
|
153
|
+
canonical_url_parts = parse.urlparse(url)
|
|
154
|
+
return canonical_url_parts.path
|
|
155
|
+
|
|
156
|
+
def _create_signature(self, method, url, request_parameters, pre_signed_headers, json_payload, time_stamp):
|
|
157
|
+
pre_signed_headers['x-amz-pay-date'] = time_stamp
|
|
158
|
+
pre_signed_headers['x-amz-pay-host'] = self.host
|
|
159
|
+
|
|
160
|
+
hashed_payload = self._hex_and_hash(json_payload).lower()
|
|
161
|
+
canonical_url = self._get_canonical_url(url)
|
|
162
|
+
canonical_query_string = self._get_canonical_query_string(request_parameters)
|
|
163
|
+
canonical_headers = self._get_canonical_header_string(pre_signed_headers)
|
|
164
|
+
signed_headers = ';'.join(sorted([k.lower() for k in pre_signed_headers.keys()]))
|
|
165
|
+
|
|
166
|
+
canonical_request = '\n'.join([method, canonical_url, canonical_query_string, canonical_headers, '', signed_headers, hashed_payload])
|
|
167
|
+
hashed_canonical_request = self._hex_and_hash(canonical_request).lower()
|
|
168
|
+
str_to_sign = f'{self.AMAZON_SIGNATURE_ALGORITHM}\n{hashed_canonical_request}'.encode('utf-8')
|
|
169
|
+
|
|
170
|
+
signed_canonical_request = self._rsa_sign(str_to_sign)
|
|
171
|
+
|
|
172
|
+
if not signed_canonical_request:
|
|
173
|
+
raise Exception("Unable to sign your request in _create_signature. Is the private key correct?")
|
|
174
|
+
|
|
175
|
+
signature = base64.b64encode(signed_canonical_request).decode('utf-8')
|
|
176
|
+
return signature
|
|
177
|
+
|
|
178
|
+
def _rsa_sign(self, message):
|
|
179
|
+
signature = self.private_key.sign(
|
|
180
|
+
data=message,
|
|
181
|
+
padding=padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=20),
|
|
182
|
+
algorithm=hashes.SHA256()
|
|
183
|
+
)
|
|
184
|
+
return signature
|
|
185
|
+
|
|
186
|
+
def generate_button_signature(self, json_payload):
|
|
187
|
+
hashed_payload = self._hex_and_hash(json_payload)
|
|
188
|
+
|
|
189
|
+
hashed_and_salted_payload = f"{self.AMAZON_SIGNATURE_ALGORITHM}\n{hashed_payload}"
|
|
190
|
+
hashed_and_salted_payload = hashed_and_salted_payload.encode('utf-8')
|
|
191
|
+
|
|
192
|
+
signature = self._rsa_sign(hashed_and_salted_payload)
|
|
193
|
+
|
|
194
|
+
if not signature:
|
|
195
|
+
raise Exception("Unable to sign your payload in generate_button_signature. Is the private key correct?")
|
|
196
|
+
|
|
197
|
+
base64_encoded_signature = base64.b64encode(signature)
|
|
198
|
+
return base64_encoded_signature.decode('utf-8')
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _hex_and_hash(data):
|
|
202
|
+
try:
|
|
203
|
+
data = data.encode('utf-8')
|
|
204
|
+
except AttributeError as e:
|
|
205
|
+
print(e)
|
|
206
|
+
|
|
207
|
+
m = hashlib.sha256()
|
|
208
|
+
m.update(data)
|
|
209
|
+
|
|
210
|
+
return m.hexdigest()
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _get_canonical_headers(headers):
|
|
214
|
+
headers = {k.lower().strip(): v.strip() for k, v in headers.items()}
|
|
215
|
+
sorted_keys = sorted(list(headers.keys()))
|
|
216
|
+
sorted_headers = OrderedDict()
|
|
217
|
+
for key in sorted_keys:
|
|
218
|
+
sorted_headers[key] = headers[key]
|
|
219
|
+
return sorted_headers
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _get_canonical_header_names(headers):
|
|
223
|
+
header_keys = sorted([k.lower() for k in (headers.keys())])
|
|
224
|
+
return ';'.join(header_keys)
|
|
225
|
+
|
|
226
|
+
def _get_canonical_header_string(self, headers):
|
|
227
|
+
sorted_headers = self._get_canonical_headers(headers)
|
|
228
|
+
header_data_list = []
|
|
229
|
+
for k, v in sorted_headers.items():
|
|
230
|
+
if isinstance(v, (list, tuple)):
|
|
231
|
+
v = ' '.join(v)
|
|
232
|
+
header_data_list.append(f'{k}:{v}')
|
|
233
|
+
|
|
234
|
+
return '\n'.join(header_data_list)
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _get_canonical_query_string(query_params):
|
|
238
|
+
canonical_query_params = {}
|
|
239
|
+
for key, value in query_params.items():
|
|
240
|
+
if type(value) == list:
|
|
241
|
+
index = 0
|
|
242
|
+
for e in value:
|
|
243
|
+
index += 1
|
|
244
|
+
new_key = quote(f'{key}.{index}')
|
|
245
|
+
canonical_query_params[new_key] = quote(e)
|
|
246
|
+
else:
|
|
247
|
+
canonical_query_params[quote(key)] = quote(value)
|
|
248
|
+
|
|
249
|
+
canonical_query_params = collections.OrderedDict(sorted(canonical_query_params.items()))
|
|
250
|
+
canonical_query_string = '&'.join([f'{k}={v}' for k, v in canonical_query_params.items()])
|
|
251
|
+
return canonical_query_string
|
|
252
|
+
|
|
253
|
+
def get_checkout_session(self, session_id):
|
|
254
|
+
endpoint = f'/checkoutSessions/{session_id}'
|
|
255
|
+
method = 'GET'
|
|
256
|
+
return self._call(method, endpoint)
|
|
257
|
+
|
|
258
|
+
def create_checkout_session(self, payload, idempotency_key=None):
|
|
259
|
+
endpoint = '/checkoutSessions/'
|
|
260
|
+
method = 'POST'
|
|
261
|
+
if idempotency_key:
|
|
262
|
+
headers = {'x-amz-pay-idempotency-key': idempotency_key}
|
|
263
|
+
else:
|
|
264
|
+
headers = None
|
|
265
|
+
return self._call(method, endpoint, payload, headers=headers)
|
|
266
|
+
|
|
267
|
+
def update_checkout_session(self, session_id, payload):
|
|
268
|
+
endpoint = f'/checkoutSessions/{session_id}'
|
|
269
|
+
method = 'PATCH'
|
|
270
|
+
return self._call(method, endpoint, payload)
|
|
271
|
+
|
|
272
|
+
def complete_checkout_session(self, session_id, payload, idempotency_key=None):
|
|
273
|
+
endpoint = f'/checkoutSessions/{session_id}/complete/'
|
|
274
|
+
method = 'POST'
|
|
275
|
+
if idempotency_key:
|
|
276
|
+
headers = {'x-amz-pay-idempotency-key': idempotency_key}
|
|
277
|
+
else:
|
|
278
|
+
headers = None
|
|
279
|
+
return self._call(method, endpoint, payload, headers=headers)
|
|
280
|
+
|
|
281
|
+
def get_refund(self, refund_id):
|
|
282
|
+
endpoint = f'/refunds/{refund_id}'
|
|
283
|
+
method = "GET"
|
|
284
|
+
return self._call(method, endpoint)
|
|
285
|
+
|
|
286
|
+
def create_refund(self, payload, idempotency_key=None):
|
|
287
|
+
endpoint = f'/refunds/'
|
|
288
|
+
method = "POST"
|
|
289
|
+
if idempotency_key:
|
|
290
|
+
headers = {'x-amz-pay-idempotency-key': idempotency_key}
|
|
291
|
+
else:
|
|
292
|
+
headers = None
|
|
293
|
+
return self._call(method, endpoint, payload, headers=headers)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "amazon-pay-v2"
|
|
3
|
+
version = "0.0.12"
|
|
4
|
+
dependencies = [
|
|
5
|
+
"requests",
|
|
6
|
+
"cryptography",
|
|
7
|
+
]
|
|
8
|
+
requires-python = ">=3.7"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Hatraco GmbH", email = "webdev@hatraco.de"},
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
[project.urls]
|
|
14
|
+
Repository = "https://github.com/Hatraco-GmbH/amazon-pay-v2"
|
|
15
|
+
Homepage = "https://github.com/Hatraco-GmbH/amazon-pay-v2"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.sdist]
|
|
22
|
+
exclude = [
|
|
23
|
+
".venv",
|
|
24
|
+
".envrc",
|
|
25
|
+
".github",
|
|
26
|
+
]
|
|
27
|
+
ignore-vcs = true
|