Skip to content

Commit 0657a4b

Browse files
authored
feat(python): add support for offset, cursor, link and page based pagination (#95)
Since many APIs use different pagination strategies, and this change ensures the SDK can handle them flexibly. This commit adds support for offset, link, page, and cursor-based pagination types in the core library, including relevant unit tests for each type.
1 parent 5a37ba2 commit 0657a4b

52 files changed

Lines changed: 3668 additions & 33 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-runner.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
matrix:
1818
os: [ubuntu-22.04]
19-
python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
19+
python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
2020
steps:
2121
- uses: actions/checkout@v3
2222
- name: Setup Python

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,5 @@ cython_debug/
160160
.idea/
161161

162162
# Visual Studio Code
163-
.vscode/
163+
.vscode/
164+
.qodo

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ pip install apimatic-core
9696
| [`AnyOf`](apimatic_core/types/union_types/any_of.py ) | A class to represent information about AnyOf union types |
9797
| [`LeafType`](apimatic_core/types/union_types/leaf_type.py ) | A class to represent the case information in an OneOf or AnyOf union type |
9898

99+
## Pagination
100+
| Name | Description |
101+
|--------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
102+
| [`CursorPagination`](apimatic_core/pagination/strategies/cursor_pagination.py) | This class manages the extraction and injection of cursor values between API requests and responses, enabling seamless traversal of paginated data. It validates required pointers, updates the request builder with the appropriate cursor, and applies a metadata wrapper to paged responses. |
103+
| [`LinkPagination`](apimatic_core/pagination/strategies/link_pagination.py) | This class updates the request builder with query parameters from the next page link and applies a metadata wrapper to the paged response. |
104+
| [`OffsetPagination`](apimatic_core/pagination/strategies/offset_pagination.py) | This class manages pagination by updating an offset parameter in the request builder, allowing sequential retrieval of paginated data. It extracts and updates the offset based on a configurable JSON pointer and applies a metadata wrapper to each page response. |
105+
| [`PagePagination`](apimatic_core/pagination/strategies/page_pagination.py) | This class manages pagination by updating the request builder with the appropriate page number, using a JSON pointer to identify the pagination parameter. It also applies a metadata wrapper to each paged response, including the current page number. |
106+
| [`PaginatedData`](apimatic_core/pagination/paginated_data.py) | Provides methods to iterate over items and pages, fetch next pages using defined pagination strategies, and access the latest HTTP response and request builder. Supports independent iterators for concurrent traversals. |
107+
108+
99109
## Utilities
100110
| Name | Description |
101111
|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|

apimatic_core/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
'types',
1212
'logger',
1313
'exceptions',
14-
'constants'
14+
'constants',
15+
'pagination'
1516
]

apimatic_core/api_call.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
from apimatic_core.configurations.endpoint_configuration import EndpointConfiguration
22
from apimatic_core.configurations.global_configuration import GlobalConfiguration
33
from apimatic_core.logger.sdk_logger import LoggerFactory
4+
from apimatic_core.pagination.paginated_data import PaginatedData
45
from apimatic_core.response_handler import ResponseHandler
5-
6+
import copy
67

78
class ApiCall:
89

910
@property
1011
def new_builder(self):
1112
return ApiCall(self._global_configuration)
1213

14+
@property
15+
def request_builder(self):
16+
return self._request_builder
17+
18+
@property
19+
def get_pagination_strategies(self):
20+
return self._pagination_strategies
21+
22+
@property
23+
def global_configuration(self):
24+
return self._global_configuration
25+
1326
def __init__(
1427
self,
1528
global_configuration=GlobalConfiguration()
@@ -20,6 +33,7 @@ def __init__(
2033
self._endpoint_configuration = EndpointConfiguration()
2134
self._api_logger = LoggerFactory.get_api_logger(self._global_configuration.get_http_client_configuration()
2235
.logging_configuration)
36+
self._pagination_strategies = None
2337

2438
def request(self, request_builder):
2539
self._request_builder = request_builder
@@ -29,6 +43,10 @@ def response(self, response_handler):
2943
self._response_handler = response_handler
3044
return self
3145

46+
def pagination_strategies(self, *pagination_strategies):
47+
self._pagination_strategies = pagination_strategies
48+
return self
49+
3250
def endpoint_configuration(self, endpoint_configuration):
3351
self._endpoint_configuration = endpoint_configuration
3452
return self
@@ -62,3 +80,25 @@ def execute(self):
6280
_http_callback.on_after_response(_http_response)
6381

6482
return self._response_handler.handle(_http_response, self._global_configuration.get_global_errors())
83+
84+
85+
def paginate(self, page_iterable_creator, paginated_items_converter):
86+
return page_iterable_creator(PaginatedData(self, paginated_items_converter))
87+
88+
def clone(self, global_configuration=None, request_builder=None, response_handler=None,
89+
endpoint_configuration=None, pagination_strategies=None):
90+
new_instance = copy.deepcopy(self)
91+
new_instance._global_configuration = global_configuration or self._global_configuration
92+
new_instance._request_builder = request_builder or self._request_builder
93+
new_instance._response_handler = response_handler or self._response_handler
94+
new_instance._endpoint_configuration = endpoint_configuration or self._endpoint_configuration
95+
new_instance._pagination_strategies = pagination_strategies or self._pagination_strategies
96+
return new_instance
97+
98+
def __deepcopy__(self, memodict={}):
99+
copy_instance = ApiCall(self._global_configuration)
100+
copy_instance._request_builder = copy.deepcopy(self._request_builder, memo=memodict)
101+
copy_instance._response_handler = copy.deepcopy(self._response_handler, memo=memodict)
102+
copy_instance._endpoint_configuration = copy.deepcopy(self._endpoint_configuration, memo=memodict)
103+
copy_instance._pagination_strategies = copy.deepcopy(self._pagination_strategies, memo=memodict)
104+
return copy_instance
Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,60 @@
1-
21
class EndpointConfiguration:
2+
"""Configuration for an API endpoint, including binary response handling,
3+
retry behavior, and request builder management.
4+
"""
5+
6+
def __init__(self):
7+
self._has_binary_response = None
8+
self._to_retry = None
39

410
@property
511
def contains_binary_response(self):
12+
"""Indicates whether the response is expected to be binary."""
613
return self._has_binary_response
714

815
@property
916
def should_retry(self):
17+
"""Indicates whether the request should be retried on failure."""
1018
return self._to_retry
1119

12-
def __init__(
13-
self
14-
):
15-
self._has_binary_response = None
16-
self._to_retry = None
17-
1820
def has_binary_response(self, has_binary_response):
21+
"""Sets whether the response should be treated as binary.
22+
23+
Args:
24+
has_binary_response (bool): True if the response is binary.
25+
26+
Returns:
27+
EndpointConfiguration: The current instance for chaining.
28+
"""
1929
self._has_binary_response = has_binary_response
2030
return self
2131

2232
def to_retry(self, to_retry):
33+
"""Sets whether the request should be retried on failure.
34+
35+
Args:
36+
to_retry (bool): True if retries should be attempted.
37+
38+
Returns:
39+
EndpointConfiguration: The current instance for chaining.
40+
"""
2341
self._to_retry = to_retry
2442
return self
2543

44+
def __deepcopy__(self, memo={}):
45+
if id(self) in memo:
46+
return memo[id(self)]
47+
copy_instance = EndpointConfiguration()
48+
copy_instance._has_binary_response = self._has_binary_response
49+
copy_instance._to_retry = self._to_retry
50+
memo[id(self)] = copy_instance
51+
return copy_instance
52+
53+
def clone_with(self, **kwargs):
54+
clone_instance = self.__deepcopy__()
55+
for key, value in kwargs.items():
56+
if hasattr(clone_instance, key):
57+
setattr(clone_instance, key, value)
58+
else:
59+
raise AttributeError(f"'EndpointConfiguration' object has no attribute '{key}'")
60+
return clone_instance

apimatic_core/configurations/global_configuration.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration
44
from apimatic_core.utilities.api_helper import ApiHelper
5-
5+
import copy
66

77
class GlobalConfiguration:
88

@@ -72,3 +72,18 @@ def add_useragent_in_global_headers(self, user_agent, user_agent_parameters):
7272
user_agent, user_agent_parameters).replace(' ', ' ')
7373
if user_agent:
7474
self._global_headers['user-agent'] = user_agent
75+
76+
def clone_with(self, http_client_configuration=None):
77+
clone_instance = self.__deepcopy__()
78+
clone_instance._http_client_configuration = http_client_configuration or self._http_client_configuration
79+
return clone_instance
80+
81+
def __deepcopy__(self, memo={}):
82+
copy_instance = GlobalConfiguration()
83+
copy_instance._http_client_configuration = copy.deepcopy(self._http_client_configuration, memo)
84+
copy_instance._global_errors = copy.deepcopy(self._global_errors, memo)
85+
copy_instance._global_headers = copy.deepcopy(self._global_headers, memo)
86+
copy_instance._additional_headers = copy.deepcopy(self._additional_headers, memo)
87+
copy_instance._auth_managers = copy.deepcopy(self._auth_managers, memo)
88+
copy_instance._base_uri_executor = copy.deepcopy(self._base_uri_executor, memo)
89+
return copy_instance

apimatic_core/http/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
'http_callback',
33
'configurations',
44
'request',
5-
'response'
5+
'response',
6+
'http_call_context'
67
]

apimatic_core/http/configurations/http_client_configuration.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# -*- coding: utf-8 -*-
2-
from apimatic_core.factories.http_response_factory import HttpResponseFactory
31

2+
from apimatic_core.factories.http_response_factory import HttpResponseFactory
3+
import copy
44

55
class HttpClientConfiguration(object): # pragma: no cover
66
"""A class used for configuring the SDK by a user.
@@ -95,3 +95,14 @@ def __init__(self, http_client_instance=None,
9595

9696
def set_http_client(self, http_client):
9797
self._http_client = http_client
98+
99+
def clone(self, http_callback=None):
100+
http_client_instance = HttpClientConfiguration(
101+
http_client_instance=self.http_client_instance,
102+
override_http_client_configuration=self.override_http_client_configuration,
103+
http_call_back=http_callback or self.http_callback,
104+
timeout=self.timeout, max_retries=self.max_retries, backoff_factor=self.backoff_factor,
105+
retry_statuses=self.retry_statuses, retry_methods=self.retry_methods,
106+
logging_configuration=self.logging_configuration)
107+
http_client_instance.set_http_client(self.http_client)
108+
return http_client_instance
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from apimatic_core.http.http_callback import HttpCallBack
2+
3+
4+
class HttpCallContext(HttpCallBack):
5+
6+
@property
7+
def request(self):
8+
return self._request
9+
10+
@property
11+
def response(self):
12+
return self._response
13+
14+
def __init__(self):
15+
self._request = None
16+
self._response = None
17+
18+
"""An interface for the callback to be called before and after the
19+
HTTP call for an endpoint is made.
20+
21+
This class should not be instantiated but should be used as a base class
22+
for HttpCallBack classes.
23+
24+
"""
25+
26+
def on_before_request(self, request): # pragma: no cover
27+
"""The controller will call this method before making the HttpRequest.
28+
29+
Args:
30+
request (HttpRequest): The request object which will be sent
31+
to the HttpClient to be executed.
32+
"""
33+
self._request = request
34+
35+
def on_after_response(self, response): # pragma: no cover
36+
"""The controller will call this method after making the HttpRequest.
37+
38+
Args:
39+
response (HttpResponse): The HttpResponse of the API call.
40+
"""
41+
self._response = response

0 commit comments

Comments
 (0)