Skip to content

Commit 210f58e

Browse files
committed
DEVEXP-733: [Python SDK] Auto pagination of elements
- Implemented the automatic and manual pagination of elements. This iterator allows users to navigate through multiple pages while abstracting away the underlying HTTP requests. Signed-off-by: Jessica Matsuoka <jessica.akemi.matsuoka@sinch.com>
1 parent f535801 commit 210f58e

35 files changed

+857
-378
lines changed

sinch/core/adapters/requests_http_transport.py

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
1717
f"Sync HTTP {request_data.http_method} call with headers:"
1818
f" {request_data.headers} and body: {request_data.request_body} to URL: {request_data.url}"
1919
)
20-
2120
response = self.http_session.request(
2221
method=request_data.http_method,
2322
url=request_data.url,

sinch/core/clients/sinch_client_configuration.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,18 @@ def __init__(
3434
self.connection_timeout = connection_timeout
3535
self.sms_api_token = sms_api_token
3636
self.service_plan_id = service_plan_id
37-
self.auth_origin = "auth.sinch.com"
38-
self.numbers_origin = "numbers.api.sinch.com"
39-
self.verification_origin = "verification.api.sinch.com"
40-
self.voice_applications_origin = "callingapi.sinch.com"
41-
self._voice_domain = "{}.api.sinch.com"
37+
self.auth_origin = "https://auth.sinch.com"
38+
self.numbers_origin = "https://numbers.api.sinch.com"
39+
self.verification_origin = "https://verification.api.sinch.com"
40+
self.voice_applications_origin = "https://callingapi.sinch.com"
41+
self._voice_domain = "https://{}.api.sinch.com"
4242
self._voice_region = None
4343
self._conversation_region = "eu"
4444
self._conversation_domain = ".conversation.api.sinch.com"
4545
self._sms_region = "us"
4646
self._sms_region_with_service_plan_id = "us"
47-
self._sms_domain = "zt.{}.sms.api.sinch.com"
48-
self._sms_domain_with_service_plan_id = "{}.sms.api.sinch.com"
47+
self._sms_domain = "https://zt.{}.sms.api.sinch.com"
48+
self._sms_domain_with_service_plan_id = "https://{}.sms.api.sinch.com"
4949
self._sms_authentication = HTTPAuthentication.OAUTH.value
5050
self._templates_region = "eu"
5151
self._templates_domain = ".template.api.sinch.com"

sinch/core/pagination.py

+106-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
from abc import ABC, abstractmethod
2+
from collections import namedtuple
23

34

45
class PageIterator:
5-
def __init__(self, paginator):
6+
def __init__(self, paginator, yield_first_page=False):
67
self.paginator = paginator
8+
self.yield_first_page = yield_first_page
9+
# If yielding the first page, set started to False
10+
self.started = not yield_first_page
711

812
def __iter__(self):
913
return self
1014

1115
def __next__(self):
16+
if not self.started:
17+
self.started = True
18+
return self.paginator
19+
1220
if self.paginator.has_next_page:
1321
return self.paginator.next_page()
1422
else:
@@ -113,32 +121,118 @@ async def _initialize(cls, sinch, endpoint):
113121
return cls(sinch, endpoint, result)
114122

115123

116-
class TokenBasedPaginator(Paginator):
117-
__doc__ = Paginator.__doc__
124+
class TokenBasedPaginatorBase(Paginator):
125+
"""Base paginator for token-based pagination."""
126+
127+
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
131+
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)
134+
135+
def __repr__(self):
136+
pass
118137

119138
def _calculate_next_page(self):
120-
if self.result.next_page_token:
121-
self.has_next_page = True
122-
else:
123-
self.has_next_page = False
139+
self.has_next_page = bool(self.result.next_page_token)
124140

125141
def next_page(self):
142+
"""Fetches the next page and updates pagination state."""
126143
self.endpoint.request_data.page_token = self.result.next_page_token
127144
self.result = self._sinch.configuration.transport.request(self.endpoint)
128145
self._calculate_next_page()
129146
return self
130147

131148
def auto_paging_iter(self):
132-
return PageIterator(self)
149+
"""Returns an iterator for automatic pagination."""
150+
return PageIterator(self, yield_first_page=self.yield_first_page)
133151

134152
@classmethod
135153
def _initialize(cls, sinch, endpoint):
154+
"""Creates an instance of the paginator skipping first page."""
136155
result = sinch.configuration.transport.request(endpoint)
137-
return cls(sinch, endpoint, result)
156+
return cls(sinch, endpoint, yield_first_page=False, result=result)
138157

139158

140-
class AsyncTokenBasedPaginator(TokenBasedPaginator):
141-
__doc__ = TokenBasedPaginator.__doc__
159+
class TokenBasedPaginator(TokenBasedPaginatorBase):
160+
"""Paginator that skips the first page."""
161+
pass
162+
163+
164+
class TokenBasedPaginatorNumbers(TokenBasedPaginatorBase):
165+
"""
166+
Paginator for handling token-based pagination specifically for phone numbers.
167+
168+
This paginator is designed to iterate through phone numbers automatically or manually, fetching new pages as needed.
169+
It extends the TokenBasedPaginatorBase class and provides additional methods for number-specific pagination.
170+
"""
171+
172+
def __init__(self, sinch, endpoint):
173+
super().__init__(sinch, endpoint, yield_first_page=True)
174+
175+
def numbers_iterator(self):
176+
"""Iterates through numbers individually, fetching new pages as needed."""
177+
while True:
178+
if self.result and self.result.active_numbers:
179+
yield from self.result.active_numbers
180+
181+
if not self.has_next_page:
182+
break
183+
184+
self.next_page()
185+
186+
def list(self):
187+
"""Returns the first page's numbers along with pagination metadata."""
188+
189+
PagedListResponse = namedtuple(
190+
"PagedResponse", ["result", "has_next_page", "next_page_info", "next_page"]
191+
)
192+
193+
next_page_result = self._get_next_page_result()
194+
195+
return PagedListResponse(
196+
result=self.result.active_numbers,
197+
has_next_page=self.has_next_page,
198+
next_page_info=self._build_next_pagination_info(next_page_result),
199+
next_page=self._next_page_wrapper()
200+
)
201+
202+
def _get_next_page_result(self):
203+
"""Fetches the next page result."""
204+
if not self.has_next_page:
205+
return None
206+
207+
current_state = self.result
208+
self.next_page()
209+
next_page_result = self.result
210+
self.result = current_state
211+
212+
return next_page_result
213+
214+
def _build_next_pagination_info(self, next_page_result):
215+
"""Constructs and returns structured pagination metadata."""
216+
return {
217+
"result": self.result.active_numbers,
218+
"result.next": (
219+
self.result.active_numbers + next_page_result.active_numbers
220+
if next_page_result else self.result.active_numbers
221+
),
222+
"has_next_page": self.has_next_page,
223+
"has_next_page.next": bool(next_page_result and next_page_result.next_page_token),
224+
}
225+
226+
def _next_page_wrapper(self):
227+
"""Fetches and returns the next page as a formatted PagedListResponse object."""
228+
def wrapper():
229+
self.next_page()
230+
return self.list()
231+
return wrapper
232+
233+
234+
class AsyncTokenBasedPaginator(TokenBasedPaginatorBase):
235+
"""Asynchronous token-based paginator."""
142236

143237
async def next_page(self):
144238
self.endpoint.request_data.page_token = self.result.next_page_token
@@ -152,4 +246,4 @@ def auto_paging_iter(self):
152246
@classmethod
153247
async def _initialize(cls, sinch, endpoint):
154248
result = await sinch.configuration.transport.request(endpoint)
155-
return cls(sinch, endpoint, result)
249+
return cls(sinch, endpoint, result=result)

sinch/domains/numbers/__init__.py

+1-130
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
1-
from sinch.core.pagination import TokenBasedPaginator, AsyncTokenBasedPaginator
21
from sinch.domains.numbers.available_numbers import AvailableNumbers
2+
from sinch.domains.numbers.active_numbers import ActiveNumbers, ActiveNumbersWithAsyncPagination
33
from sinch.domains.numbers.endpoints.callbacks.get_configuration import GetNumbersCallbackConfigurationEndpoint
44
from sinch.domains.numbers.endpoints.callbacks.update_configuration import UpdateNumbersCallbackConfigurationEndpoint
5-
from sinch.domains.numbers.endpoints.active.list_active_numbers_for_project import ListActiveNumbersEndpoint
6-
from sinch.domains.numbers.endpoints.active.update_number_configuration import UpdateNumberConfigurationEndpoint
7-
from sinch.domains.numbers.endpoints.active.get_number_configuration import GetNumberConfigurationEndpoint
8-
from sinch.domains.numbers.endpoints.active.release_number_from_project import ReleaseNumberFromProjectEndpoint
95
from sinch.domains.numbers.endpoints.regions.list_available_regions import ListAvailableRegionsEndpoint
106

117
from sinch.domains.numbers.models.regions.requests import ListAvailableRegionsForProjectRequest
12-
from sinch.domains.numbers.models.active.requests import (
13-
ListActiveNumbersRequest, GetNumberConfigurationRequest,
14-
UpdateNumberConfigurationRequest, ReleaseNumberFromProjectRequest
15-
)
168
from sinch.domains.numbers.models.regions.responses import ListAvailableRegionsResponse
17-
from sinch.domains.numbers.models.active.responses import (
18-
ListActiveNumbersResponse, UpdateNumberConfigurationResponse,
19-
GetNumberConfigurationResponse, ReleaseNumberFromProjectResponse
20-
)
219
from sinch.domains.numbers.models.callbacks.responses import (
2210
GetNumbersCallbackConfigurationResponse,
2311
UpdateNumbersCallbackConfigurationResponse
@@ -27,123 +15,6 @@
2715
)
2816

2917

30-
class ActiveNumbers:
31-
def __init__(self, sinch):
32-
self._sinch = sinch
33-
34-
def list(
35-
self,
36-
region_code: str,
37-
number_type: str,
38-
number_pattern: str = None,
39-
number_search_pattern: str = None,
40-
capabilities: list = None,
41-
page_size: int = None,
42-
page_token: str = None
43-
) -> ListActiveNumbersResponse:
44-
"""
45-
Search for all active virtual numbers associated with a certain project.
46-
For additional documentation, see https://www.sinch.com and visit our developer portal.
47-
"""
48-
return TokenBasedPaginator._initialize(
49-
sinch=self._sinch,
50-
endpoint=ListActiveNumbersEndpoint(
51-
project_id=self._sinch.configuration.project_id,
52-
request_data=ListActiveNumbersRequest(
53-
region_code=region_code,
54-
number_type=number_type,
55-
page_size=page_size,
56-
capabilities=capabilities,
57-
number_pattern=number_pattern,
58-
number_search_pattern=number_search_pattern,
59-
page_token=page_token
60-
)
61-
)
62-
)
63-
64-
def update(
65-
self,
66-
phone_number: str = None,
67-
display_name: str = None,
68-
sms_configuration: dict = None,
69-
voice_configuration: dict = None,
70-
app_id: str = None
71-
) -> UpdateNumberConfigurationResponse:
72-
"""
73-
Make updates to the configuration of your virtual number.
74-
Update the display name, change the currency type, or reconfigure for either SMS and/or Voice.
75-
For additional documentation, see https://www.sinch.com and visit our developer portal.
76-
"""
77-
return self._sinch.configuration.transport.request(
78-
UpdateNumberConfigurationEndpoint(
79-
project_id=self._sinch.configuration.project_id,
80-
request_data=UpdateNumberConfigurationRequest(
81-
phone_number=phone_number,
82-
display_name=display_name,
83-
sms_configuration=sms_configuration,
84-
voice_configuration=voice_configuration,
85-
app_id=app_id
86-
)
87-
)
88-
)
89-
90-
def get(self, phone_number: str) -> GetNumberConfigurationResponse:
91-
"""
92-
List of configuration settings for your virtual number.
93-
For additional documentation, see https://www.sinch.com and visit our developer portal.
94-
"""
95-
return self._sinch.configuration.transport.request(
96-
GetNumberConfigurationEndpoint(
97-
project_id=self._sinch.configuration.project_id,
98-
request_data=GetNumberConfigurationRequest(
99-
phone_number=phone_number
100-
)
101-
)
102-
)
103-
104-
def release(self, phone_number: str) -> ReleaseNumberFromProjectResponse:
105-
"""
106-
Release numbers you no longer need from your project.
107-
For additional documentation, see https://www.sinch.com and visit our developer portal.
108-
"""
109-
return self._sinch.configuration.transport.request(
110-
ReleaseNumberFromProjectEndpoint(
111-
project_id=self._sinch.configuration.project_id,
112-
request_data=ReleaseNumberFromProjectRequest(
113-
phone_number=phone_number
114-
)
115-
)
116-
)
117-
118-
119-
class ActiveNumbersWithAsyncPagination(ActiveNumbers):
120-
async def list(
121-
self,
122-
region_code: str,
123-
number_type: str,
124-
number_pattern: str = None,
125-
number_search_pattern: str = None,
126-
capabilities: list = None,
127-
page_size: int = None,
128-
page_token: str = None
129-
) -> ListActiveNumbersResponse:
130-
return await AsyncTokenBasedPaginator._initialize(
131-
sinch=self._sinch,
132-
endpoint=ListActiveNumbersEndpoint(
133-
project_id=self._sinch.configuration.project_id,
134-
request_data=ListActiveNumbersRequest(
135-
region_code=region_code,
136-
number_type=number_type,
137-
page_size=page_size,
138-
capabilities=capabilities,
139-
number_pattern=number_pattern,
140-
number_search_pattern=number_search_pattern,
141-
page_token=page_token
142-
)
143-
)
144-
)
145-
146-
14718
class AvailableRegions:
14819
def __init__(self, sinch):
14920
self._sinch = sinch

0 commit comments

Comments
 (0)