Skip to content

Commit d841824

Browse files
committed
feature/sdk_python#149 ApiContext is now saved and restored correctly.
1 parent b784552 commit d841824

File tree

8 files changed

+149
-64
lines changed

8 files changed

+149
-64
lines changed

bunq/sdk/context/api_context.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,13 @@ def __register_device(self,
145145

146146
def __initialize_session(self) -> None:
147147
from bunq.sdk.model.core.session_server import SessionServer
148-
149148
session_server = SessionServer.create(self).value
150-
self._session_context = SessionContext(session_server)
149+
150+
token = session_server.token
151+
expiry_time = self._get_expiry_timestamp(session_server)
152+
user = session_server.get_user_reference()
153+
154+
self._session_context = SessionContext(token, expiry_time, user)
151155

152156
@classmethod
153157
def _get_expiry_timestamp(cls, session_server: SessionServer) -> datetime.datetime:

bunq/sdk/context/bunq_context.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ def __init__(self) -> None:
1717
@classmethod
1818
def load_api_context(cls, api_context: ApiContext) -> None:
1919
cls._api_context = api_context
20-
cls._user_context = UserContext(api_context.session_context.user_id, api_context.session_context.user)
20+
cls._user_context = UserContext(
21+
api_context.session_context.user_id,
22+
api_context.session_context.get_user_reference()
23+
)
2124
cls._user_context.init_main_monetary_account()
2225

2326
@classmethod

bunq/sdk/context/session_context.py

+83-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1+
from __future__ import annotations
2+
13
import datetime
4+
from typing import Optional
25

3-
from bunq.sdk.model.core.session_server import SessionServer
6+
from bunq.sdk.exception.bunq_exception import BunqException
7+
from bunq.sdk.model.core.bunq_model import BunqModel
8+
from bunq.sdk.model.core.session_token import SessionToken
9+
from bunq.sdk.model.generated.endpoint import UserPerson, UserCompany, UserApiKey, UserPaymentServiceProvider
410

511

612
class SessionContext:
7-
def __init__(self, session_server: SessionServer) -> None:
8-
self._token = session_server.token.token
9-
self._expiry_time = self._get_expiry_timestamp(session_server)
10-
self._user_id = session_server.get_referenced_user().id_
11-
self._user = session_server.get_referenced_user()
13+
"""
14+
:type _token: str
15+
:type _expiry_time: datetime.datetime
16+
:type _user_id: int
17+
:type _user_person: UserPerson|None
18+
:type _user_company: UserCompany|None
19+
:type _user_api_key: UserApiKey|None
20+
:type _user_payment_service_provider: UserPaymentServiceProvider|None
21+
"""
22+
23+
# Error constants
24+
_ERROR_ALL_FIELD_IS_NULL = 'All fields are null'
25+
_ERROR_UNEXPECTED_USER_INSTANCE = '"{}" is unexpected user instance.'
1226

1327
@property
1428
def token(self) -> str:
@@ -23,5 +37,66 @@ def user_id(self) -> int:
2337
return self._user_id
2438

2539
@property
26-
def user(self):
27-
return self._user
40+
def user_person(self) -> Optional[UserPerson]:
41+
return self._user_person
42+
43+
@property
44+
def user_company(self) -> Optional[UserCompany]:
45+
return self._user_company
46+
47+
@property
48+
def user_api_key(self) -> Optional[UserApiKey]:
49+
return self._user_api_key
50+
51+
@property
52+
def user_payment_service_provider(self) -> Optional[UserPaymentServiceProvider]:
53+
return self._user_payment_service_provider
54+
55+
def __init__(self, token: SessionToken, expiry_time: datetime.datetime, user: BunqModel) -> None:
56+
self._user_person = None
57+
self._user_company = None
58+
self._user_api_key = None
59+
self._user_payment_service_provider = None
60+
self._token = token.token
61+
self._expiry_time = expiry_time
62+
self._user_id = self.__get_user_id(user)
63+
self.__set_user(user)
64+
65+
def __get_user_id(self, user: BunqModel) -> int:
66+
if isinstance(user, UserPerson):
67+
return user.id_
68+
69+
if isinstance(user, UserCompany):
70+
return user.id_
71+
72+
if isinstance(user, UserApiKey):
73+
return user.id_
74+
75+
if isinstance(user, UserPaymentServiceProvider):
76+
return user.id_
77+
78+
raise BunqException(self._ERROR_UNEXPECTED_USER_INSTANCE)
79+
80+
def __set_user(self, user: BunqModel):
81+
if isinstance(user, UserPerson):
82+
self._user_person = user
83+
elif isinstance(user, UserCompany):
84+
self._user_company = user
85+
elif isinstance(user, UserApiKey):
86+
self._user_api_key = user
87+
elif isinstance(user, UserPaymentServiceProvider):
88+
self._user_payment_service_provider = user
89+
else:
90+
raise BunqException(self._ERROR_UNEXPECTED_USER_INSTANCE)
91+
92+
def get_user_reference(self) -> BunqModel:
93+
if self.user_person is not None:
94+
return self.user_person
95+
elif self.user_company is not None:
96+
return self.user_company
97+
elif self.user_api_key is not None:
98+
return self.user_api_key
99+
elif self.user_payment_service_provider is not None:
100+
return self.user_payment_service_provider
101+
else:
102+
raise BunqException(self._ERROR_ALL_FIELD_IS_NULL)

bunq/sdk/json/converter.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import typing
88
import warnings
99
from types import ModuleType
10-
from typing import Type, Optional, Callable, Generator, Dict, Match, List, Union, Generic
10+
from typing import Type, Optional, Callable, Generator, Dict, Match, List, Union, Generic, Any
1111

1212
from bunq.sdk.exception.bunq_exception import BunqException
1313
from bunq.sdk.util.type_alias import T, JsonValue
@@ -22,6 +22,7 @@
2222
class JsonAdapter(Generic[T]):
2323
# Error constants
2424
_ERROR_COULD_NOT_FIND_CLASS = 'Could not find class: {}'
25+
_ERROR_MISSING_DOC_COMMENT = 'A doc :type is missing for {} in class {}'
2526

2627
# Maps to store custom serializers and deserializers
2728
_custom_serializers = {}
@@ -41,15 +42,14 @@ class JsonAdapter(Generic[T]):
4142
_PREFIX_KEY_PROTECTED = '_'
4243

4344
# Constants to fetch param types from the docstrings
44-
_TEMPLATE_PATTERN_PARAM_TYPES = ':type (_?{}):[\s\n\r]+([\w.]+)(?:\[([\w.]+)\])?'
45-
_PATTERN_PARAM_NAME_TYPED_ANY = ':type (\w+):'
45+
_TEMPLATE_PATTERN_PARAM_TYPES = ':type (_?{}):[\\s\\n\\r]+([\\w.]+)(?:\\[([\\w.]+)\\])?'
46+
_PATTERN_PARAM_NAME_TYPED_ANY = ':type (\\w+):'
4647
_SUBMATCH_INDEX_NAME = 1
4748
_SUBMATCH_INDEX_TYPE_MAIN = 2
4849
_SUBMATCH_INDEX_TYPE_SUB = 3
4950

5051
# List of builtin type names
51-
_TYPE_NAMES_BUILTIN = {'int', 'bool', 'float', 'str', 'list', 'dict',
52-
'bytes', 'unicode'}
52+
_TYPE_NAMES_BUILTIN = {'int', 'bool', 'float', 'str', 'list', 'dict', 'bytes', 'unicode'}
5353

5454
# Delimiter between modules in class paths
5555
_DELIMITER_MODULE = '.'
@@ -187,7 +187,12 @@ def _fetch_attribute_specs_from_doc(cls,
187187
cls_in: Type[T],
188188
attribute_name: str) -> Optional[ValueSpecs]:
189189
pattern = cls._TEMPLATE_PATTERN_PARAM_TYPES.format(attribute_name)
190-
match = re.search(pattern, cls_in.__doc__)
190+
doc_type = cls_in.__doc__
191+
192+
if doc_type is None:
193+
raise BunqException(cls._ERROR_MISSING_DOC_COMMENT.format(attribute_name, cls_in))
194+
195+
match = re.search(pattern, doc_type)
191196

192197
if match is not None:
193198
return ValueSpecs(

bunq/sdk/json/installation_adapter.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ def serialize(cls, installation: Installation) -> List:
5151
return [
5252
{cls._FIELD_ID: converter.serialize(installation.id_)},
5353
{cls._FIELD_TOKEN: converter.serialize(installation.token)},
54-
{cls._FIELD_SERVER_PUBLIC_KEY: converter.serialize(installation.server_public_key), },
54+
{cls._FIELD_SERVER_PUBLIC_KEY: converter.serialize(installation.server_public_key)},
5555
]

bunq/sdk/model/core/session_server.py

+33-40
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class SessionServer(BunqModel):
2020
:type _user_person: UserPerson|None
2121
:type _user_company: UserCompany|None
2222
:type _user_api_key: UserApiKey|None
23-
:type _user_payment_service_provider:UserPaymentServiceProvider|None
23+
:type _user_payment_service_provider: UserPaymentServiceProvider|None
2424
"""
2525

2626
# Endpoint name.
@@ -32,40 +32,41 @@ class SessionServer(BunqModel):
3232
# Error constants
3333
_ERROR_ALL_FIELD_IS_NULL = 'All fields are null'
3434

35-
def __init__(self) -> None:
36-
self._id_ = None
37-
self._token = None
38-
self._user_person = None
39-
self._user_company = None
40-
self._user_api_key = None
41-
self._user_payment_service_provider = None
42-
4335
@property
44-
def id_(self) -> Id:
36+
def id_(self) -> Optional[Id]:
4537
return self._id_
4638

4739
@property
48-
def token(self) -> SessionToken:
40+
def token(self) -> Optional[SessionToken]:
4941
return self._token
5042

5143
@property
52-
def user_person(self) -> UserPerson:
44+
def user_person(self) -> Optional[UserPerson]:
5345
return self._user_person
5446

5547
@property
56-
def user_company(self) -> UserCompany:
48+
def user_company(self) -> Optional[UserCompany]:
5749
return self._user_company
5850

5951
@property
6052
def user_api_key(self) -> Optional[UserApiKey]:
6153
return self._user_api_key
6254

6355
@property
64-
def user_payment_service_provider(self) -> UserPaymentServiceProvider:
56+
def user_payment_service_provider(self) -> Optional[UserPaymentServiceProvider]:
6557
return self._user_payment_service_provider
6658

59+
def __init__(self) -> None:
60+
self._user_person = None
61+
self._user_company = None
62+
self._user_api_key = None
63+
self._user_payment_service_provider = None
64+
self._token = None
65+
self._id_ = None
66+
6767
@classmethod
6868
def create(cls, api_context: ApiContext) -> BunqResponse[SessionServer]:
69+
cls.__init__(cls)
6970
api_client = ApiClient(api_context)
7071
body_bytes = cls.generate_request_body_bytes(api_context.api_key)
7172
response_raw = api_client.post(cls._ENDPOINT_URL_POST, body_bytes, {})
@@ -79,35 +80,27 @@ def generate_request_body_bytes(cls, secret: str) -> bytes:
7980
def is_all_field_none(self) -> bool:
8081
if self.id_ is not None:
8182
return False
82-
83-
if self.token is not None:
83+
elif self.token is not None:
8484
return False
85-
86-
if self.user_person is not None:
85+
elif self.user_person is not None:
8786
return False
88-
89-
if self.user_company is not None:
87+
elif self.user_company is not None:
9088
return False
91-
92-
if self.user_payment_service_provider is not None:
89+
elif self.user_api_key is not None:
9390
return False
94-
95-
if self.user_api_key is not None:
91+
elif self.user_payment_service_provider is not None:
9692
return False
93+
else:
94+
return True
9795

98-
return True
99-
100-
def get_referenced_user(self) -> BunqModel:
101-
if self._user_person is not None:
102-
return self._user_person
103-
104-
if self._user_company is not None:
105-
return self._user_company
106-
107-
if self._user_payment_service_provider is not None:
108-
return self._user_payment_service_provider
109-
110-
if self._user_api_key is not None:
111-
return self._user_api_key
112-
113-
raise BunqException(self._ERROR_ALL_FIELD_IS_NULL)
96+
def get_user_reference(self) -> BunqModel:
97+
if self.user_person is not None:
98+
return self.user_person
99+
elif self.user_company is not None:
100+
return self.user_company
101+
elif self.user_api_key is not None:
102+
return self.user_api_key
103+
elif self.user_payment_service_provider is not None:
104+
return self.user_payment_service_provider
105+
else:
106+
raise BunqException(self._ERROR_ALL_FIELD_IS_NULL)

bunq/sdk/security/security.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from Cryptodome.PublicKey import RSA
1717
from Cryptodome.PublicKey.RSA import RsaKey
1818
from Cryptodome.Signature import PKCS1_v1_5
19+
from requests.structures import CaseInsensitiveDict
1920

2021
from bunq.sdk.exception.bunq_exception import BunqException
2122

@@ -58,6 +59,7 @@
5859
_HEADER_CLIENT_ENCRYPTION_HMAC = 'X-Bunq-Client-Encryption-Hmac'
5960
_HEADER_SERVER_SIGNATURE = 'X-Bunq-Server-Signature'
6061

62+
6163
def generate_rsa_private_key() -> RsaKey:
6264
return RSA.generate(_RSA_KEY_SIZE)
6365

@@ -177,7 +179,7 @@ def _add_header_client_encryption_hmac(request_bytes: bytes,
177179
def validate_response(public_key_server: RsaKey,
178180
status_code: int,
179181
body_bytes: bytes,
180-
headers: Dict[str, str]) -> None:
182+
headers: CaseInsensitiveDict[str, str]) -> None:
181183
if is_valid_response_header_with_body(public_key_server, status_code, body_bytes, headers):
182184
return
183185
elif is_valid_response_body(public_key_server, body_bytes, headers):
@@ -189,7 +191,7 @@ def validate_response(public_key_server: RsaKey,
189191
def is_valid_response_header_with_body(public_key_server: RsaKey,
190192
status_code: int,
191193
body_bytes: bytes,
192-
headers: Dict[str, str]) -> bool:
194+
headers: CaseInsensitiveDict[str, str]) -> bool:
193195
head_bytes = _generate_response_head_bytes(status_code, headers)
194196
bytes_signed = head_bytes + body_bytes
195197
signer = PKCS1_v1_5.pkcs1_15.new(public_key_server)
@@ -205,8 +207,8 @@ def is_valid_response_header_with_body(public_key_server: RsaKey,
205207

206208

207209
def is_valid_response_body(public_key_server: RsaKey,
208-
body_bytes: bytes,
209-
headers: Dict[str, str]) -> bool:
210+
body_bytes: bytes,
211+
headers: CaseInsensitiveDict[str, str]) -> bool:
210212
signer = PKCS1_v1_5.pkcs1_15.new(public_key_server)
211213
digest = SHA256.new()
212214
digest.update(body_bytes)
@@ -220,7 +222,7 @@ def is_valid_response_body(public_key_server: RsaKey,
220222

221223

222224
def _generate_response_head_bytes(status_code: int,
223-
headers: Dict[str, str]) -> bytes:
225+
headers: CaseInsensitiveDict[str, str]) -> bytes:
224226
head_string = str(status_code) + _DELIMITER_NEWLINE
225227
header_tuples = sorted((k, headers[k]) for k in headers)
226228

tests/context/test_user_context.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ def setUpClass(cls):
1414

1515
def test_build_user_context(self):
1616
from bunq.sdk.context.user_context import UserContext
17-
user_context = UserContext(self._API_CONTEXT.session_context.user_id, self._API_CONTEXT.session_context.user)
17+
user_context = UserContext(
18+
self._API_CONTEXT.session_context.user_id,
19+
self._API_CONTEXT.session_context.get_user_reference()
20+
)
1821
user_context.refresh_user_context()
1922

2023
self.assertIsNotNone(user_context.user_id)

0 commit comments

Comments
 (0)