/
base.py
309 lines (269 loc) · 10.7 KB
/
base.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Hive Shopify API
# Copyright (c) 2008-2020 Hive Solutions Lda.
#
# This file is part of Hive Shopify API.
#
# Hive Shopify API is free software: you can redistribute it and/or modify
# it under the terms of the Apache License as published by the Apache
# Foundation, either version 2.0 of the License, or (at your option) any
# later version.
#
# Hive Shopify API is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Apache License for more details.
#
# You should have received a copy of the Apache License along with
# Hive Shopify API. If not, see <http://www.apache.org/licenses/>.
__author__ = "João Magalhães <joamag@hive.pt>"
""" The author(s) of the module """
__version__ = "1.0.0"
""" The version of the module """
__revision__ = "$LastChangedRevision$"
""" The revision number of the module """
__date__ = "$LastChangedDate$"
""" The last change date of the module """
__copyright__ = "Copyright (c) 2008-2020 Hive Solutions Lda."
""" The copyright for the module """
__license__ = "Apache License, Version 2.0"
""" The license for the module """
import hmac
import base64
import hashlib
import appier
from . import cart
from . import inventory_item
from . import shop
from . import order
from . import product
from . import webhook
from . import location
from . import smart_collection
CLIENT_ID = None
""" The default value to be used for the client id
in case no client id is provided to the API client """
CLIENT_SECRET = None
""" The secret value to be used for situations where
no client secret has been provided to the client """
REDIRECT_URL = "http://localhost:8080/oauth"
""" The redirect URL used as default (fallback) value
in case none is provided to the API (client) """
SCOPE = (
"read_products",
"write_products"
)
""" The list of permissions to be used to create the
scope string for the OAuth value """
class API(
appier.API,
cart.CartAPI,
inventory_item.InventoryItemAPI,
shop.ShopAPI,
order.OrderAPI,
product.ProductAPI,
webhook.WebhookAPI,
location.LocationAPI,
smart_collection.SmartCollectionAPI
):
def __init__(self, *args, **kwargs):
appier.API.__init__(self, *args, **kwargs)
self.api_version = appier.conf("SHOPIFY_API_VERSION", None)
self.api_key = appier.conf("SHOPIFY_API_KEY", None)
self.password = appier.conf("SHOPIFY_PASSWORD", None)
self.secret = appier.conf("SHOPIFY_SECRET", None)
self.store_url = appier.conf("SHOPIFY_STORE", None)
self.website_url = appier.conf("SHOPIFY_WEBSITE", None)
self.api_version = kwargs.get("api_version", self.api_version)
self.api_key = kwargs.get("api_key", self.api_key)
self.password = kwargs.get("password", self.password)
self.secret = kwargs.get("secret", self.secret)
self.store_url = kwargs.get("store_url", self.store_url)
self.website_url = kwargs.get("website_url", self.website_url)
self._build_url()
def build(
self,
method,
url,
data = None,
data_j = None,
data_m = None,
headers = None,
params = None,
mime = None,
kwargs = None
):
cookie_l = []
if hasattr(self, "session_id"):
cookie_l.append("_session_id=%s" % self.session_id)
if hasattr(self, "cart"): cookie_l.append("cart=%s" % self.cart)
cookie = ";".join(cookie_l)
if not cookie: return
headers["Cookie"] = cookie
def get_many(self, url, key = None, **kwargs):
page = 1
result = []
while True:
items = self.get(url, page = page, **kwargs)
if key: items = items[key]
if not items: break
result.extend(items)
page += 1
if key: result = {key : result}
return result
def graphql(self, query_data):
url = self.base_url + "admin/api/graphql.json"
contents = self.post(url = url, data = query_data, headers = {
"Content-Type" : "application/graphql"
})
return contents
def verify_request(
self,
request,
field = "hmac",
header = "X-Shopify-Hmac-SHA256"
):
is_param = True if request.get_param(field, None) else False
signature = request.get_param(field, None)
signature = request.get_header(header, signature)
appier.verify(
signature,
message = "No signature found in request",
exception = appier.OperationalError
)
if is_param:
params_l = [(key, param) for key, param in appier.legacy.iteritems(request.get_params()) if not key == field]
params_l.sort()
params_s = appier.legacy.urlencode(params_l)
data = appier.legacy.bytes(params_s, encoding = "utf-8", force = True)
else:
data = request.get_data()
self.verify_signature(signature, data, base_64 = not is_param)
def verify_signature(self, signature, data, key = None, base_64 = True):
key = key if key else self.secret
appier.verify(
key,
message = "No key for signing found",
exception = appier.OperationalError
)
signature_b = appier.legacy.bytes(signature)
key_b = appier.legacy.bytes(key)
if base_64:
_signature = hmac.new(key_b, data, hashlib.sha256).digest()
_signature_b64 = base64.b64encode(_signature)
valid = hmac.compare_digest(_signature_b64, signature_b)
else:
_signature = hmac.new(key_b, data, hashlib.sha256).hexdigest()
_signature_b = appier.legacy.bytes(_signature)
valid = hmac.compare_digest(_signature_b, signature_b)
appier.verify(
valid,
message = "Request signature is not valid",
exception = appier.SecurityError
)
def _build_admin_url(self):
api_version_path = "api/%s/" % self.api_version if self.api_version else ""
return self.base_url + "admin/%s" % api_version_path
def _build_url(self):
if not self.api_key:
raise appier.OperationalError(message = "No API key provided")
if not self.password:
raise appier.OperationalError(message = "No password provided")
if not self.store_url:
raise appier.OperationalError(message = "No store URL provided")
self.base_url = "https://%s:%s@%s/" % (
self.api_key, self.password, self.store_url
)
self.website_url = "http://%s/" % (self.website_url or self.store_url)
self.admin_url = self._build_admin_url()
class OAuthAPI(appier.OAuth2API, API):
def __init__(self, *args, **kwargs):
appier.OAuth2API.__init__(self, *args, **kwargs)
API.__init__(self, *args, **kwargs)
self.client_id = appier.conf("SHOPIFY_ID", CLIENT_ID)
self.secret = appier.conf("SHOPIFY_SECRET", CLIENT_SECRET)
self.redirect_url = appier.conf("SHOPIFY_REDIRECT_URL", REDIRECT_URL)
self.store_url = appier.conf("SHOPIFY_STORE", None)
self.client_id = kwargs.get("client_id", self.client_id)
self.secret = kwargs.get("secret", self.secret)
self.redirect_url = kwargs.get("redirect_url", self.redirect_url)
self.store_url = kwargs.get("store_url", self.store_url)
self.scope = kwargs.get("scope", SCOPE)
self.access_token = kwargs.get("access_token", None)
self.access_mode = kwargs.get("access_mode", None)
self._build_url()
def oauth_authorize(self, state = None):
url = self.base_url + "admin/oauth/authorize"
values = dict(
client_id = self.client_id,
redirect_uri = self.redirect_url,
response_type = "code",
scope = ",".join(self.scope)
)
if self.access_mode: values["grant_options[]"] = self.access_mode
if state: values["state"] = state
data = appier.legacy.urlencode(values)
url = url + "?" + data
return url
def oauth_access(self, code):
url = self.base_url + "admin/oauth/access_token"
contents = self.post(
url,
token = False,
client_id = self.client_id,
client_secret = self.client_secret,
code = code
)
self.access_token = contents["access_token"]
self.trigger("access_token", self.access_token)
return self.access_token
@property
def client_secret(self):
return self.secret
def _build_url(self):
if not self.store_url:
raise appier.OperationalError(message = "No store URL provided")
self.base_url = "https://%s/" % self.store_url
self.admin_url = self._build_admin_url()
def _fetch_many(
self,
url,
item_name = None,
method_count = None,
limit = 50,
bulk_limit = 250,
all = False,
**kwargs
):
# creates the sequence that is going to hold the complete set of
# items to be retrieved from teh remote data source
items = []
# creates a variable to store the identifier of the last item that was
# retrieved, assumes this will allow proper pagination of the items
last_id = None
# sets the initial value of the items remaining to be fetched as the
# limit value, this value will change as the loop continues
item_remaining = limit
# if "all" flag is set to true then sets the the items remaining value
# to the value obtained method count method call
if all:
limit = bulk_limit
item_remaining = method_count()
# keeps fetching items until there isn't any more items to fetch
while item_remaining > 0:
contents = self.get(
url,
limit = limit,
since_id = last_id,
**kwargs
)
items.extend(contents[item_name or "items"])
try:
last_id = items[-1]["id"]
except:
return []
item_remaining -= limit
# returns the final set of items that have been retrieved from the
# remote data source according to the specification
return items