Skip to content

Commit b20c3ab

Browse files
committed
refactor: improve pagination logic
1 parent 40521ae commit b20c3ab

25 files changed

+317
-212
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ By default, two HTTP implementations are provided:
141141
For creating custom HTTP client code, use either `SinchClient` or `SinchClientAsync` client and inject your transport during initialisation:
142142
```python
143143
sinch_client = SinchClientAsync(
144-
key_id="Spanish",
145-
key_secret="Inquisition",
144+
key_id="key_id",
145+
key_secret="key_secret",
146146
project_id="some_project",
147147
transport=MyHTTPAsyncImplementation
148148
)

sinch/core/pagination.py

+104-82
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from abc import ABC, abstractmethod
22
from collections import namedtuple
3+
from typing import Generic
4+
from sinch.domains.numbers.models.numbers import BM
35

46

57
class PageIterator:
@@ -18,26 +20,35 @@ def __next__(self):
1820
return self.paginator
1921

2022
if self.paginator.has_next_page:
21-
return self.paginator.next_page()
23+
self.paginator = self.paginator.next_page()
24+
return self.paginator
2225
else:
2326
raise StopIteration
2427

2528

2629
class AsyncPageIterator:
2730
def __init__(self, paginator):
2831
self.paginator = paginator
32+
self.first_yield = True
2933

3034
def __aiter__(self):
3135
return self
3236

3337
async def __anext__(self):
38+
if self.first_yield:
39+
self.first_yield = False
40+
return self.paginator
41+
3442
if self.paginator.has_next_page:
35-
return await self.paginator.next_page()
36-
else:
37-
raise StopAsyncIteration
43+
next_paginator = await self.paginator.next_page()
44+
if next_paginator:
45+
self.paginator = next_paginator
46+
return self.paginator
3847

48+
raise StopAsyncIteration
3949

40-
class Paginator(ABC):
50+
51+
class Paginator(ABC, Generic[BM]):
4152
"""
4253
Pagination response object.
4354
@@ -51,7 +62,7 @@ class Paginator(ABC):
5162
if paginated_response.has_next_page:
5263
paginated_response = paginated_response.next_page()
5364
"""
54-
def __init__(self, sinch, endpoint, result):
65+
def __init__(self, sinch, endpoint, result: BM):
5566
self._sinch = sinch
5667
self.result = result
5768
self.endpoint = endpoint
@@ -61,6 +72,16 @@ def __init__(self, sinch, endpoint, result):
6172
def __repr__(self):
6273
return "Paginated response content: " + str(self.result)
6374

75+
# TODO: Make content() method abstract in Parent class as we implement in the other domains:
76+
# - Refactor pydantic models in other domains to have a content property.
77+
def content(self):
78+
pass
79+
80+
# TODO: Make iterator() method abstract in Parent class as we implement in the other domains:
81+
# - Refactor pydantic models in other domains to have a content property.
82+
def iterator(self):
83+
pass
84+
6485
@abstractmethod
6586
def auto_paging_iter(self):
6687
pass
@@ -121,116 +142,100 @@ async def _initialize(cls, sinch, endpoint):
121142
return cls(sinch, endpoint, result)
122143

123144

124-
class TokenBasedPaginator(Paginator):
125-
"""Base paginator for token-based pagination."""
145+
class TokenBasedPaginator(Paginator[BM]):
146+
"""Base paginator for token-based pagination with explicit page navigation and metadata."""
126147

127148
def __init__(self, sinch, endpoint, yield_first_page=False, result=None):
128-
self._sinch = sinch
129-
self.endpoint = endpoint
130-
# Determines if the first page should be included
149+
super().__init__(sinch, endpoint, result or sinch.configuration.transport.request(endpoint))
131150
self.yield_first_page = yield_first_page
132-
self.result = result or self._sinch.configuration.transport.request(self.endpoint)
133-
self.has_next_page = bool(self.result.next_page_token)
134151

135-
def _calculate_next_page(self):
136-
self.has_next_page = bool(self.result.next_page_token)
152+
def content(self) -> list[BM]:
153+
return getattr(self.result, "content", [])
137154

138155
def next_page(self):
139-
"""Fetches the next page and updates pagination state."""
156+
"""Returns a new paginator instance for the next page."""
157+
if not self.has_next_page:
158+
return None
159+
140160
self.endpoint.request_data.page_token = self.result.next_page_token
141-
self.result = self._sinch.configuration.transport.request(self.endpoint)
142-
self._calculate_next_page()
143-
return self
161+
next_result = self._sinch.configuration.transport.request(self.endpoint)
162+
163+
return TokenBasedPaginator(self._sinch, self.endpoint, result=next_result)
144164

145165
def auto_paging_iter(self):
146166
"""Returns an iterator for automatic pagination."""
147-
return PageIterator(self, yield_first_page=self.yield_first_page)
148-
149-
@classmethod
150-
def _initialize(cls, sinch, endpoint):
151-
"""Creates an instance of the paginator skipping first page."""
152-
result = sinch.configuration.transport.request(endpoint)
153-
return cls(sinch, endpoint, yield_first_page=False, result=result)
154-
155-
156-
class TokenBasedPaginatorNumbers(TokenBasedPaginator):
157-
"""
158-
Paginator for handling token-based pagination specifically for phone numbers.
167+
return PageIterator(self, yield_first_page=True)
159168

160-
This paginator is designed to iterate through phone numbers automatically or manually, fetching new pages as needed.
161-
It extends the TokenBasedPaginatorBase class and provides additional methods for number-specific pagination.
162-
"""
163-
164-
def __init__(self, sinch, endpoint):
165-
super().__init__(sinch, endpoint, yield_first_page=True)
169+
def iterator(self):
170+
"""Iterates over individual items across all pages."""
171+
paginator = self
172+
while paginator:
173+
yield from paginator.content()
166174

167-
def numbers_iterator(self):
168-
"""Iterates through numbers individually, fetching new pages as needed."""
169-
while True:
170-
if self.result and self.result.active_numbers:
171-
yield from self.result.active_numbers
172-
173-
if not self.has_next_page:
175+
next_page_instance = paginator.next_page()
176+
if not next_page_instance:
174177
break
175-
176-
self.next_page()
178+
paginator = next_page_instance
177179

178180
def list(self):
179-
"""Returns the first page's numbers along with pagination metadata."""
181+
"""Returns structured pagination metadata along with the first page's content (sync)."""
182+
next_page_instance = self.next_page()
183+
return self._list(next_page_instance, sync=True)
180184

185+
def _list(self, next_page_instance, sync=True):
186+
"""Core logic for `list()`, shared between sync and async versions."""
181187
PagedListResponse = namedtuple(
182188
"PagedResponse", ["result", "has_next_page", "next_page_info", "next_page"]
183189
)
184190

185-
next_page_result = self._get_next_page_result()
191+
next_page_info = {
192+
"result": self.content(),
193+
"result.next": (
194+
self.content() + (next_page_instance.content() if next_page_instance else [])
195+
),
196+
"has_next_page": self.has_next_page,
197+
"has_next_page.next": bool(next_page_instance and next_page_instance.has_next_page),
198+
}
199+
200+
next_page_wrapper = self._get_next_page_wrapper(next_page_instance, sync)
186201

187202
return PagedListResponse(
188-
result=self.result.active_numbers,
203+
result=self.content(),
189204
has_next_page=self.has_next_page,
190-
next_page_info=self._build_next_pagination_info(next_page_result),
191-
next_page=self._next_page_wrapper()
205+
next_page_info=next_page_info,
206+
next_page=next_page_wrapper
192207
)
193208

194-
def _get_next_page_result(self):
195-
"""Fetches the next page result."""
196-
if not self.has_next_page:
197-
return None
198-
199-
current_state = self.result
200-
self.next_page()
201-
next_page_result = self.result
202-
self.result = current_state
203-
204-
return next_page_result
209+
def _get_next_page_wrapper(self, next_page_instance, sync):
210+
"""Returns a function for fetching the next page."""
211+
if sync:
212+
return lambda: next_page_instance.list() if next_page_instance else None
213+
else:
214+
async def async_next_page_wrapper():
215+
return await next_page_instance.list() if next_page_instance else None
216+
return async_next_page_wrapper
205217

206-
def _build_next_pagination_info(self, next_page_result):
207-
"""Constructs and returns structured pagination metadata."""
208-
return {
209-
"result": self.result.active_numbers,
210-
"result.next": (
211-
self.result.active_numbers + next_page_result.active_numbers
212-
if next_page_result else self.result.active_numbers
213-
),
214-
"has_next_page": self.has_next_page,
215-
"has_next_page.next": bool(next_page_result and next_page_result.next_page_token),
216-
}
218+
def _calculate_next_page(self):
219+
self.has_next_page = bool(getattr(self.result, "next_page_token", None))
217220

218-
def _next_page_wrapper(self):
219-
"""Fetches and returns the next page as a formatted PagedListResponse object."""
220-
def wrapper():
221-
self.next_page()
222-
return self.list()
223-
return wrapper
221+
@classmethod
222+
def _initialize(cls, sinch, endpoint):
223+
"""Creates an instance of the paginator skipping first page."""
224+
result = sinch.configuration.transport.request(endpoint)
225+
return cls(sinch, endpoint, yield_first_page=False, result=result)
224226

225227

226228
class AsyncTokenBasedPaginator(TokenBasedPaginator):
227229
"""Asynchronous token-based paginator."""
228230

229231
async def next_page(self):
232+
if not self.has_next_page:
233+
return None
234+
230235
self.endpoint.request_data.page_token = self.result.next_page_token
231-
self.result = await self._sinch.configuration.transport.request(self.endpoint)
232-
self._calculate_next_page()
233-
return self
236+
next_result = await self._sinch.configuration.transport.request(self.endpoint)
237+
238+
return AsyncTokenBasedPaginator(self._sinch, self.endpoint, result=next_result)
234239

235240
def auto_paging_iter(self):
236241
return AsyncPageIterator(self)
@@ -239,3 +244,20 @@ def auto_paging_iter(self):
239244
async def _initialize(cls, sinch, endpoint):
240245
result = await sinch.configuration.transport.request(endpoint)
241246
return cls(sinch, endpoint, result=result)
247+
248+
async def list(self):
249+
"""Returns structured pagination metadata"""
250+
next_page_instance = await self.next_page()
251+
return self._list(next_page_instance, sync=False)
252+
253+
async def iterator(self):
254+
"""Iterates asynchronously over individual items across all pages."""
255+
paginator = self
256+
while paginator:
257+
for item in paginator.content():
258+
yield item
259+
260+
next_page_instance = await paginator.next_page()
261+
if not next_page_instance:
262+
break
263+
paginator = next_page_instance

sinch/domains/numbers/active_numbers.py

+25-18
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from typing import Optional
22
from pydantic import StrictStr, StrictInt
3-
from sinch.core.pagination import TokenBasedPaginatorNumbers, AsyncTokenBasedPaginator
3+
from sinch.core.pagination import TokenBasedPaginator, AsyncTokenBasedPaginator, Paginator
44
from sinch.domains.numbers.base_numbers import BaseNumbers
55
from sinch.domains.numbers.endpoints.active import (
66
GetNumberConfigurationEndpoint, ListActiveNumbersEndpoint, ReleaseNumberFromProjectEndpoint,
77
UpdateNumberConfigurationEndpoint
88
)
99
from sinch.domains.numbers.models.active import (
10-
ListActiveNumbersRequest, ListActiveNumbersResponse
10+
ListActiveNumbersRequest
1111
)
1212
from sinch.domains.numbers.models.active.requests import (
1313
GetNumberConfigurationRequest, UpdateNumberConfigurationRequest, ReleaseNumberFromProjectRequest
@@ -16,7 +16,7 @@
1616
UpdateNumberConfigurationResponse, GetNumberConfigurationResponse, ReleaseNumberFromProjectResponse
1717
)
1818
from sinch.domains.numbers.models.numbers import (
19-
CapabilityTypeValuesList, NumberTypeValues, NumberSearchPatternTypeValues, OrderByValues
19+
CapabilityTypeValuesList, NumberTypeValues, NumberSearchPatternTypeValues, OrderByValues, ActiveNumber
2020
)
2121

2222

@@ -33,17 +33,18 @@ def list(
3333
page_token: Optional[StrictStr] = None,
3434
order_by: Optional[OrderByValues] = None,
3535
**kwargs
36-
) -> TokenBasedPaginatorNumbers:
36+
) -> Paginator[ActiveNumber]:
3737
"""
3838
Search for all active virtual numbers associated with a certain project.
3939
4040
Args:
4141
region_code (StrictStr): ISO 3166-1 alpha-2 country code of the phone number.
42-
number_type (NumberType): Type of number (e.g., "MOBILE", "LOCAL", "TOLL_FREE").
42+
number_type (NumberTypeValues): Type of number (e.g., "MOBILE", "LOCAL", "TOLL_FREE").
4343
number_pattern (Optional[StrictStr]): Specific sequence of digits to search for.
44-
number_search_pattern (Optional[NumberSearchPatternType]):
44+
number_search_pattern (Optional[NumberSearchPatternTypeValues]):
4545
Pattern to apply (e.g., "START", "CONTAINS", "END").
46-
capability (Optional[CapabilityType]): Capabilities required for the number. (e.g., ["SMS", "VOICE"])
46+
capability (Optional[CapabilityTypeValuesList]): Capabilities required for the number.
47+
(e.g., ["SMS", "VOICE"])
4748
page_size (StrictInt): Maximum number of items to return.
4849
page_token (Optional[StrictStr]): Token for the next page of results.
4950
order_by (Optional[OrderByValues]): Field to order the results by. (e.g., "phoneNumber", "displayName")
@@ -54,7 +55,7 @@ def list(
5455
5556
For detailed documentation, visit https://developers.sinch.com
5657
"""
57-
return TokenBasedPaginatorNumbers(
58+
return TokenBasedPaginator(
5859
sinch=self._sinch,
5960
endpoint=ListActiveNumbersEndpoint(
6061
project_id=self._sinch.configuration.project_id,
@@ -72,6 +73,10 @@ def list(
7273
)
7374
)
7475

76+
# TODO: Refactor the update(), get(), release() functions to use Pydantic models:
77+
# - Replace primitive types with Pydantic for better validation and maintainability.
78+
# - Define Pydantic models for request and response data.
79+
# - Improve readability and maintainability through refactoring.
7580
def update(
7681
self,
7782
phone_number: str = None,
@@ -130,15 +135,16 @@ def release(self, phone_number: str) -> ReleaseNumberFromProjectResponse:
130135
class ActiveNumbersWithAsyncPagination(ActiveNumbers):
131136
async def list(
132137
self,
133-
region_code: str,
134-
number_type: str,
135-
number_pattern: str = None,
136-
number_search_pattern: str = None,
137-
capabilities: list = None,
138-
page_size: int = None,
139-
page_token: str = None,
138+
region_code: StrictStr,
139+
number_type: StrictStr,
140+
number_pattern: Optional[StrictStr] = None,
141+
number_search_pattern: Optional[NumberSearchPatternTypeValues] = None,
142+
capability: Optional[CapabilityTypeValuesList] = None,
143+
page_size: Optional[StrictInt] = None,
144+
page_token: Optional[StrictStr] = None,
145+
order_by: Optional[OrderByValues] = None,
140146
**kwargs
141-
) -> ListActiveNumbersResponse:
147+
) -> AsyncTokenBasedPaginator:
142148
return await AsyncTokenBasedPaginator._initialize(
143149
sinch=self._sinch,
144150
endpoint=ListActiveNumbersEndpoint(
@@ -147,10 +153,11 @@ async def list(
147153
region_code=region_code,
148154
number_type=number_type,
149155
page_size=page_size,
150-
capabilities=capabilities,
156+
capabilities=capability,
151157
number_pattern=number_pattern,
152158
number_search_pattern=number_search_pattern,
153-
page_token=page_token
159+
page_token=page_token,
160+
order_by=order_by,
154161
)
155162
)
156163
)

0 commit comments

Comments
 (0)