Skip to content

Commit 33f5d47

Browse files
authored
Merge pull request #28 from bunq/27-strictly-typed-response
Add strictly typed responses; fix circular dependencies; improve namespaces
2 parents 59f4c5f + 68e8977 commit 33f5d47

33 files changed

+8999
-6330
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,6 @@ bunq.conf
8989
bunq-test.conf
9090
**/tmp
9191
config.json
92-
tests/connectQr.png
92+
connectQr.png
9393
.DS_Store
9494
bunq_sdk.egg-info

bunq/__init__.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1-
from bunq.sdk.json import registry
1+
from bunq.sdk.json import converter
22

3-
registry.initialize()
3+
4+
def initialize_converter():
5+
"""
6+
:rtype: None
7+
"""
8+
9+
import datetime
10+
11+
from bunq.sdk import client
12+
from bunq.sdk import context
13+
from bunq.sdk.model import core
14+
from bunq.sdk.json import adapters
15+
from bunq.sdk.json import converter
16+
from bunq.sdk.model.generated import object_
17+
18+
converter.register_adapter(core.Installation, adapters.InstallationAdapter)
19+
converter.register_adapter(
20+
core.SessionServer,
21+
adapters.SessionServerAdapter
22+
)
23+
converter.register_adapter(
24+
context.InstallationContext,
25+
adapters.InstallationContextAdapter
26+
)
27+
converter.register_adapter(
28+
context.ApiEnvironmentType,
29+
adapters.ApiEnvironmentTypeAdapter
30+
)
31+
converter.register_adapter(float, adapters.FloatAdapter)
32+
converter.register_adapter(object_.Geolocation, adapters.GeolocationAdapter)
33+
converter.register_adapter(
34+
object_.MonetaryAccountReference,
35+
adapters.MonetaryAccountReferenceAdapter
36+
)
37+
converter.register_adapter(object_.ShareDetail, adapters.ShareDetailAdapter)
38+
converter.register_adapter(datetime.datetime, adapters.DateTimeAdapter)
39+
converter.register_adapter(client.Pagination, adapters.PaginationAdapter)
40+
41+
42+
converter.set_initializer_function(initialize_converter)

bunq/sdk/client.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44

55
import requests
66

7-
from bunq.sdk import context
87
from bunq.sdk import exception
98
from bunq.sdk import security
109
from bunq.sdk.json import converter
1110

1211

1312
class ApiClient(object):
1413
"""
15-
:type _api_context: context.ApiContext
14+
:type _api_context: bunq.sdk.context.ApiContext
1615
"""
1716

1817
# HTTPS type of proxy, the only used at bunq
@@ -372,6 +371,18 @@ def pagination(self):
372371

373372
return self._pagination
374373

374+
@classmethod
375+
def cast_from_bunq_response(cls, bunq_response):
376+
"""
377+
:type bunq_response: BunqResponse
378+
"""
379+
380+
return cls(
381+
bunq_response.value,
382+
bunq_response.headers,
383+
bunq_response.pagination
384+
)
385+
375386

376387
class Pagination(object):
377388
"""

bunq/sdk/context.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import aenum
44
from Cryptodome.PublicKey import RSA
55

6-
from bunq.sdk import model
6+
from bunq.sdk.model import core
77
from bunq.sdk import security
88
from bunq.sdk.json import converter
9-
from bunq.sdk.model import generated
9+
from bunq.sdk.model.generated import endpoint
1010

1111

1212
class ApiEnvironmentType(aenum.AutoNumberEnum):
@@ -95,7 +95,7 @@ def _initialize_installation(self):
9595
"""
9696

9797
private_key_client = security.generate_rsa_private_key()
98-
installation = model.Installation.create(
98+
installation = core.Installation.create(
9999
self,
100100
security.public_key_to_string(private_key_client.publickey())
101101
).value
@@ -119,12 +119,12 @@ def _register_device(self, device_description,
119119
:rtype: None
120120
"""
121121

122-
generated.DeviceServer.create(
122+
endpoint.DeviceServer.create(
123123
self,
124124
{
125-
generated.DeviceServer.FIELD_DESCRIPTION: device_description,
126-
generated.DeviceServer.FIELD_SECRET: self.api_key,
127-
generated.DeviceServer.FIELD_PERMITTED_IPS: permitted_ips,
125+
endpoint.DeviceServer.FIELD_DESCRIPTION: device_description,
126+
endpoint.DeviceServer.FIELD_SECRET: self.api_key,
127+
endpoint.DeviceServer.FIELD_PERMITTED_IPS: permitted_ips,
128128
}
129129
)
130130

@@ -133,7 +133,7 @@ def _initialize_session(self):
133133
:rtype: None
134134
"""
135135

136-
session_server = model.SessionServer.create(self).value
136+
session_server = core.SessionServer.create(self).value
137137
token = session_server.token.token
138138
expiry_time = self._get_expiry_timestamp(session_server)
139139

@@ -142,7 +142,7 @@ def _initialize_session(self):
142142
@classmethod
143143
def _get_expiry_timestamp(cls, session_server):
144144
"""
145-
:type session_server: model.SessionServer
145+
:type session_server: core.SessionServer
146146
147147
:rtype: datetime.datetime
148148
"""
@@ -155,7 +155,7 @@ def _get_expiry_timestamp(cls, session_server):
155155
@classmethod
156156
def _get_session_timeout_seconds(cls, session_server):
157157
"""
158-
:type session_server: model.SessionServer
158+
:type session_server: core.SessionServer
159159
160160
:rtype: int
161161
"""
@@ -210,7 +210,7 @@ def _delete_session(self):
210210
:rtype: None
211211
"""
212212

213-
generated.Session.delete(self, self._SESSION_ID_DUMMY)
213+
endpoint.Session.delete(self, self._SESSION_ID_DUMMY)
214214

215215
@property
216216
def environment_type(self):
@@ -300,7 +300,7 @@ def restore(cls, path=None):
300300

301301
with open(path, cls._FILE_MODE_READ) as file_:
302302
return cls.from_json(file_.read())
303-
303+
304304
@classmethod
305305
def from_json(cls, json_str):
306306
"""

bunq/sdk/json/adapters.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
from bunq.sdk import client
55
from bunq.sdk import context
6-
from bunq.sdk import model
6+
from bunq.sdk.model import core
77
from bunq.sdk import security
88
from bunq.sdk.json import converter
9-
from bunq.sdk.model import generated
9+
from bunq.sdk.model.generated import endpoint
1010
from bunq.sdk.model.generated import object_
1111

1212

@@ -29,25 +29,25 @@ class InstallationAdapter(converter.JsonAdapter):
2929
@classmethod
3030
def deserialize(cls, target_class, array):
3131
"""
32-
:type target_class: model.Installation|type
32+
:type target_class: core.Installation|type
3333
:type array: list
3434
35-
:rtype: model.Installation
35+
:rtype: core.Installation
3636
"""
3737

3838
installation = target_class.__new__(target_class)
3939
server_public_key_wrapped = array[cls._INDEX_SERVER_PUBLIC_KEY]
4040
installation.__dict__ = {
4141
cls._ATTRIBUTE_ID: converter.deserialize(
42-
model.Id,
42+
core.Id,
4343
array[cls._INDEX_ID][cls._FIELD_ID]
4444
),
4545
cls._ATTRIBUTE_TOKEN: converter.deserialize(
46-
model.SessionToken,
46+
core.SessionToken,
4747
array[cls._INDEX_TOKEN][cls._FIELD_TOKEN]
4848
),
4949
cls._ATTRIBUTE_SERVER_PUBLIC_KEY: converter.deserialize(
50-
model.PublicKeyServer,
50+
core.PublicKeyServer,
5151
server_public_key_wrapped[cls._FIELD_SERVER_PUBLIC_KEY]
5252
),
5353
}
@@ -57,7 +57,7 @@ def deserialize(cls, target_class, array):
5757
@classmethod
5858
def serialize(cls, installation):
5959
"""
60-
:type installation: model.Installation
60+
:type installation: core.Installation
6161
6262
:rtype: list
6363
"""
@@ -98,20 +98,20 @@ class SessionServerAdapter(converter.JsonAdapter):
9898
@classmethod
9999
def deserialize(cls, target_class, array):
100100
"""
101-
:type target_class: model.SessionServer|type
101+
:type target_class: core.SessionServer|type
102102
:type array: list
103103
104-
:rtype: model.SessionServer
104+
:rtype: core.SessionServer
105105
"""
106106

107107
session_server = target_class.__new__(target_class)
108108
session_server.__dict__ = {
109109
cls._ATTRIBUTE_ID: converter.deserialize(
110-
model.Id,
110+
core.Id,
111111
array[cls._INDEX_ID][cls._FIELD_ID]
112112
),
113113
cls._ATTRIBUTE_TOKEN: converter.deserialize(
114-
model.SessionToken,
114+
core.SessionToken,
115115
array[cls._INDEX_TOKEN][cls._FIELD_TOKEN]
116116
),
117117
cls._ATTRIBUTE_USER_COMPANY: None,
@@ -123,13 +123,13 @@ def deserialize(cls, target_class, array):
123123
if cls._FIELD_USER_COMPANY in user_dict_wrapped:
124124
session_server.__dict__[cls._ATTRIBUTE_USER_COMPANY] = \
125125
converter.deserialize(
126-
generated.UserCompany,
126+
endpoint.UserCompany,
127127
user_dict_wrapped[cls._FIELD_USER_COMPANY]
128128
)
129129
elif cls._FIELD_USER_PERSON in user_dict_wrapped:
130130
session_server.__dict__[cls._ATTRIBUTE_USER_PERSON] = \
131131
converter.deserialize(
132-
generated.UserPerson,
132+
endpoint.UserPerson,
133133
user_dict_wrapped[cls._FIELD_USER_PERSON]
134134
)
135135

@@ -138,7 +138,7 @@ def deserialize(cls, target_class, array):
138138
@classmethod
139139
def serialize(cls, session_server):
140140
"""
141-
:type session_server: model.SessionServer
141+
:type session_server: core.SessionServer
142142
143143
:rtype: list
144144
"""
@@ -545,4 +545,4 @@ def serialize(cls, pagination):
545545

546546
_ = pagination
547547

548-
raise NotImplementedError()
548+
raise NotImplementedError()

bunq/sdk/json/converter.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ class JsonAdapter(object):
1818
_custom_serializers = {}
1919
_custom_deserializers = {}
2020

21+
# Initializer
22+
_initializer = None
23+
2124
# Warning for when a key from raw object is unknown
2225
_WARNING_KEY_UNKNOWN = '[bunq SDK beta] Key "{}" in "{}" is unknown.'
2326

@@ -29,7 +32,8 @@ class JsonAdapter(object):
2932
_PREFIX_KEY_PROTECTED = '_'
3033

3134
# Constants to fetch param types from the docstrings
32-
_TEMPLATE_PATTERN_PARAM_TYPES = ':type (_?{}): ([\w.]+)(?:\[([\w.]+)\])?'
35+
_TEMPLATE_PATTERN_PARAM_TYPES = \
36+
':type (_?{}):[\s\n\r]+([\w.]+)(?:\[([\w.]+)\])?'
3337
_PATTERN_PARAM_NAME_TYPED_ANY = ':type (\w+):'
3438
_SUBMATCH_INDEX_NAME = 1
3539
_SUBMATCH_INDEX_TYPE_MAIN = 2
@@ -45,6 +49,14 @@ class JsonAdapter(object):
4549
# List of byte-array type names
4650
_TYPE_NAMES_BYTES = {'bytes', 'unicode'}
4751

52+
@classmethod
53+
def set_initializer(cls, initializer):
54+
"""
55+
:type initializer: Generator[bool, None, None]
56+
"""
57+
58+
cls._initializer = initializer
59+
4860
@classmethod
4961
def register_custom_adapter(cls, target_class, adapter):
5062
"""
@@ -105,13 +117,22 @@ def deserialize(cls, cls_target, obj_raw):
105117
:rtype: T
106118
"""
107119

120+
cls._initialize()
108121
deserializer = cls._get_deserializer(cls_target)
109122

110123
if deserializer == cls:
111124
return cls._deserialize_default(cls_target, obj_raw)
112125
else:
113126
return deserializer.deserialize(cls_target, obj_raw)
114127

128+
@classmethod
129+
def _initialize(cls):
130+
"""
131+
:rtype: None
132+
"""
133+
134+
next(cls._initializer, None)
135+
115136
@classmethod
116137
def _deserialize_default(cls, cls_target, obj_raw):
117138
"""
@@ -404,6 +425,7 @@ def serialize(cls, obj):
404425
:rtype: int|str|bool|list|dict
405426
"""
406427

428+
cls._initialize()
407429
serializer = cls._get_serializer(type(obj))
408430

409431
if serializer == cls:
@@ -581,6 +603,30 @@ def types(self):
581603
return self._types
582604

583605

606+
def set_initializer_function(initializer_function):
607+
"""
608+
:type initializer_function: callable
609+
"""
610+
611+
JsonAdapter.set_initializer(create_initializer(initializer_function))
612+
613+
614+
def create_initializer(initializer_function):
615+
"""
616+
:type initializer_function: callable
617+
618+
:rtype: bool
619+
"""
620+
621+
is_disposed = False
622+
623+
if not is_disposed:
624+
initializer_function()
625+
is_disposed = True
626+
627+
yield is_disposed
628+
629+
584630
def register_adapter(target_class, adapter):
585631
"""
586632
:type target_class: type

0 commit comments

Comments
 (0)