Skip to content

Commit 0a9e998

Browse files
committed
Merge branch 'release-0.10.0'
2 parents 37371e4 + c20694b commit 0a9e998

29 files changed

+936
-599
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# bunq Python SDK
2-
Version 0.9.0 **BETA**
32

43
## Introduction
54
Hi developers!
@@ -183,4 +182,4 @@ Please do not forget to set the `_API_KEY` constant in
183182
## Running Tests
184183

185184
Information regarding the test cases can be found in the [README.md](./tests/README.md)
186-
located in [test](/tests)
185+
located in [test](/tests)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.9.0
1+
0.10.0

bunq/sdk/client.py

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class ApiClient(object):
1919
:type _api_context: context.ApiContext
2020
"""
2121

22+
# HTTPS type of proxy, the only used at bunq
23+
_FIELD_PROXY_HTTPS = 'https'
24+
2225
# Header constants
2326
HEADER_ATTACHMENT_DESCRIPTION = 'X-Bunq-Attachment-Description'
2427
HEADER_CONTENT_TYPE = 'Content-Type'
@@ -32,7 +35,7 @@ class ApiClient(object):
3235
HEADER_AUTHENTICATION = 'X-Bunq-Client-Authentication'
3336

3437
# Default header values
35-
_USER_AGENT_BUNQ = 'bunq-sdk-python/0.9'
38+
_USER_AGENT_BUNQ = 'bunq-sdk-python/0.10.0'
3639
_GEOLOCATION_ZERO = '0 0 0 0 NL'
3740
_LANGUAGE_EN_US = 'en_US'
3841
_REGION_NL_NL = 'nl_NL'
@@ -66,7 +69,7 @@ def post(self, uri_relative, request_bytes, custom_headers):
6669
:type request_bytes: bytes
6770
:type custom_headers: dict[str, str]
6871
69-
:return: requests.Response
72+
:return: BunqResponseRaw
7073
"""
7174

7275
return self._request(
@@ -83,7 +86,7 @@ def _request(self, method, uri_relative, request_bytes, custom_headers):
8386
:type request_bytes: bytes
8487
:type custom_headers: dict[str, str]
8588
86-
:return: requests.Response
89+
:return: BunqResponseRaw
8790
"""
8891

8992
self._api_context.ensure_session_active()
@@ -98,7 +101,8 @@ def _request(self, method, uri_relative, request_bytes, custom_headers):
98101
method,
99102
self._get_uri_full(uri_relative),
100103
data=request_bytes,
101-
headers=all_headers
104+
headers=all_headers,
105+
proxies={self._FIELD_PROXY_HTTPS: self._api_context.proxy_url}
102106
)
103107

104108
self._assert_response_success(response)
@@ -111,7 +115,7 @@ def _request(self, method, uri_relative, request_bytes, custom_headers):
111115
response.headers
112116
)
113117

114-
return response
118+
return self._create_bunq_response_raw(response)
115119

116120
def _get_all_headers(self, method, endpoint, request_bytes, custom_headers):
117121
"""
@@ -184,6 +188,16 @@ def _assert_response_success(self, response):
184188
self._fetch_error_messages(response)
185189
)
186190

191+
@classmethod
192+
def _create_bunq_response_raw(cls, response):
193+
"""
194+
:type response: requests.Response
195+
196+
:rtype: BunqResponseRaw
197+
"""
198+
199+
return BunqResponseRaw(response.content, response.headers)
200+
187201
def _fetch_error_messages(self, response):
188202
"""
189203
:type response: requests.Response
@@ -221,7 +235,7 @@ def put(self, uri_relative, request_bytes, custom_headers):
221235
:type request_bytes: bytes
222236
:type custom_headers: dict[str, str]
223237
224-
:rtype: requests.Response
238+
:rtype: BunqResponseRaw
225239
"""
226240

227241
return self._request(
@@ -236,7 +250,7 @@ def get(self, uri_relative, custom_headers):
236250
:type uri_relative: str
237251
:type custom_headers: dict[str, str]
238252
239-
:rtype: requests.Response
253+
:rtype: BunqResponseRaw
240254
"""
241255

242256
return self._request(
@@ -251,7 +265,7 @@ def delete(self, uri_relative, custom_headers):
251265
:type uri_relative: str
252266
:type custom_headers: dict[str, str]
253267
254-
:rtype: requests.Response
268+
:rtype: BunqResponseRaw
255269
"""
256270

257271
return self._request(
@@ -260,3 +274,67 @@ def delete(self, uri_relative, custom_headers):
260274
self._BYTES_EMPTY,
261275
custom_headers
262276
)
277+
278+
279+
class BunqResponseRaw(object):
280+
"""
281+
:type _body_bytes: bytes
282+
:type _headers: dict[str, str]
283+
"""
284+
285+
def __init__(self, body_bytes, headers):
286+
"""
287+
:type body_bytes: bytes
288+
:type headers: dict[str, str]
289+
"""
290+
291+
self._body_bytes = body_bytes
292+
self._headers = headers
293+
294+
@property
295+
def body_bytes(self):
296+
"""
297+
:rtype: bytes
298+
"""
299+
300+
return self._body_bytes
301+
302+
@property
303+
def headers(self):
304+
"""
305+
:rtype: dict[str, str]
306+
"""
307+
308+
return self._headers
309+
310+
311+
class BunqResponse(object):
312+
"""
313+
:type _value: T
314+
:type _headers: dict[str, str]
315+
"""
316+
317+
def __init__(self, value, headers):
318+
"""
319+
:type value: T
320+
:type headers: dict[str, str]
321+
"""
322+
323+
self._value = value
324+
self._headers = headers
325+
326+
@property
327+
def value(self):
328+
"""
329+
:rtype: T
330+
"""
331+
332+
return self._value
333+
334+
@property
335+
def headers(self):
336+
"""
337+
:rtype: dict[str, str]
338+
"""
339+
340+
return self._headers

bunq/sdk/context.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class ApiContext(object):
4141
:type _api_key: str
4242
:type _session_context: SessionContext
4343
:type _installation_context: InstallationContext
44+
:type _proxy_url: str|None
4445
"""
4546

4647
# File mode for saving and restoring the context
@@ -57,12 +58,13 @@ class ApiContext(object):
5758
_PATH_API_CONTEXT_DEFAULT = 'bunq.conf'
5859

5960
def __init__(self, environment_type, api_key, device_description,
60-
permitted_ips=None):
61+
permitted_ips=None, proxy_url=None):
6162
"""
6263
:type environment_type: ApiEnvironmentType
6364
:type api_key: str
6465
:type device_description: str
6566
:type permitted_ips: list[str]|None
67+
:type proxy_url: str|None
6668
"""
6769

6870
if permitted_ips is None:
@@ -72,6 +74,7 @@ def __init__(self, environment_type, api_key, device_description,
7274
self._api_key = api_key
7375
self._installation_context = None
7476
self._session_context = None
77+
self._proxy_url = proxy_url
7578
self._initialize(device_description, permitted_ips)
7679

7780
def _initialize(self, device_description, permitted_ips):
@@ -95,7 +98,7 @@ def _initialize_installation(self):
9598
installation = model.Installation.create(
9699
self,
97100
security.public_key_to_string(private_key_client.publickey())
98-
)
101+
).value
99102
token = installation.token.token
100103
public_key_server_string = \
101104
installation.server_public_key.server_public_key
@@ -116,14 +119,21 @@ def _register_device(self, device_description,
116119
:rtype: None
117120
"""
118121

119-
model.DeviceServer.create(self, device_description, permitted_ips)
122+
generated.DeviceServer.create(
123+
self,
124+
{
125+
generated.DeviceServer.FIELD_DESCRIPTION: device_description,
126+
generated.DeviceServer.FIELD_SECRET: self.api_key,
127+
generated.DeviceServer.FIELD_PERMITTED_IPS: permitted_ips,
128+
}
129+
)
120130

121131
def _initialize_session(self):
122132
"""
123133
:rtype: None
124134
"""
125135

126-
session_server = model.SessionServer.create(self)
136+
session_server = model.SessionServer.create(self).value
127137
token = session_server.token.token
128138
expiry_time = self._get_expiry_timestamp(session_server)
129139

@@ -247,6 +257,14 @@ def session_context(self):
247257

248258
return self._session_context
249259

260+
@property
261+
def proxy_url(self):
262+
"""
263+
:rtype: str
264+
"""
265+
266+
return self._proxy_url
267+
250268
def save(self, path=None):
251269
"""
252270
:type path: str
@@ -257,8 +275,17 @@ def save(self, path=None):
257275
if path is None:
258276
path = self._PATH_API_CONTEXT_DEFAULT
259277

260-
with open(path, self._FILE_MODE_WRITE) as file:
261-
file.write(converter.class_to_json(self))
278+
with open(path, self._FILE_MODE_WRITE) as file_:
279+
file_.write(self.to_json())
280+
281+
def to_json(self):
282+
"""
283+
Serializes an ApiContext to JSON string.
284+
285+
:rtype: str
286+
"""
287+
288+
return converter.class_to_json(self)
262289

263290
@classmethod
264291
def restore(cls, path=None):
@@ -271,8 +298,25 @@ def restore(cls, path=None):
271298
if path is None:
272299
path = cls._PATH_API_CONTEXT_DEFAULT
273300

274-
with open(path, cls._FILE_MODE_READ) as file:
275-
return converter.json_to_class(ApiContext, file.read())
301+
with open(path, cls._FILE_MODE_READ) as file_:
302+
return cls.from_json(file_.read())
303+
304+
@classmethod
305+
def from_json(cls, json_str):
306+
"""
307+
Creates an ApiContext instance from JSON string.
308+
309+
:type json_str: str
310+
311+
:rtype: ApiContext
312+
"""
313+
314+
return converter.json_to_class(ApiContext, json_str)
315+
316+
def __eq__(self, other):
317+
return (self.token == other.token and
318+
self.api_key == other.api_key and
319+
self.environment_type == other.environment_type)
276320

277321

278322
class InstallationContext(object):

bunq/sdk/json/converter.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class JsonAdapter(object):
2929
_PREFIX_KEY_PROTECTED = '_'
3030

3131
# Constants to fetch param types from the docstrings
32-
_PATTERN_PARAM_TYPES = ':type (_?{}): ([\w.]+)(?:\[([\w.]+)\])?'
32+
_TEMPLATE_PATTERN_PARAM_TYPES = ':type (_?{}): ([\w.]+)(?:\[([\w.]+)\])?'
33+
_PATTERN_PARAM_NAME_TYPED_ANY = ':type (\w+):'
3334
_SUBMATCH_INDEX_NAME = 1
3435
_SUBMATCH_INDEX_TYPE_MAIN = 2
3536
_SUBMATCH_INDEX_TYPE_SUB = 3
@@ -163,7 +164,9 @@ def _deserialize_dict(cls, cls_target, dict_):
163164
"""
164165

165166
instance = cls_target.__new__(cls_target, cls_target)
166-
instance.__dict__ = cls._deserialize_dict_attributes(cls_target, dict_)
167+
dict_deserialized = cls._deserialize_dict_attributes(cls_target, dict_)
168+
instance.__dict__ = cls._fill_default_values(cls_target,
169+
dict_deserialized)
167170

168171
return instance
169172

@@ -192,18 +195,6 @@ def _deserialize_dict_attributes(cls, cls_context, dict_):
192195

193196
return dict_deserialized
194197

195-
@classmethod
196-
def _warn_key_unknown(cls, cls_context, key):
197-
"""
198-
:type cls_context: type
199-
:type key: str
200-
201-
:rtype: None
202-
"""
203-
204-
context_name = cls_context.__name__
205-
warnings.warn(cls._WARNING_KEY_UNKNOWN.format(key, context_name))
206-
207198
@classmethod
208199
def _deserialize_key(cls, key):
209200
"""
@@ -240,7 +231,7 @@ def _fetch_attribute_specs_from_doc(cls, cls_in, attribute_name):
240231
:rtype: ValueSpecs
241232
"""
242233

243-
pattern = cls._PATTERN_PARAM_TYPES.format(attribute_name)
234+
pattern = cls._TEMPLATE_PATTERN_PARAM_TYPES.format(attribute_name)
244235
match = re.search(pattern, cls_in.__doc__)
245236

246237
if match is not None:
@@ -366,6 +357,37 @@ def _deserialize_list(cls, type_item, list_):
366357

367358
return list_deserialized
368359

360+
@classmethod
361+
def _warn_key_unknown(cls, cls_context, key):
362+
"""
363+
:type cls_context: type
364+
:type key: str
365+
366+
:rtype: None
367+
"""
368+
369+
context_name = cls_context.__name__
370+
warnings.warn(cls._WARNING_KEY_UNKNOWN.format(key, context_name))
371+
372+
@classmethod
373+
def _fill_default_values(cls, cls_context, dict_):
374+
"""
375+
:type cls_context: type
376+
:type dict_: dict
377+
378+
:rtype: dict
379+
"""
380+
381+
dict_with_default_values = dict(dict_)
382+
params = re.findall(cls._PATTERN_PARAM_NAME_TYPED_ANY,
383+
cls_context.__doc__)
384+
385+
for param in params:
386+
if param not in dict_with_default_values:
387+
dict_with_default_values[param] = None
388+
389+
return dict_with_default_values
390+
369391
@classmethod
370392
def can_serialize(cls):
371393
"""

0 commit comments

Comments
 (0)