Skip to content

Commit a270ebb

Browse files
authored
Merge pull request #76 from bunq/bunq_update_7_
Bunq update 7
2 parents 4bab187 + abdfa4d commit a270ebb

17 files changed

+10347
-5347
lines changed

bunq/sdk/client.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,7 @@ def _fetch_response_id(self, response):
284284
if self.HEADER_RESPONSE_ID_LOWER_CASED in headers:
285285
return headers[self.HEADER_RESPONSE_ID_LOWER_CASED]
286286

287-
return exception.BunqException(
288-
self._ERROR_COULD_NOT_DETERMINE_RESPONSE_ID_HEADER
289-
)
287+
return self._ERROR_COULD_NOT_DETERMINE_RESPONSE_ID_HEADER;
290288

291289
def put(self, uri_relative, request_bytes, custom_headers):
292290
"""

bunq/sdk/context.py

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

6-
from bunq.sdk.model import core
76
from bunq.sdk import security
7+
from bunq.sdk.exception import BunqException
88
from bunq.sdk.json import converter
9+
from bunq.sdk.model import core
10+
from bunq.sdk.model.device_server_internal import DeviceServerInternal
911
from bunq.sdk.model.generated import endpoint
1012

1113

@@ -119,13 +121,11 @@ def _register_device(self, device_description,
119121
:rtype: None
120122
"""
121123

122-
endpoint.DeviceServer.create(
123-
self,
124-
{
125-
endpoint.DeviceServer.FIELD_DESCRIPTION: device_description,
126-
endpoint.DeviceServer.FIELD_SECRET: self.api_key,
127-
endpoint.DeviceServer.FIELD_PERMITTED_IPS: permitted_ips,
128-
}
124+
DeviceServerInternal.create(
125+
device_description,
126+
self.api_key,
127+
permitted_ips,
128+
api_context=self
129129
)
130130

131131
def _initialize_session(self):
@@ -136,8 +136,9 @@ def _initialize_session(self):
136136
session_server = core.SessionServer.create(self).value
137137
token = session_server.token.token
138138
expiry_time = self._get_expiry_timestamp(session_server)
139+
user_id = session_server.get_referenced_object().id_
139140

140-
self._session_context = SessionContext(token, expiry_time)
141+
self._session_context = SessionContext(token, expiry_time, user_id)
141142

142143
@classmethod
143144
def _get_expiry_timestamp(cls, session_server):
@@ -377,16 +378,18 @@ class SessionContext(object):
377378
"""
378379
:type _token: str
379380
:type _expiry_time: datetime.datetime
381+
:type _user_id: int
380382
"""
381383

382-
def __init__(self, token, expiry_time):
384+
def __init__(self, token, expiry_time, user_id):
383385
"""
384386
:type token: str
385387
:type expiry_time: datetime.datetime
386388
"""
387389

388390
self._token = token
389391
self._expiry_time = expiry_time
392+
self._user_id = user_id
390393

391394
@property
392395
def token(self):
@@ -403,3 +406,146 @@ def expiry_time(self):
403406
"""
404407

405408
return self._expiry_time
409+
410+
@property
411+
def user_id(self):
412+
return self._user_id
413+
414+
415+
class UserContext(object):
416+
_ERROR_UNEXPECTED_USER_INSTANCE = '"{}" is unexpected user instance.'
417+
_ERROR_NO_ACTIVE_MONETARY_ACCOUNT_FOUND = \
418+
'No active monetary account found.'
419+
_STATUS_ACTIVE = 'ACTIVE'
420+
421+
def __init__(self, user_id):
422+
"""
423+
:type user_id: int
424+
"""
425+
426+
self._user_id = user_id
427+
self._user_person = None
428+
self._user_company = None
429+
self._primary_monetary_account = None
430+
431+
user_object = endpoint.User.list().value[0].get_referenced_object()
432+
self._set_user(user_object)
433+
434+
def _set_user(self, user):
435+
if isinstance(user, endpoint.UserPerson):
436+
self._user_person = user
437+
438+
elif isinstance(user, endpoint.UserCompany):
439+
self._user_company = user
440+
441+
else:
442+
raise BunqException(
443+
self._ERROR_UNEXPECTED_USER_INSTANCE.format(user.__class__))
444+
445+
def init_main_monetary_account(self):
446+
all_monetary_account = endpoint.MonetaryAccountBank.list().value
447+
448+
for account in all_monetary_account:
449+
if account.status == self._STATUS_ACTIVE:
450+
self._primary_monetary_account = account
451+
452+
return
453+
454+
raise BunqException(self._ERROR_NO_ACTIVE_MONETARY_ACCOUNT_FOUND)
455+
456+
@property
457+
def user_id(self):
458+
return self._user_id
459+
460+
def is_only_user_person_set(self):
461+
"""
462+
:rtype: bool
463+
"""
464+
465+
return self._user_person is not None and self._user_company is None
466+
467+
def is_only_user_company_set(self):
468+
"""
469+
:rtype: bool
470+
"""
471+
472+
return self._user_company is not None and self._user_person is None
473+
474+
def is_both_user_type_set(self):
475+
"""
476+
:rtype: bool
477+
"""
478+
479+
return self._user_company is not None and self._user_person is not None
480+
481+
@property
482+
def user_company(self):
483+
"""
484+
:rtype: endpoint.UserCompany
485+
"""
486+
487+
return self._user_company
488+
489+
@property
490+
def user_person(self):
491+
"""
492+
:rtype: endpoint.UserPerson
493+
"""
494+
495+
return self._user_person
496+
497+
@property
498+
def primary_monetary_account(self):
499+
"""
500+
:rtype: endpoint.MonetaryAccountBank
501+
"""
502+
503+
return self._primary_monetary_account
504+
505+
506+
class BunqContext(object):
507+
_ERROR_CLASS_SHOULD_NOT_BE_INITIALIZED = \
508+
'This class should not be instantiated'
509+
_ERROR_API_CONTEXT_HAS_NOT_BEEN_LOADED = \
510+
'ApiContext has not been loaded. Please load ApiContext in BunqContext'
511+
_ERROR_USER_CONTEXT_HAS_NOT_BEEN_LOADED = \
512+
'UserContext has not been loaded, please load this' \
513+
' by loading ApiContext.'
514+
515+
_api_context = None
516+
_user_context = None
517+
518+
def __init__(self):
519+
raise TypeError(self._ERROR_CLASS_SHOULD_NOT_BE_INITIALIZED)
520+
521+
@classmethod
522+
def load_api_context(cls, api_context):
523+
"""
524+
:type api_context: ApiContext
525+
"""
526+
527+
cls._api_context = api_context
528+
cls._user_context = UserContext(api_context.session_context.user_id)
529+
cls._user_context.init_main_monetary_account()
530+
531+
@classmethod
532+
def api_context(cls):
533+
"""
534+
:rtype: ApiContext
535+
"""
536+
537+
if cls._api_context is not None:
538+
return cls._api_context
539+
540+
raise BunqException(cls._ERROR_API_CONTEXT_HAS_NOT_BEEN_LOADED)
541+
542+
@classmethod
543+
def user_context(cls):
544+
"""
545+
:rtype: UserContext
546+
"""
547+
548+
if cls._user_context is not None:
549+
return cls._user_context
550+
551+
raise BunqException(cls._ERROR_USER_CONTEXT_HAS_NOT_BEEN_LOADED)

bunq/sdk/model/core.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from bunq.sdk import client
22
from bunq.sdk.json import converter
3+
from bunq.sdk.exception import BunqException
4+
from bunq.sdk import context
35

46

57
class AnchoredObjectInterface:
@@ -135,6 +137,35 @@ def _from_json_list(cls, response_raw, wrapper=None):
135137
return client.BunqResponse(array_deserialized, response_raw.headers,
136138
pagination)
137139

140+
@classmethod
141+
def _get_api_context(cls):
142+
"""
143+
:rtype: context.ApiContext
144+
"""
145+
146+
return context.BunqContext.api_context()
147+
148+
@classmethod
149+
def _determine_user_id(cls):
150+
"""
151+
:rtype: int
152+
"""
153+
154+
return context.BunqContext.user_context().user_id
155+
156+
@classmethod
157+
def _determine_monetary_account_id(cls, monetary_account_id=None):
158+
"""
159+
:type monetary_account_id: int
160+
161+
:rtype: int
162+
"""
163+
164+
if monetary_account_id is None:
165+
return context.BunqContext.user_context().primary_monetary_account.id_
166+
167+
return monetary_account_id
168+
138169

139170
class Id(BunqModel):
140171
"""
@@ -366,6 +397,9 @@ class SessionServer(BunqModel):
366397
# Field constants
367398
FIELD_SECRET = "secret"
368399

400+
# Error constants
401+
_ERROR_ALL_FIELD_IS_NULL = 'All fields are null'
402+
369403
def __init__(self):
370404
self._id_ = None
371405
self._token = None
@@ -442,3 +476,16 @@ def is_all_field_none(self):
442476
return False
443477

444478
return True
479+
480+
def get_referenced_object(self):
481+
"""
482+
:rtype: BunqModel
483+
"""
484+
485+
if self._user_person is not None:
486+
return self._user_person
487+
488+
if self._user_company is not None:
489+
return self._user_company
490+
491+
raise BunqException(self._ERROR_ALL_FIELD_IS_NULL)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from bunq.sdk.model.generated.endpoint import DeviceServer
2+
from bunq.sdk.model.generated.endpoint import BunqResponseInt
3+
from bunq.sdk import client
4+
from bunq.sdk.json import converter
5+
from bunq.sdk.exception import BunqException
6+
7+
8+
class DeviceServerInternal(DeviceServer):
9+
_ERROR_API_CONTEXT_IS_NULL = 'ApiContext should not be None,' \
10+
' use the generated class instead.'
11+
12+
@classmethod
13+
def create(cls, description, secret, permitted_ips=None,
14+
custom_headers=None, api_context=None):
15+
"""
16+
Create a new DeviceServer providing the installation token in the header
17+
and signing the request with the private part of the key you used to
18+
create the installation. The API Key that you are using will be bound to
19+
the IP address of the DeviceServer which you have
20+
created.<br/><br/>Using a Wildcard API Key gives you the freedom to make
21+
API calls even if the IP address has changed after the POST
22+
device-server.<br/><br/>Find out more at this link <a
23+
href="https://bunq.com/en/apikey-dynamic-ip"
24+
target="_blank">https://bunq.com/en/apikey-dynamic-ip</a>.
25+
26+
:param description: The description of the DeviceServer. This is only
27+
for your own reference when reading the DeviceServer again.
28+
:type description: str
29+
:param secret: The API key. You can request an API key in the bunq app.
30+
:type secret: str
31+
:param permitted_ips: An array of IPs (v4 or v6) this DeviceServer will
32+
be able to do calls from. These will be linked to the API key.
33+
:type permitted_ips: list[str]
34+
:type custom_headers: dict[str, str]|None
35+
:type api_context: context.ApiContext
36+
37+
:rtype: BunqResponseInt
38+
"""
39+
40+
if api_context is None:
41+
raise BunqException(cls._ERROR_API_CONTEXT_IS_NULL)
42+
43+
if custom_headers is None:
44+
custom_headers = {}
45+
46+
request_map = {
47+
cls.FIELD_DESCRIPTION: description,
48+
cls.FIELD_SECRET: secret,
49+
cls.FIELD_PERMITTED_IPS: permitted_ips
50+
}
51+
52+
api_client = client.ApiClient(api_context)
53+
request_bytes = converter.class_to_json(request_map).encode()
54+
endpoint_url = cls._ENDPOINT_URL_CREATE
55+
response_raw = api_client.post(endpoint_url, request_bytes,
56+
custom_headers)
57+
58+
return BunqResponseInt.cast_from_bunq_response(
59+
cls._process_for_id(response_raw)
60+
)

0 commit comments

Comments
 (0)