Skip to content

Commit cec2286

Browse files
Merge branch 'release/1.13.1'
2 parents f487783 + 05c9688 commit cec2286

25 files changed

+578
-112
lines changed

CHANGELOG.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,24 @@
66

77
**Implemented enhancements:**
88

9+
- Python SDK PSD2 [\#123](https://github.com/bunq/sdk_python/pull/123) ([angelomelonas](https://github.com/angelomelonas))
910
- Python SDK Refactor [\#117](https://github.com/bunq/sdk_python/pull/117) ([angelomelonas](https://github.com/angelomelonas))
1011

12+
**Fixed bugs:**
13+
14+
- Fix notification adapter and test. [\#126](https://github.com/bunq/sdk_python/pull/126) ([NickvandeGroes](https://github.com/NickvandeGroes))
15+
16+
**Closed issues:**
17+
18+
- Dependencies severely out of date \(and vulnerable: CVEs\) [\#121](https://github.com/bunq/sdk_python/issues/121)
19+
- Typo in EXCEPTIONS.md [\#110](https://github.com/bunq/sdk_python/issues/110)
20+
21+
**Merged pull requests:**
22+
23+
- feature/fix\_typo: fix typo. [\#129](https://github.com/bunq/sdk_python/pull/129) ([angelomelonas](https://github.com/angelomelonas))
24+
- Feature/dependency upgrades [\#128](https://github.com/bunq/sdk_python/pull/128) ([angelomelonas](https://github.com/angelomelonas))
25+
- Add internal NotificationFilters [\#127](https://github.com/bunq/sdk_python/pull/127) ([angelomelonas](https://github.com/angelomelonas))
26+
1127
## [1.10.16](https://github.com/bunq/sdk_python/tree/1.10.16) (2019-06-17)
1228
[Full Changelog](https://github.com/bunq/sdk_python/compare/1.10.2...1.10.16)
1329

@@ -132,10 +148,8 @@
132148

133149
**Merged pull requests:**
134150

135-
- Regenerate code for release [\#74](https://github.com/bunq/sdk_python/pull/74) ([OGKevin](https://github.com/OGKevin))
136151
- Regenerated code to add object types. \(bunq/sdk\_python\#53\) [\#70](https://github.com/bunq/sdk_python/pull/70) ([OGKevin](https://github.com/OGKevin))
137152
- Bunq/sdk python\#67 add missing token qr id field [\#69](https://github.com/bunq/sdk_python/pull/69) ([OGKevin](https://github.com/OGKevin))
138-
- Added missing id field to mastercard action. \(bunq/sdk\_python\#54\) [\#66](https://github.com/bunq/sdk_python/pull/66) ([OGKevin](https://github.com/OGKevin))
139153
- Feature/bunq/sdk python\#59 add response id to request error [\#64](https://github.com/bunq/sdk_python/pull/64) ([OGKevin](https://github.com/OGKevin))
140154
- Configure Zappr [\#63](https://github.com/bunq/sdk_python/pull/63) ([OGKevin](https://github.com/OGKevin))
141155
- \(bunq/sdk\_python\#60\) improve issue and pr template [\#61](https://github.com/bunq/sdk_python/pull/61) ([OGKevin](https://github.com/OGKevin))
@@ -159,6 +173,8 @@
159173

160174
**Merged pull requests:**
161175

176+
- Regenerate code for release [\#74](https://github.com/bunq/sdk_python/pull/74) ([OGKevin](https://github.com/OGKevin))
177+
- Added missing id field to mastercard action. \(bunq/sdk\_python\#54\) [\#66](https://github.com/bunq/sdk_python/pull/66) ([OGKevin](https://github.com/OGKevin))
162178
- Feature/make sure headers are correctly cased bunq/sdk python\#51 [\#57](https://github.com/bunq/sdk_python/pull/57) ([OGKevin](https://github.com/OGKevin))
163179
- Feature/improve decoder bunq/sdk python\#42 [\#56](https://github.com/bunq/sdk_python/pull/56) ([OGKevin](https://github.com/OGKevin))
164180
- Renamed camelCase methods. \(bunq/sdk\_python\#45\) [\#48](https://github.com/bunq/sdk_python/pull/48) ([OGKevin](https://github.com/OGKevin))

README.md

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This SDK is in **beta**. We cannot guarantee constant availability or stability.
2121
Thanks to your feedback we will make improvements on it.
2222

2323
## Installation
24-
``pip install bunq_sdk --upgrade``
24+
pip install bunq_sdk --upgrade
2525

2626
## Usage
2727

@@ -30,19 +30,37 @@ In order to start making calls with the bunq API, you must first register your A
3030
and create a session. In the SDKs, we group these actions and call it "creating an API context". The
3131
context can be created by using the following code snippet:
3232

33-
```
34-
apiContext = context.ApiContext(ENVIRONMENT_TYPE, API_KEY,
35-
DEVICE_DESCRIPTION);
36-
apiContext.save(API_CONTEXT_FILE_PATH)
37-
context.BunqContext.loadApiContext(apiContext)
38-
```
3933

40-
This code snippet, except for `context.BunqContext.loadApiContext(apiContext)` should be called once per API key.
34+
apiContext = ApiContext.create(ENVIRONMENT_TYPE, API_KEY, DEVICE_DESCRIPTION)
35+
apiContext.save(API_CONTEXT_FILE_PATH)
36+
37+
38+
**Please note**: initialising your application is a heavy task and it is recommended to do it only once per device.
39+
40+
apiContext = ApiContext.restore(self.API_CONTEXT_FILE_PATH)
41+
BunqContext.loadApiContext(apiContext)
42+
43+
After saving the context, you can restore it at any time:
4144

4245
#### Example
4346

4447
See [`tinker/setup_context`](https://github.com/bunq/tinker_python/blob/2182b8be276fda921657ad22cfe0b8b48a585ccf/tinker/libs/bunq_lib.py#L44-L59)
4548

49+
#### PSD2
50+
It is possible to create an ApiContext as PSD2 Service Provider. Although this might seem a complex task, we wrote some
51+
helper implementations to get you started. You need to create a certificate and private key to get you started.
52+
Our sandbox environment currently accepts all certificates, if these criteria are met:
53+
54+
- Up to 64 characters
55+
- PISP and/or AISP used in the end.
56+
57+
Make sure you have your unique eIDAS certificate number and certificates ready when you want to perform these tasks on
58+
our production environment.
59+
60+
Creating a PSD2 context is very easy:
61+
62+
apiContext = ApiContext.create_for_psd2(ENVIRONMENT_TYPE, CERTIFICATE, PRIVATE_KEY, CERTIFICATE_CHAIN, DEVICE_DESCRIPTION)
63+
4664
#### Safety considerations
4765
The file storing the context details (i.e. `bunq.conf`) is a key to your account. Anyone having
4866
access to it is able to perform any Public API actions with your account. Therefore, we recommend
@@ -62,14 +80,11 @@ Creating objects through the API requires an `ApiContext`, a `requestMap` and id
6280
dependencies (such as User ID required for accessing a Monetary Account). Optionally, custom headers
6381
can be passed to requests.
6482

65-
66-
```
67-
payment_id = endpoint.Payment.create(
68-
amount=Amount(amount_string, self._CURRENCY_EURL),
69-
counterparty_alias=Pointer(self._POINTER_TYPE_EMAIL, recipient),
70-
description=description
83+
payment_id = endpoint.Payment.create(
84+
amount=Amount(amount_string, self._CURRENCY_EURL),
85+
counterparty_alias=Pointer(self._POINTER_TYPE_EMAIL, recipient),
86+
description=description
7187
)
72-
```
7388

7489
##### Example
7590
See [`tinker/make_payment`](https://github.com/bunq/tinker_python/blob/2182b8be276fda921657ad22cfe0b8b48a585ccf/tinker/libs/bunq_lib.py#L140-L151)
@@ -81,11 +96,9 @@ UUID) Optionally, custom headers can be passed to requests.
8196

8297
This type of calls always returns a model.
8398

84-
```
85-
monetary_account = generated.MonetaryAccountBank.get(
86-
_MONETARY_ACCOUNT_ITEM_ID
87-
)
88-
```
99+
monetary_account = generated.MonetaryAccountBank.get(
100+
_MONETARY_ACCOUNT_ITEM_ID
101+
)
89102

90103
##### Example
91104
See [`tinker/list_all_payment`](https://github.com/bunq/tinker_python/blob/2182b8be276fda921657ad22cfe0b8b48a585ccf/tinker/libs/bunq_lib.py#L85-L103)
@@ -94,12 +107,10 @@ See [`tinker/list_all_payment`](https://github.com/bunq/tinker_python/blob/2182b
94107
Updating objects through the API goes the same way as creating objects, except that also the object to update identifier
95108
(ID or UUID) is needed.
96109

97-
```
98-
endpoint.Card.update(
99-
card_id=int(card_id),
100-
monetary_account_current_id=int(account_id)
101-
)
102-
```
110+
endpoint.Card.update(
111+
card_id=int(card_id),
112+
monetary_account_current_id=int(account_id)
113+
)
103114

104115
##### Example
105116
See [`tinker/update_card`](https://github.com/bunq/tinker_python/blob/2182b8be276fda921657ad22cfe0b8b48a585ccf/tinker/libs/bunq_lib.py#L167-L174)
@@ -109,20 +120,15 @@ Deleting objects through the API requires an `ApiContext`, identifiers of all de
109120
accessing a Monetary Account), and the identifier of the object to delete (ID or UUID) Optionally, custom headers can be
110121
passed to requests.
111122

112-
```
113-
Session.delete(self._SESSION_ID)
114-
```
123+
Session.delete(self._SESSION_ID)
115124

116125
##### Example
117126

118-
119127
#### Listing objects
120128
Listing objects through the API requires an `ApiContext` and identifiers of all dependencies (such as User ID required
121129
for accessing a Monetary Account). Optionally, custom headers can be passed to requests.
122130

123-
```
124-
users = generated.User.list(api_context)
125-
```
131+
users = endpoint.User.list(api_context)
126132

127133
##### Example
128134
See [`UserListExample.py`](./examples/user_list_example.py)
@@ -133,8 +139,8 @@ To get an indication on how the SDK works you can use the python tinker which is
133139
## Running Tests
134140

135141
Information regarding the test cases can be found in the [README.md](./tests/README.md)
136-
located in [test](/tests)
142+
located in [test](/tests).
137143

138144
## Exceptions
139145
The SDK can raise multiple exceptions. For an overview of these exceptions please
140-
take a look at [EXCEPTIONS.md](./bunq/sdk/exception/EXCEPTIONS.md)
146+
take a look at [EXCEPTIONS.md](./bunq/sdk/exception/EXCEPTIONS.md).

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.13.0
1+
1.13.1

bunq/sdk/context/api_context.py

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from bunq.sdk.context.session_context import SessionContext
1212
from bunq.sdk.exception.bunq_exception import BunqException
1313
from bunq.sdk.json import converter
14+
from bunq.sdk.model.core.payment_service_provider_credential_internal import PaymentServiceProviderCredentialInternal
1415
from bunq.sdk.model.generated import endpoint
16+
from bunq.sdk.model.generated.endpoint import UserCredentialPasswordIp, UserPaymentServiceProvider
1517
from bunq.sdk.security import security
1618

1719
if typing.TYPE_CHECKING:
@@ -21,9 +23,9 @@
2123
class ApiContext:
2224
"""
2325
:type _environment_type: ApiEnvironmentType
24-
:type _api_key: str
25-
:type _session_context: SessionContext
26-
:type _installation_context: InstallationContext
26+
:type _api_key: str|None
27+
:type _session_context: SessionContext|None
28+
:type _installation_context: InstallationContext|None
2729
:type _proxy_url: str|None
2830
"""
2931

@@ -42,28 +44,56 @@ class ApiContext:
4244

4345
def __init__(self,
4446
environment_type: ApiEnvironmentType,
45-
api_key: str,
46-
device_description: str,
47-
permitted_ips: List[str] = None,
4847
proxy_url: List[str] = None) -> None:
49-
if permitted_ips is None:
50-
permitted_ips = []
51-
5248
self._environment_type = environment_type
53-
self._api_key = api_key
49+
self._proxy_url = proxy_url
50+
self._api_key = None
5451
self._installation_context = None
5552
self._session_context = None
56-
self._proxy_url = proxy_url
57-
self._initialize(device_description, permitted_ips)
5853

59-
def _initialize(self,
60-
device_description: str,
61-
permitted_ips: List[str]) -> None:
62-
self._initialize_installation()
63-
self._register_device(device_description, permitted_ips)
64-
self._initialize_session()
54+
@classmethod
55+
def create(cls,
56+
environment_type: ApiEnvironmentType,
57+
api_key: str,
58+
description: str,
59+
all_permitted_ip: List[str] = None,
60+
proxy_url: List[str] = None) -> ApiContext:
61+
api_context = cls(environment_type, proxy_url)
62+
63+
api_context._api_key = api_key
64+
65+
api_context.__initialize_installation()
66+
api_context.__register_device(description, all_permitted_ip)
67+
api_context.__initialize_session()
68+
69+
return api_context
70+
71+
@classmethod
72+
def create_for_psd2(cls,
73+
environment_type: ApiEnvironmentType,
74+
certificate: str,
75+
private_key: str,
76+
all_chain_certificate: List[str],
77+
description: str,
78+
all_permitted_ip: List[str] = None,
79+
proxy_url: List[str] = None) -> ApiContext:
80+
api_context = cls(environment_type, proxy_url)
81+
82+
api_context.__initialize_installation()
83+
84+
service_provider_credential = api_context.__initialize_psd2_credential(
85+
certificate,
86+
private_key,
87+
all_chain_certificate)
88+
89+
api_context._api_key = service_provider_credential.token_value
90+
91+
api_context.__register_device(description, all_permitted_ip)
92+
api_context.__initialize_session_for_psd2(service_provider_credential)
93+
94+
return api_context
6595

66-
def _initialize_installation(self) -> None:
96+
def __initialize_installation(self) -> None:
6797
from bunq.sdk.model.core.installation import Installation
6898

6999
private_key_client = security.generate_rsa_private_key()
@@ -83,9 +113,28 @@ def _initialize_installation(self) -> None:
83113
public_key_server
84114
)
85115

86-
def _register_device(self,
87-
device_description: str,
88-
permitted_ips: List[str]) -> None:
116+
def __initialize_psd2_credential(self,
117+
certificate: str,
118+
private_key: str,
119+
all_chain_certificate: List[str], ) -> UserCredentialPasswordIp:
120+
session_token = self.installation_context.token
121+
client_key_pair = self.installation_context.private_key_client
122+
123+
string_to_sign = security.public_key_to_string(client_key_pair.publickey()) + "\n" + session_token
124+
encoded_signature = security.generate_signature(string_to_sign, security.rsa_key_from_string(private_key))
125+
126+
payment_response_provider = PaymentServiceProviderCredentialInternal.create_with_api_context(
127+
certificate,
128+
security.get_certificate_chain_string(all_chain_certificate),
129+
encoded_signature,
130+
self
131+
)
132+
133+
return payment_response_provider
134+
135+
def __register_device(self,
136+
device_description: str,
137+
permitted_ips: List[str]) -> None:
89138
from bunq.sdk.model.core.device_server_internal import DeviceServerInternal
90139

91140
DeviceServerInternal.create(
@@ -95,7 +144,7 @@ def _register_device(self,
95144
api_context=self
96145
)
97146

98-
def _initialize_session(self) -> None:
147+
def __initialize_session(self) -> None:
99148
from bunq.sdk.model.core.session_server import SessionServer
100149

101150
session_server = SessionServer.create(self).value
@@ -105,6 +154,17 @@ def _initialize_session(self) -> None:
105154

106155
self._session_context = SessionContext(token, expiry_time, user_id)
107156

157+
def __initialize_session_for_psd2(self, user_payment_service_provider: UserPaymentServiceProvider) -> None:
158+
from bunq.sdk.model.core.session_server import SessionServer
159+
160+
session_server = SessionServer.create(self).value
161+
162+
token = session_server.token.token
163+
expiry_time = self._get_expiry_timestamp(session_server)
164+
user_id = session_server.get_referenced_user().id_
165+
166+
self._session_context = SessionContext(token, expiry_time, user_id)
167+
108168
@classmethod
109169
def _get_expiry_timestamp(cls, session_server: SessionServer) -> datetime.datetime:
110170
timeout_seconds = cls._get_session_timeout_seconds(session_server)
@@ -118,6 +178,8 @@ def _get_session_timeout_seconds(cls, session_server: SessionServer) -> int:
118178
return session_server.user_company.session_timeout
119179
elif session_server.user_person is not None:
120180
return session_server.user_person.session_timeout
181+
elif session_server.user_payment_service_provider is not None:
182+
return session_server.user_payment_service_provider.session_timeout
121183
elif session_server.user_api_key is not None:
122184
return session_server \
123185
.user_api_key \
@@ -159,7 +221,7 @@ def reset_session(self) -> None:
159221
"""
160222

161223
self._drop_session_context()
162-
self._initialize_session()
224+
self.__initialize_session()
163225

164226
def _drop_session_context(self) -> None:
165227
self._session_context = None

bunq/sdk/context/user_context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def __init__(self, user_id: int) -> None:
1414
self._user_person = None
1515
self._user_company = None
1616
self._user_api_key = None
17+
self._user_payment_service_provider = None
1718
self._primary_monetary_account = None
1819

1920
self._set_user(self.__get_user_object())
@@ -32,11 +33,17 @@ def _set_user(self, user: BunqModel) -> None:
3233
elif isinstance(user, endpoint.UserApiKey):
3334
self._user_api_key = user
3435

36+
elif isinstance(user, endpoint.UserPaymentServiceProvider):
37+
self._user_payment_service_provider = user
38+
3539
else:
3640
raise BunqException(
3741
self._ERROR_UNEXPECTED_USER_INSTANCE.format(user.__class__))
3842

3943
def init_main_monetary_account(self) -> None:
44+
if self._user_payment_service_provider is not None:
45+
return
46+
4047
all_monetary_account = endpoint.MonetaryAccountBank.list().value
4148

4249
for account in all_monetary_account:
@@ -73,6 +80,10 @@ def is_all_user_type_set(self) -> bool:
7380

7481
def refresh_user_context(self) -> None:
7582
self._set_user(self.__get_user_object())
83+
84+
if self._user_payment_service_provider is not None:
85+
return
86+
7687
self.init_main_monetary_account()
7788

7889
@property

0 commit comments

Comments
 (0)