Skip to content

Commit a366e63

Browse files
committed
Initial version
1 parent 09dc1ca commit a366e63

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ nosetests.xml
3434
.mr.developer.cfg
3535
.project
3636
.pydevproject
37+
38+
.idea

client.py

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
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

Comments
 (0)