Skip to content

Commit 2bb078e

Browse files
authored
Merge pull request #46 from sinch/DEVEXP-733_Auto-Pagination
DEVEXP-733: Auto pagination of elements
2 parents f535801 + 5048bc8 commit 2bb078e

Some content is hidden

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

43 files changed

+973
-583
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/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

+77-36
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,56 @@
11
from abc import ABC, abstractmethod
2+
from typing import Generic
3+
from sinch.core.types import BM
24

35

46
class PageIterator:
5-
def __init__(self, paginator):
7+
def __init__(self, paginator, yield_first_page=False):
68
self.paginator = paginator
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:
13-
return self.paginator.next_page()
21+
self.paginator = self.paginator.next_page()
22+
return self.paginator
1423
else:
1524
raise StopIteration
1625

1726

1827
class AsyncPageIterator:
19-
def __init__(self, paginator):
28+
def __init__(self, paginator, yield_first_page=False):
2029
self.paginator = paginator
30+
self.started = not yield_first_page
2131

2232
def __aiter__(self):
2333
return self
2434

2535
async def __anext__(self):
36+
if not self.started:
37+
self.started = True
38+
return self.paginator
39+
2640
if self.paginator.has_next_page:
27-
return await self.paginator.next_page()
41+
next_paginator = await self.paginator.next_page()
42+
if next_paginator:
43+
self.paginator = next_paginator
44+
return self.paginator
2845
else:
2946
raise StopAsyncIteration
3047

3148

32-
class Paginator(ABC):
49+
class Paginator(ABC, Generic[BM]):
3350
"""
3451
Pagination response object.
35-
36-
auto_paging_iter method returns an iterator object that can be used for iterator-based page traversing.
37-
For example:
38-
for page in paginated_response.auto_paging_iter():
39-
...process page object
40-
41-
For manual pagination use has_next_page property with next_page() method.
42-
For example:
43-
if paginated_response.has_next_page:
44-
paginated_response = paginated_response.next_page()
4552
"""
46-
def __init__(self, sinch, endpoint, result):
53+
def __init__(self, sinch, endpoint, result: BM):
4754
self._sinch = sinch
4855
self.result = result
4956
self.endpoint = endpoint
@@ -53,8 +60,14 @@ def __init__(self, sinch, endpoint, result):
5360
def __repr__(self):
5461
return "Paginated response content: " + str(self.result)
5562

56-
@abstractmethod
57-
def auto_paging_iter(self):
63+
# TODO: Make content() method abstract in Parent class as we implement in the other domains:
64+
# - Refactor pydantic models in other domains to have a content property.
65+
def content(self):
66+
pass
67+
68+
# TODO: Make iterator() method abstract in Parent class as we implement in the other domains:
69+
# - Refactor pydantic models in other domains to have a content property.
70+
def iterator(self):
5871
pass
5972

6073
@abstractmethod
@@ -105,51 +118,79 @@ async def next_page(self):
105118
return self
106119

107120
def auto_paging_iter(self):
108-
return AsyncPageIterator(self)
121+
return AsyncPageIterator(self, yield_first_page=True)
109122

110123
@classmethod
111124
async def _initialize(cls, sinch, endpoint):
112125
result = await sinch.configuration.transport.request(endpoint)
113126
return cls(sinch, endpoint, result)
114127

115128

116-
class TokenBasedPaginator(Paginator):
117-
__doc__ = Paginator.__doc__
129+
class TokenBasedPaginator(Paginator[BM]):
130+
"""Base paginator for token-based pagination with explicit page navigation and metadata."""
118131

119-
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
132+
def __init__(self, sinch, endpoint, result=None):
133+
super().__init__(sinch, endpoint, result or sinch.configuration.transport.request(endpoint))
134+
135+
def content(self) -> list[BM]:
136+
return getattr(self.result, "content", [])
124137

125138
def next_page(self):
139+
"""Returns a new paginator instance for the next page."""
140+
if not self.has_next_page:
141+
return None
142+
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

131-
def auto_paging_iter(self):
132-
return PageIterator(self)
148+
def iterator(self):
149+
"""Iterates over individual items across all pages."""
150+
paginator = self
151+
while paginator:
152+
yield from paginator.content()
153+
154+
next_page_instance = paginator.next_page()
155+
if not next_page_instance:
156+
break
157+
paginator = next_page_instance
158+
159+
def _calculate_next_page(self):
160+
self.has_next_page = bool(getattr(self.result, "next_page_token", None))
133161

134162
@classmethod
135163
def _initialize(cls, sinch, endpoint):
164+
"""Creates an instance of the paginator skipping first page."""
136165
result = sinch.configuration.transport.request(endpoint)
137-
return cls(sinch, endpoint, result)
166+
return cls(sinch, endpoint, result=result)
138167

139168

140169
class AsyncTokenBasedPaginator(TokenBasedPaginator):
141-
__doc__ = TokenBasedPaginator.__doc__
170+
"""Asynchronous token-based paginator."""
142171

143172
async def next_page(self):
173+
if not self.has_next_page:
174+
return None
175+
144176
self.endpoint.request_data.page_token = self.result.next_page_token
145-
self.result = await self._sinch.configuration.transport.request(self.endpoint)
146-
self._calculate_next_page()
147-
return self
177+
next_result = await self._sinch.configuration.transport.request(self.endpoint)
148178

149-
def auto_paging_iter(self):
150-
return AsyncPageIterator(self)
179+
return self.__class__(self._sinch, self.endpoint, result=next_result)
180+
181+
async def iterator(self):
182+
"""Iterates asynchronously over individual items across all pages."""
183+
paginator = self
184+
while paginator:
185+
for item in paginator.content():
186+
yield item
187+
188+
next_page_instance = await paginator.next_page()
189+
if not next_page_instance:
190+
break
191+
paginator = next_page_instance
151192

152193
@classmethod
153194
async def _initialize(cls, sinch, endpoint):
154195
result = await sinch.configuration.transport.request(endpoint)
155-
return cls(sinch, endpoint, result)
196+
return cls(sinch, endpoint, result=result)

sinch/core/types.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from typing import TypeVar
2+
from pydantic import BaseModel
3+
4+
BM = TypeVar("BM", bound=BaseModel)

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)