1
+ from Crypto .PublicKey import RSA
2
+ from Crypto .Cipher import PKCS1_OAEP
3
+ from Crypto .Signature import PKCS1_v1_5
4
+ from Crypto .Hash import SHA256
5
+ from base64 import b64encode
6
+ import requests
7
+ import simplejson
8
+
9
+
10
+ class Client (object ):
11
+
12
+ def __init__ (self , protocol = 'http' , server = 'api.redshelf.com' , port = 80 ):
13
+ self .protocol = protocol
14
+ self .server = server
15
+ self .port = port
16
+ self .version = 'v1'
17
+
18
+ self .crypt = RSACrypto ()
19
+
20
+ self .username = ''
21
+
22
+ ## default endpoints
23
+ ## ------------
24
+
25
+ def index (self ):
26
+ """
27
+ API Index
28
+ GET /
29
+ """
30
+ r = requests .get (self ._get_server ())
31
+ try :
32
+ return r .json ()
33
+ except Exception :
34
+ return {'code' : r .status_code , 'text' : r .text }
35
+
36
+ def repeat (self , data ):
37
+ """
38
+ Repeater (debug & testing)
39
+ POST /repeat/
40
+ """
41
+
42
+ test_data = {'request' : simplejson .dumps (data )}
43
+ r = requests .post (self ._get_endpoint ('repeat' ), test_data , headers = self ._get_signed_headers (data ))
44
+ try :
45
+ return r .json ()
46
+ except Exception :
47
+ return {'code' : r .status_code , 'text' : r .text }
48
+
49
+ def profile (self ):
50
+ """
51
+ Profile information
52
+ POST /profile/
53
+ """
54
+ r = requests .get (url = self ._get_endpoint ('profile' ), headers = self ._get_signed_headers ())
55
+ try :
56
+ return r .json ()
57
+ except Exception :
58
+ return {'code' : r .status_code , 'text' : r .text }
59
+
60
+ ## helpers
61
+ ## ------------
62
+
63
+ def _get_server (self ):
64
+ if self .port :
65
+ return str (self .protocol ) + '://' + str (self .server ) + ':' + str (self .port ) + '/'
66
+ else :
67
+ return str (self .protocol ) + '://' + str (self .server ) + '/'
68
+
69
+ def _get_endpoint (self , ep , id = None , action = None ):
70
+ if id :
71
+ if action :
72
+ return self ._get_server () + str (ep ) + '/' + str (id ) + '/' + str (action ) + '/'
73
+ else :
74
+ return self ._get_server () + str (ep ) + '/' + str (id ) + '/'
75
+ else :
76
+ return self ._get_server () + str (ep ) + '/'
77
+
78
+ def _get_version_endpoint (self , ep , id = None , action = None ):
79
+ if id :
80
+ if action :
81
+ return self ._get_server () + str (self .version ) + '/' + str (ep ) + '/' + str (id ) + '/' + str (action ) + '/'
82
+ else :
83
+ return self ._get_server () + str (self .version ) + '/' + str (ep ) + '/' + str (id ) + '/'
84
+ else :
85
+ return self ._get_server () + str (self .version ) + '/' + str (ep ) + '/'
86
+
87
+ def _get_signed_headers (self , data = None ):
88
+ if not data :
89
+ sig = self .crypt .sign_data (self .username )
90
+ else :
91
+ sig = self .crypt .sign_data (simplejson .dumps (data ))
92
+
93
+ headers = {'signature' : sig , 'api_user' : self .username , 'user' : self .username , 'version' : self .version , 'auth_method' : 'CRYPTO' }
94
+ return headers
95
+
96
+ def _get_request_data (self , data ):
97
+ return {'request' : simplejson .dumps (data )}
98
+
99
+ def set_user (self , name ):
100
+ self .username = name
101
+
102
+ def set_key (self , val ):
103
+ self .crypt .set_private_key (val )
104
+
105
+ def load_key (self , filename ):
106
+ self .crypt .load_private_key (filename )
107
+
108
+
109
+ class RSACrypto (object ):
110
+
111
+ def __init__ (self ):
112
+ self .__public_key = None
113
+ self .__private_key = None
114
+
115
+ def encrypt_RSA (self , message ):
116
+ rsakey = RSA .importKey (self .__public_key )
117
+ rsakey = PKCS1_OAEP .new (rsakey )
118
+ encrypted = rsakey .encrypt (message )
119
+ return encrypted .encode ('base64' )
120
+
121
+ def sign_data (self , data ):
122
+ rsakey = RSA .importKey (self .__private_key )
123
+ signer = PKCS1_v1_5 .new (rsakey )
124
+ digest = SHA256 .new ()
125
+ digest .update (data )
126
+ sign = signer .sign (digest )
127
+ return b64encode (sign )
128
+
129
+ def set_public_key (self , var ):
130
+ self .__public_key = var
131
+
132
+ def load_public_key (self , file ):
133
+ self .__public_key = open (file , "r" ).read ()
134
+
135
+ def set_private_key (self , var ):
136
+ self .__private_key = var
137
+
138
+ def load_private_key (self , file ):
139
+ self .__private_key = open (file , "r" ).read ()
140
+
141
+
142
+ class ClientV1 (Client ):
143
+
144
+ def __init__ (self , protocol = 'http' , server = 'api.redshelf.com' , port = 80 ):
145
+ super (ClientV1 , self ).__init__ (protocol , server , port )
146
+ self .version = 'v1'
147
+
148
+ ## Book endpoints
149
+ ## ------------
150
+
151
+ def book (self , hash_id = None , isbn = None ):
152
+ """
153
+ Book endpoint
154
+ GET /v1/book/<hash_id>/
155
+ GET /v1/book/isbn/<isbn/
156
+
157
+ args: hash_id (str) OR isbn (str)
158
+ """
159
+ if hash_id :
160
+ r = requests .get (url = self ._get_version_endpoint ('book' , hash_id ), headers = self ._get_signed_headers ())
161
+ elif isbn :
162
+ r = requests .get (url = self ._get_version_endpoint ('book' , 'isbn' , isbn ), headers = self ._get_signed_headers ())
163
+ else :
164
+ raise Exception ("Please provide the book hash_id or ISBN field." )
165
+
166
+ try :
167
+ return r .json ()
168
+ except Exception :
169
+ return {'code' : r .status_code , 'text' : r .text }
170
+
171
+ def book_search (self , isbn = None , title = None ):
172
+ """
173
+ Book search endpoint
174
+ POST /v1/book/search/
175
+
176
+ args: isbn (list <str>), title (str)
177
+ """
178
+ payload = {'isbn' : isbn , 'title' : title }
179
+ request_data = self ._get_request_data (payload )
180
+ r = requests .post (url = self ._get_version_endpoint ('book' , 'search' ), data = request_data , headers = self ._get_signed_headers (payload ))
181
+
182
+ try :
183
+ return r .json ()
184
+ except Exception :
185
+ return {'code' : r .status_code , 'text' : r .text }
186
+
187
+ ## User endpoints
188
+ ## ------------
189
+
190
+ def invite_user (self , email = None , first_name = None , last_name = None , label = None ):
191
+ """
192
+ User invite endpoint
193
+ POST /v1/user/invite/
194
+
195
+ args: email (str), first_name (str), last_name (str), label (str) <optional>
196
+
197
+ notes: Create a new RedShelf user and send them an invite email with a generated password. Requires the
198
+ 'invite_user' scope and management permission for the associated white label (if provided).
199
+ """
200
+ payload = {'email' : email , 'first_name' : first_name , 'last_name' : last_name , 'label' : label }
201
+ request_data = self ._get_request_data (payload )
202
+ r = requests .post (url = self ._get_version_endpoint ('user' , 'invite' ), data = request_data , headers = self ._get_signed_headers (payload ))
203
+
204
+ try :
205
+ return r .json ()
206
+ except Exception :
207
+ return {'code' : r .status_code , 'text' : r .text }
208
+
209
+ ## Order / Store endpoints
210
+ ## ------------
211
+
212
+ def create_order (self , username , digital_pricing = [], print_pricing = [], combo_pricing = [], billing_address = {},
213
+ shipping_address = {}, label = None ):
214
+ """
215
+ Order creation endpoint for third-party processed orders
216
+ POST /v1/order/external/
217
+
218
+ args: username (str), digital_pricing (list <pricing_id>), print_pricing (list <print_option_id>) <opt>,
219
+ combo_pricing (list <print_option_id>) <opt>, billing_address (dict), shipping_address (dict), label (str) <opt>
220
+
221
+ notes: This endpoint allows the creation of orders in one step, bypassing the typical checkout system. The
222
+ endpoint should only be used for 'forcing' in orders where the collection of funds and order fulfillment
223
+ process is handled by the integration partner. Requires the 'create_orders' scope and management
224
+ permission for the associated white label (if provided).
225
+ """
226
+ payload = {'username' : username , 'digital_pricing' : digital_pricing , 'billing_address' : billing_address ,
227
+ 'shipping_address' : shipping_address , 'label' : label }
228
+ request_data = self ._get_request_data (payload )
229
+ r = requests .post (url = self ._get_version_endpoint ('order' , 'external' ), data = request_data , headers = self ._get_signed_headers (payload ))
230
+
231
+ try :
232
+ return r .json ()
233
+ except Exception :
234
+ return {'code' : r .status_code , 'text' : r .text }
235
+
236
+ ## Misc / help endpoints
237
+ ## ------------
238
+
239
+ def describe (self ):
240
+ """
241
+ V1 Describe endpoint
242
+ GET /v1/describe/
243
+ """
244
+ r = requests .get (url = self ._get_version_endpoint ('describe' ), headers = self ._get_signed_headers ())
245
+
246
+ try :
247
+ return r .json ()
248
+ except Exception :
249
+ return {'code' : r .status_code , 'text' : r .text }
250
+
251
+
252
+ ## #################
253
+ ## Support functions
254
+ ## #################
255
+
256
+ def format_address (first_name = None , last_name = None , full_name = None , company_name = None , line_1 = None ,
257
+ line_2 = None , city = None , state = None , postal_code = None , country = None ):
258
+ """
259
+ Helper function for creating API safe addresses.
260
+ """
261
+ addr = {}
262
+
263
+ ## parse single field names
264
+ if full_name and not first_name and not last_name :
265
+ n_list = unicode (full_name ).strip ().split ()
266
+ first_name = n_list [0 ]
267
+ last_name = n_list [len (n_list ) - 1 ]
268
+
269
+ if not postal_code :
270
+ raise ClientException ('Postal code is required.' )
271
+
272
+ if not first_name :
273
+ raise ClientException ('First name is required.' )
274
+
275
+ if not last_name :
276
+ raise ClientException ('Last name is required.' )
277
+
278
+ ## clean data for transit and do some basic validation
279
+ addr .update ({'first_name' : first_name , 'last_name' : last_name })
280
+
281
+ if company_name :
282
+ company_name = unicode (company_name ).strip ()
283
+
284
+ if line_1 :
285
+ line_1 = unicode (line_1 ).strip ()
286
+
287
+ if line_2 :
288
+ line_2 = unicode (line_2 ).strip ()
289
+
290
+ if city :
291
+ city = unicode (city ).strip ()
292
+
293
+ if state :
294
+ state = unicode (state ).strip ()
295
+ if len (state ) > 3 :
296
+ raise ClientException ('State code is not valid.' )
297
+
298
+ if postal_code :
299
+ postal_code = unicode (postal_code ).strip ()
300
+ if len (postal_code ) > 12 :
301
+ raise ClientException ('Postal code is not valid.' )
302
+
303
+ if country :
304
+ country = unicode (country ).strip ()
305
+ if len (country ) > 2 :
306
+ raise ClientException ('Country code should only be two digits.' )
307
+
308
+ addr .update ({'company_name' : company_name , 'line_1' : line_1 , 'line_2' : line_2 , 'city' : city ,
309
+ 'state' : state , 'postal_code' : postal_code , 'country' : country })
310
+ return addr
311
+
312
+
313
+ class ClientException (Exception ):
314
+ pass
0 commit comments