Skip to content

Commit 2b1d4cb

Browse files
committed
SDK-1076: Update SignedRequest and SignedRequestBuilder to use new format of creating a signed request
1 parent 7523485 commit 2b1d4cb

File tree

2 files changed

+195
-60
lines changed

2 files changed

+195
-60
lines changed

yoti_python_sdk/client.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ def __init__(self, sdk_id=None, pem_file_path=None):
3131
pem_file_path_env = environ.get("YOTI_KEY_FILE_PATH", pem_file_path)
3232

3333
self.__crypto = Crypto.read_pem_file(pem_file_path_env)
34-
35-
self.__signed_request = (
36-
SignedRequest.builder()
37-
.with_pem_file(self.__crypto)
38-
.with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT)
39-
.build()
40-
)
4134
self.__endpoint = Endpoint(sdk_id)
4235

4336
def get_activity_details(self, encrypted_request_token):
@@ -119,12 +112,21 @@ def __make_activity_details_request(self, encrypted_request_token):
119112
decrypted_token = self.__crypto.decrypt_token(encrypted_request_token).decode(
120113
"utf-8"
121114
)
122-
query_params = {"appId": self.sdk_id}
123115
path = self.__endpoint.get_activity_details_request_path(
124116
decrypted_token, no_params=True
125117
)
126118

127-
response = self.__signed_request.get(path, query_params)
119+
signed_request = (
120+
SignedRequest.builder()
121+
.with_get()
122+
.with_pem_file(self.__crypto)
123+
.with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT)
124+
.with_endpoint(path)
125+
.with_param("appId", self.sdk_id)
126+
.build()
127+
)
128+
129+
response = signed_request.execute()
128130

129131
self.http_error_handler(
130132
response, {"default": "Unsuccessful Yoti API call: {} {}"}

yoti_python_sdk/http.py

Lines changed: 184 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -25,54 +25,175 @@
2525

2626

2727
class SignedRequest(object):
28-
def __init__(self, base_url, crypto):
29-
self.__base_url = base_url
30-
self.__crypto = crypto
28+
def __init__(self, url, http_method, payload, headers):
29+
self.__url = url
30+
self.__http_method = http_method
31+
self.__payload = payload
32+
self.__headers = headers
3133

32-
def do_request(self, endpoint, http_method, payload=None, query_params=None):
33-
endpoint = endpoint + self.__append_query_params(query_params)
34-
headers = self.__get_request_headers(endpoint, http_method, payload)
35-
url = self.__base_url + endpoint
34+
@property
35+
def url(self):
36+
"""
37+
Returns the URL for the SignedRequest
38+
"""
39+
return self.__url
3640

37-
return requests.request(
38-
method=http_method,
39-
url=url,
40-
data=payload,
41-
headers=headers,
42-
verify=yoti_python_sdk.YOTI_API_VERIFY_SSL,
43-
)
41+
@property
42+
def method(self):
43+
"""
44+
Returns the HTTP method for the SignedRequest
45+
"""
46+
return self.__http_method
47+
48+
@property
49+
def data(self):
50+
"""
51+
Returns the payload data for the SignedRequest
52+
"""
53+
return self.__payload
54+
55+
@property
56+
def headers(self):
57+
"""
58+
Returns the HTTP headers for the SignedRequest
59+
"""
60+
return self.__headers
4461

45-
def post(self, endpoint, payload=None, query_params=None):
46-
return self.do_request(
47-
endpoint, HTTP_POST, payload=payload, query_params=query_params
62+
def prepare(self):
63+
"""
64+
Creates a PreparedRequest object for use in a requests Session
65+
"""
66+
r = requests.Request(
67+
method=self.method, url=self.url, headers=self.headers, data=self.data
4868
)
69+
return r.prepare()
70+
71+
def execute(self):
72+
"""
73+
Creates and sends a PreparedRequest in a requests Session, returning the requests Response object
74+
"""
75+
prepared = self.prepare()
76+
with requests.Session() as s:
77+
return s.send(prepared)
78+
79+
@staticmethod
80+
def builder():
81+
"""
82+
Returns an instance of SignedRequestBuilder
83+
"""
84+
return SignedRequestBuilder()
85+
4986

50-
def get(self, endpoint, query_params=None):
51-
return self.do_request(endpoint, HTTP_GET, query_params=query_params)
87+
class SignedRequestBuilder(object):
88+
def __init__(self):
89+
self.__crypto = None
90+
self.__base_url = None
91+
self.__endpoint = None
92+
self.__http_method = None
93+
self.__params = None
94+
self.__headers = None
95+
self.__payload = None
96+
97+
def with_pem_file(self, pem_file):
98+
"""
99+
Sets the PEM file to be used for signing the request. Can be an instance of yoti_python_sdk.crypto.Crypto
100+
or a path to a PEM file
101+
"""
102+
if isinstance(pem_file, Crypto):
103+
self.__crypto = pem_file
104+
else:
105+
self.__crypto = Crypto.read_pem_file(pem_file)
106+
107+
return self
108+
109+
def with_base_url(self, base_url):
110+
"""
111+
Sets the base URL for the signed request
112+
"""
113+
self.__base_url = base_url
114+
return self
115+
116+
def with_endpoint(self, endpoint):
117+
"""
118+
Sets the endpoint for the signed request
119+
"""
120+
self.__endpoint = endpoint
121+
return self
122+
123+
def with_param(self, name, value):
124+
"""
125+
Sets a query param to be used with the endpoint
126+
"""
127+
if self.__params is None:
128+
self.__params = {}
129+
130+
self.__params[name] = value
131+
return self
132+
133+
def with_header(self, name, value):
134+
"""
135+
Sets a HTTP header to be used in the request
136+
"""
137+
if self.__headers is None:
138+
self.__headers = {}
139+
140+
self.__headers[name] = value
141+
return self
142+
143+
def with_http_method(self, http_method):
144+
"""
145+
Sets the HTTP method to be used in the request
146+
"""
147+
self.__http_method = http_method
148+
return self
149+
150+
def with_post(self):
151+
"""
152+
Sets the HTTP method for a POST request
153+
"""
154+
self.with_http_method(HTTP_POST)
155+
return self
156+
157+
def with_get(self):
158+
"""
159+
Sets the HTTP method for a GET request
160+
"""
161+
self.__http_method = HTTP_GET
162+
return self
52163

53164
def __append_query_params(self, query_params=None):
165+
"""
166+
Appends supplied query params in a dict to default query params.
167+
Returns a url encoded query param string
168+
"""
54169
required = {
55170
"nonce": self.__create_nonce(),
56171
"timestamp": self.__create_timestamp(),
57172
}
58173

59-
query_params = self.__merge_query_params(query_params, required)
174+
query_params = self.__merge_dictionary(query_params, required)
60175
return "?{}".format(urlencode(query_params))
61176

62177
@staticmethod
63-
def __merge_query_params(query_params, required):
64-
if query_params is None:
65-
return required
178+
def __merge_dictionary(a, b):
179+
"""
180+
Merges two dictionaries a and b, with b taking precedence over a
181+
"""
182+
if a is None:
183+
return b
66184

67-
merged = query_params.copy()
68-
merged.update(required)
185+
merged = a.copy()
186+
merged.update(b)
69187
return merged
70188

71189
def __get_request_headers(self, path, http_method, content):
190+
"""
191+
Returns a dictionary of request headers, also using supplied headers from builder. Default headers take precedence.
192+
"""
72193
request = self.__create_request(http_method, path, content)
73194
sdk_version = yoti_python_sdk.__version__
74195

75-
return {
196+
default = {
76197
X_YOTI_AUTH_KEY: self.__crypto.get_public_key(),
77198
X_YOTI_AUTH_DIGEST: self.__crypto.sign(request),
78199
X_YOTI_SDK: SDK_IDENTIFIER,
@@ -81,8 +202,20 @@ def __get_request_headers(self, path, http_method, content):
81202
"Accept": JSON_CONTENT_TYPE,
82203
}
83204

205+
if self.__headers is not None:
206+
return self.__merge_dictionary(self.__headers, default)
207+
208+
return default
209+
84210
@staticmethod
85211
def __create_request(http_method, path, content):
212+
"""
213+
Creates a concatenated string that is used in the X-YOTI-AUTH-DIGEST header
214+
:param http_method:
215+
:param path:
216+
:param content:
217+
:return:
218+
"""
86219
if http_method not in HTTP_SUPPORTED_METHODS:
87220
raise ValueError(
88221
"{} is not in the list of supported methods: {}".format(
@@ -99,6 +232,20 @@ def __create_request(http_method, path, content):
99232

100233
return request
101234

235+
def __validate_request(self):
236+
"""
237+
Validates the request object to ensure the required values
238+
have been supplied.
239+
"""
240+
if self.__base_url is None:
241+
raise ValueError("Base URL must not be None")
242+
if self.__endpoint is None:
243+
raise ValueError("Endpoint must not be None")
244+
if self.__crypto is None:
245+
raise ValueError("PEM file must not be None")
246+
if self.__http_method is None:
247+
raise ValueError("HTTP method must be specified")
248+
102249
@staticmethod
103250
def __create_nonce():
104251
return uuid.uuid4()
@@ -107,30 +254,16 @@ def __create_nonce():
107254
def __create_timestamp():
108255
return int(time.time() * 1000)
109256

110-
@staticmethod
111-
def builder():
112-
return SignedRequestBuilder()
113-
114-
115-
class SignedRequestBuilder(object):
116-
def __init__(self):
117-
self.__crypto = None
118-
self.__base_url = None
119-
120-
def with_pem_file(self, pem_file):
121-
if isinstance(pem_file, Crypto):
122-
self.__crypto = pem_file
123-
else:
124-
self.__crypto = Crypto.read_pem_file(pem_file)
125-
126-
return self
127-
128-
def with_base_url(self, base_url):
129-
self.__base_url = base_url
130-
return self
131-
132257
def build(self):
133-
if self.__crypto is None or self.__base_url is None:
134-
raise ValueError("Crypto and base URL must not be None")
258+
"""
259+
Builds a SignedRequest object with the supplied values
260+
"""
261+
self.__validate_request()
262+
263+
endpoint = self.__endpoint + self.__append_query_params(self.__params)
264+
headers = self.__get_request_headers(
265+
endpoint, self.__http_method, self.__payload
266+
)
267+
url = self.__base_url + endpoint
135268

136-
return SignedRequest(self.__base_url, self.__crypto)
269+
return SignedRequest(url, self.__http_method, self.__payload, headers)

0 commit comments

Comments
 (0)