Skip to content

Commit 4cc43a1

Browse files
authored
Merge pull request #21 from bunq/20-pagination
Add pagination
2 parents e438d06 + 36b987a commit 4cc43a1

19 files changed

+1813
-334
lines changed

bunq/sdk/client.py

Lines changed: 160 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import uuid
2-
3-
# Due to compatibility requirements, we are importing a class here.
4-
try:
5-
from json import JSONDecodeError
6-
except ImportError:
7-
from simplejson import JSONDecodeError
2+
from json import JSONDecodeError
3+
from urllib.parse import urlencode
84

95
import requests
106

11-
from bunq.sdk.json import converter
12-
from bunq.sdk import security
137
from bunq.sdk import context
148
from bunq.sdk import exception
9+
from bunq.sdk import security
10+
from bunq.sdk.json import converter
1511

1612

1713
class ApiClient(object):
@@ -47,6 +43,9 @@ class ApiClient(object):
4743
_METHOD_GET = 'GET'
4844
_METHOD_DELETE = 'DELETE'
4945

46+
# Delimiter between path and params in URL
47+
_DELIMITER_URL_QUERY = '?'
48+
5049
# Status code for successful execution
5150
_STATUS_CODE_OK = 200
5251

@@ -76,30 +75,35 @@ def post(self, uri_relative, request_bytes, custom_headers):
7675
self._METHOD_POST,
7776
uri_relative,
7877
request_bytes,
78+
{},
7979
custom_headers
8080
)
8181

82-
def _request(self, method, uri_relative, request_bytes, custom_headers):
82+
def _request(self, method, uri_relative, request_bytes, params,
83+
custom_headers):
8384
"""
8485
:type method: str
8586
:type uri_relative: str
8687
:type request_bytes: bytes
88+
:type params: dict[str, str]
8789
:type custom_headers: dict[str, str]
8890
8991
:return: BunqResponseRaw
9092
"""
9193

94+
uri_relative_with_params = self._append_params_to_uri(uri_relative,
95+
params)
9296
self._api_context.ensure_session_active()
9397
all_headers = self._get_all_headers(
9498
method,
95-
uri_relative,
99+
uri_relative_with_params,
96100
request_bytes,
97101
custom_headers
98102
)
99103

100104
response = requests.request(
101105
method,
102-
self._get_uri_full(uri_relative),
106+
self._get_uri_full(uri_relative_with_params),
103107
data=request_bytes,
104108
headers=all_headers,
105109
proxies={self._FIELD_PROXY_HTTPS: self._api_context.proxy_url}
@@ -117,6 +121,20 @@ def _request(self, method, uri_relative, request_bytes, custom_headers):
117121

118122
return self._create_bunq_response_raw(response)
119123

124+
@classmethod
125+
def _append_params_to_uri(cls, uri, params):
126+
"""
127+
:type uri: str
128+
:type params: dict[str, str]
129+
130+
:rtype: str
131+
"""
132+
133+
if params:
134+
return uri + cls._DELIMITER_URL_QUERY + urlencode(params)
135+
136+
return uri
137+
120138
def _get_all_headers(self, method, endpoint, request_bytes, custom_headers):
121139
"""
122140
:type method: str
@@ -242,12 +260,14 @@ def put(self, uri_relative, request_bytes, custom_headers):
242260
self._METHOD_PUT,
243261
uri_relative,
244262
request_bytes,
263+
{},
245264
custom_headers
246265
)
247266

248-
def get(self, uri_relative, custom_headers):
267+
def get(self, uri_relative, params, custom_headers):
249268
"""
250269
:type uri_relative: str
270+
:type params: dict[str, str]
251271
:type custom_headers: dict[str, str]
252272
253273
:rtype: BunqResponseRaw
@@ -257,6 +277,7 @@ def get(self, uri_relative, custom_headers):
257277
self._METHOD_GET,
258278
uri_relative,
259279
self._BYTES_EMPTY,
280+
params,
260281
custom_headers
261282
)
262283

@@ -272,6 +293,7 @@ def delete(self, uri_relative, custom_headers):
272293
self._METHOD_DELETE,
273294
uri_relative,
274295
self._BYTES_EMPTY,
296+
{},
275297
custom_headers
276298
)
277299

@@ -312,16 +334,19 @@ class BunqResponse(object):
312334
"""
313335
:type _value: T
314336
:type _headers: dict[str, str]
337+
:type _pagination: Pagination|None
315338
"""
316339

317-
def __init__(self, value, headers):
340+
def __init__(self, value, headers, pagination=None):
318341
"""
319342
:type value: T
320343
:type headers: dict[str, str]
344+
:type pagination Pagination|None
321345
"""
322346

323347
self._value = value
324348
self._headers = headers
349+
self._pagination = pagination
325350

326351
@property
327352
def value(self):
@@ -338,3 +363,125 @@ def headers(self):
338363
"""
339364

340365
return self._headers
366+
367+
@property
368+
def pagination(self):
369+
"""
370+
:rtype: Pagination
371+
"""
372+
373+
return self._pagination
374+
375+
376+
class Pagination(object):
377+
"""
378+
:type older_id: int|None
379+
:type newer_id: int|None
380+
:type future_id: int|None
381+
:type count: int|None
382+
"""
383+
384+
# Error constants
385+
_ERROR_NO_PREVIOUS_PAGE = 'Could not generate previous page URL params: ' \
386+
'there is no previous page.'
387+
_ERROR_NO_NEXT_PAGE = 'Could not generate next page URL params: ' \
388+
'there is no next page.'
389+
390+
# URL Param constants
391+
PARAM_OLDER_ID = 'older_id'
392+
PARAM_NEWER_ID = 'newer_id'
393+
PARAM_COUNT = 'count'
394+
395+
def __init__(self):
396+
self.older_id = None
397+
self.newer_id = None
398+
self.future_id = None
399+
self.count = None
400+
401+
@property
402+
def url_params_previous_page(self):
403+
"""
404+
:rtype: dict[str, str]
405+
"""
406+
407+
self.assert_has_previous_page()
408+
409+
params = {self.PARAM_OLDER_ID: str(self.older_id)}
410+
self._add_count_to_params_if_needed(params)
411+
412+
return params
413+
414+
def assert_has_previous_page(self):
415+
"""
416+
:raise: exception.BunqException
417+
"""
418+
419+
if not self.has_previous_page():
420+
raise exception.BunqException(self._ERROR_NO_PREVIOUS_PAGE)
421+
422+
def has_previous_page(self):
423+
"""
424+
:rtype: bool
425+
"""
426+
427+
return self.older_id is not None
428+
429+
@property
430+
def url_params_count_only(self):
431+
"""
432+
:rtype: dict[str, str]
433+
"""
434+
435+
params = {}
436+
self._add_count_to_params_if_needed(params)
437+
438+
return params
439+
440+
def _add_count_to_params_if_needed(self, params):
441+
"""
442+
:type params: dict[str, str]
443+
444+
:rtype: None
445+
"""
446+
447+
if self.count is not None:
448+
params[self.PARAM_COUNT] = str(self.count)
449+
450+
def has_next_page_assured(self):
451+
"""
452+
:rtype: bool
453+
"""
454+
455+
return self.newer_id is not None
456+
457+
@property
458+
def url_params_next_page(self):
459+
"""
460+
:rtype: dict[str, str]
461+
"""
462+
463+
self.assert_has_next_page()
464+
465+
params = {self.PARAM_NEWER_ID: str(self._next_id)}
466+
self._add_count_to_params_if_needed(params)
467+
468+
return params
469+
470+
def assert_has_next_page(self):
471+
"""
472+
:raise: exception.BunqException
473+
"""
474+
475+
if self._next_id is None:
476+
raise exception.BunqException(self._ERROR_NO_NEXT_PAGE)
477+
478+
@property
479+
def _next_id(self):
480+
"""
481+
:rtype: int
482+
"""
483+
484+
if self.has_next_page_assured():
485+
return self.newer_id
486+
487+
return self.future_id

bunq/sdk/json/adapters.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import datetime
2+
import urllib.parse as urlparse
23

4+
from bunq.sdk import client
35
from bunq.sdk import context
46
from bunq.sdk import model
57
from bunq.sdk import security
@@ -440,3 +442,107 @@ def serialize(cls, timestamp):
440442
"""
441443

442444
return timestamp.strftime(cls._FORMAT_TIMESTAMP)
445+
446+
447+
class PaginationAdapter(converter.JsonAdapter):
448+
# Raw pagination response field constants.
449+
_FIELD_FUTURE_URL = 'future_url'
450+
_FIELD_NEWER_URL = 'newer_url'
451+
_FIELD_OLDER_URL = 'older_url'
452+
453+
# Processed pagination field constants.
454+
_FIELD_OLDER_ID = 'older_id'
455+
_FIELD_NEWER_ID = 'newer_id'
456+
_FIELD_FUTURE_ID = 'future_id'
457+
_FIELD_COUNT = 'count'
458+
459+
# Very first index in an array.
460+
_INDEX_FIRST = 0
461+
462+
@classmethod
463+
def deserialize(cls, target_class, pagination_response):
464+
"""
465+
:type target_class: client.Pagination|type
466+
:type pagination_response: dict
467+
468+
:rtype: client.Pagination
469+
"""
470+
471+
pagination = client.Pagination()
472+
pagination.__dict__.update(
473+
cls.parse_pagination_dict(pagination_response)
474+
)
475+
476+
return pagination
477+
478+
@classmethod
479+
def parse_pagination_dict(cls, response_obj):
480+
"""
481+
:type response_obj: dict
482+
483+
:rtype: dict
484+
"""
485+
486+
pagination_dict = {}
487+
488+
cls.update_dict_id_field_from_response_field(
489+
pagination_dict,
490+
cls._FIELD_OLDER_ID,
491+
response_obj,
492+
cls._FIELD_OLDER_URL,
493+
client.Pagination.PARAM_OLDER_ID
494+
)
495+
cls.update_dict_id_field_from_response_field(
496+
pagination_dict,
497+
cls._FIELD_NEWER_ID,
498+
response_obj,
499+
cls._FIELD_NEWER_URL,
500+
client.Pagination.PARAM_NEWER_ID
501+
)
502+
cls.update_dict_id_field_from_response_field(
503+
pagination_dict,
504+
cls._FIELD_FUTURE_ID,
505+
response_obj,
506+
cls._FIELD_FUTURE_URL,
507+
client.Pagination.PARAM_NEWER_ID
508+
)
509+
510+
return pagination_dict
511+
512+
@classmethod
513+
def update_dict_id_field_from_response_field(cls, dict_, dict_id_field,
514+
response_obj, response_field,
515+
response_param):
516+
"""
517+
:type dict_: dict
518+
:type dict_id_field: str
519+
:type response_obj: dict
520+
:type response_field: str
521+
:type response_param: str
522+
"""
523+
524+
url = response_obj[response_field]
525+
526+
if url is not None:
527+
url_parsed = urlparse.urlparse(url)
528+
parameters = urlparse.parse_qs(url_parsed.query)
529+
dict_[dict_id_field] = int(
530+
parameters[response_param][cls._INDEX_FIRST]
531+
)
532+
533+
if cls._FIELD_COUNT in parameters and cls._FIELD_COUNT not in dict_:
534+
dict_[cls._FIELD_COUNT] = int(
535+
parameters[client.Pagination.PARAM_COUNT][cls._INDEX_FIRST]
536+
)
537+
538+
@classmethod
539+
def serialize(cls, pagination):
540+
"""
541+
:type pagination: client.Pagination
542+
543+
:raise: NotImplementedError
544+
"""
545+
546+
_ = pagination
547+
548+
raise NotImplementedError()

bunq/sdk/json/registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22

3+
from bunq.sdk import client
34
from bunq.sdk import context
45
from bunq.sdk import model
56
from bunq.sdk.json import adapters
@@ -36,3 +37,4 @@ def initialize():
3637
)
3738
converter.register_adapter(object_.ShareDetail, adapters.ShareDetailAdapter)
3839
converter.register_adapter(datetime.datetime, adapters.DateTimeAdapter)
40+
converter.register_adapter(client.Pagination, adapters.PaginationAdapter)

0 commit comments

Comments
 (0)