Skip to content

Commit 482b1f8

Browse files
committed
refactor: improve async/sync pagination and rename pagination methods
1 parent 9aad89a commit 482b1f8

13 files changed

+62
-124
lines changed

sinch/core/pagination.py

+28-36
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
from abc import ABC, abstractmethod
22
from collections import namedtuple
33
from typing import Generic
4-
from sinch.domains.numbers.models.numbers import BM
4+
from sinch.core.types import BM
55

66

77
class PageIterator:
88
def __init__(self, paginator, yield_first_page=False):
99
self.paginator = paginator
10-
self.yield_first_page = yield_first_page
1110
# If yielding the first page, set started to False
1211
self.started = not yield_first_page
1312

@@ -27,25 +26,25 @@ def __next__(self):
2726

2827

2928
class AsyncPageIterator:
30-
def __init__(self, paginator):
29+
def __init__(self, paginator, yield_first_page=False):
3130
self.paginator = paginator
32-
self.first_yield = True
31+
self.started = not yield_first_page
3332

3433
def __aiter__(self):
3534
return self
3635

3736
async def __anext__(self):
38-
if self.first_yield:
39-
self.first_yield = False
37+
if not self.started:
38+
self.started = True
4039
return self.paginator
4140

4241
if self.paginator.has_next_page:
4342
next_paginator = await self.paginator.next_page()
4443
if next_paginator:
4544
self.paginator = next_paginator
4645
return self.paginator
47-
48-
raise StopAsyncIteration
46+
else:
47+
raise StopAsyncIteration
4948

5049

5150
class Paginator(ABC, Generic[BM]):
@@ -82,8 +81,9 @@ def content(self):
8281
def iterator(self):
8382
pass
8483

85-
@abstractmethod
86-
def auto_paging_iter(self):
84+
# TODO: Make get_content() method abstract in Parent class as we implement in the other domains:
85+
# - Refactor pydantic models in other domains to have a content property.
86+
def get_content(self):
8787
pass
8888

8989
@abstractmethod
@@ -134,7 +134,7 @@ async def next_page(self):
134134
return self
135135

136136
def auto_paging_iter(self):
137-
return AsyncPageIterator(self)
137+
return AsyncPageIterator(self, yield_first_page=True)
138138

139139
@classmethod
140140
async def _initialize(cls, sinch, endpoint):
@@ -147,7 +147,6 @@ class TokenBasedPaginator(Paginator[BM]):
147147

148148
def __init__(self, sinch, endpoint, yield_first_page=False, result=None):
149149
super().__init__(sinch, endpoint, result or sinch.configuration.transport.request(endpoint))
150-
self.yield_first_page = yield_first_page
151150

152151
def content(self) -> list[BM]:
153152
return getattr(self.result, "content", [])
@@ -162,10 +161,6 @@ def next_page(self):
162161

163162
return self.__class__(self._sinch, self.endpoint, result=next_result)
164163

165-
def auto_paging_iter(self):
166-
"""Returns an iterator for automatic pagination."""
167-
return PageIterator(self, yield_first_page=True)
168-
169164
def iterator(self):
170165
"""Iterates over individual items across all pages."""
171166
paginator = self
@@ -177,13 +172,13 @@ def iterator(self):
177172
break
178173
paginator = next_page_instance
179174

180-
def list(self):
175+
def get_content(self):
181176
"""Returns structured pagination metadata along with the first page's content (sync)."""
182177
next_page_instance = self.next_page()
183-
return self._list(next_page_instance, sync=True)
178+
return self._get_content(next_page_instance, sync=True)
184179

185-
def _list(self, next_page_instance, sync=True):
186-
"""Core logic for `list()`, shared between sync and async versions."""
180+
def _get_content(self, next_page_instance, sync=True):
181+
"""Core logic for `get_content()`, shared between sync and async versions."""
187182
PagedListResponse = namedtuple(
188183
"PagedResponse", ["result", "has_next_page", "next_page_info", "next_page"]
189184
)
@@ -209,10 +204,10 @@ def _list(self, next_page_instance, sync=True):
209204
def _get_next_page_wrapper(self, next_page_instance, sync):
210205
"""Returns a function for fetching the next page."""
211206
if sync:
212-
return lambda: next_page_instance.list() if next_page_instance else None
207+
return lambda: next_page_instance.get_content() if next_page_instance else None
213208
else:
214209
async def async_next_page_wrapper():
215-
return await next_page_instance.list() if next_page_instance else None
210+
return await next_page_instance.get_content() if next_page_instance else None
216211
return async_next_page_wrapper
217212

218213
def _calculate_next_page(self):
@@ -235,20 +230,7 @@ async def next_page(self):
235230
self.endpoint.request_data.page_token = self.result.next_page_token
236231
next_result = await self._sinch.configuration.transport.request(self.endpoint)
237232

238-
return AsyncTokenBasedPaginator(self._sinch, self.endpoint, result=next_result)
239-
240-
def auto_paging_iter(self):
241-
return AsyncPageIterator(self)
242-
243-
@classmethod
244-
async def _initialize(cls, sinch, endpoint):
245-
result = await sinch.configuration.transport.request(endpoint)
246-
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)
233+
return self.__class__(self._sinch, self.endpoint, result=next_result)
252234

253235
async def iterator(self):
254236
"""Iterates asynchronously over individual items across all pages."""
@@ -261,3 +243,13 @@ async def iterator(self):
261243
if not next_page_instance:
262244
break
263245
paginator = next_page_instance
246+
247+
async def get_content(self):
248+
"""Returns structured pagination metadata"""
249+
next_page_instance = await self.next_page()
250+
return self._get_content(next_page_instance, sync=False)
251+
252+
@classmethod
253+
async def _initialize(cls, sinch, endpoint):
254+
result = await sinch.configuration.transport.request(endpoint)
255+
return cls(sinch, endpoint, yield_first_page=False, 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/active_numbers.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def list(
2828
number_type: NumberTypeValues,
2929
number_pattern: Optional[StrictStr] = None,
3030
number_search_pattern: Optional[NumberSearchPatternTypeValues] = None,
31-
capability: Optional[CapabilityTypeValuesList] = None,
31+
capabilities: Optional[CapabilityTypeValuesList] = None,
3232
page_size: Optional[StrictInt] = None,
3333
page_token: Optional[StrictStr] = None,
3434
order_by: Optional[OrderByValues] = None,
@@ -43,7 +43,7 @@ def list(
4343
number_pattern (Optional[StrictStr]): Specific sequence of digits to search for.
4444
number_search_pattern (Optional[NumberSearchPatternTypeValues]):
4545
Pattern to apply (e.g., "START", "CONTAINS", "END").
46-
capability (Optional[CapabilityTypeValuesList]): Capabilities required for the number.
46+
capabilities (Optional[CapabilityTypeValuesList]): Capabilities required for the number.
4747
(e.g., ["SMS", "VOICE"])
4848
page_size (StrictInt): Maximum number of items to return.
4949
page_token (Optional[StrictStr]): Token for the next page of results.
@@ -63,7 +63,7 @@ def list(
6363
region_code=region_code,
6464
number_type=number_type,
6565
page_size=page_size,
66-
capabilities=capability,
66+
capabilities=capabilities,
6767
number_pattern=number_pattern,
6868
number_search_pattern=number_search_pattern,
6969
page_token=page_token,
@@ -139,7 +139,7 @@ async def list(
139139
number_type: StrictStr,
140140
number_pattern: Optional[StrictStr] = None,
141141
number_search_pattern: Optional[NumberSearchPatternTypeValues] = None,
142-
capability: Optional[CapabilityTypeValuesList] = None,
142+
capabilities: Optional[CapabilityTypeValuesList] = None,
143143
page_size: Optional[StrictInt] = None,
144144
page_token: Optional[StrictStr] = None,
145145
order_by: Optional[OrderByValues] = None,
@@ -153,7 +153,7 @@ async def list(
153153
region_code=region_code,
154154
number_type=number_type,
155155
page_size=page_size,
156-
capabilities=capability,
156+
capabilities=capabilities,
157157
number_pattern=number_pattern,
158158
number_search_pattern=number_search_pattern,
159159
page_token=page_token,

sinch/domains/numbers/endpoints/active/list_active_numbers_endpoint.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77

88
class ListActiveNumbersEndpoint(NumbersEndpoint):
9+
"""
10+
Endpoint to list all active numbers for a project.
11+
"""
912
ENDPOINT_URL = "{origin}/v1/projects/{project_id}/activeNumbers"
1013
HTTP_METHOD = HTTPMethods.GET.value
1114
HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value
@@ -18,9 +21,6 @@ def __init__(self, project_id: str, request_data: ListActiveNumbersRequest):
1821
def build_query_params(self) -> dict:
1922
return self.request_data.model_dump(exclude_none=True, by_alias=True)
2023

21-
def request_body(self) -> str:
22-
return ""
23-
2424
def handle_response(self, response: HTTPResponse) -> ListActiveNumbersResponse:
2525
super(ListActiveNumbersEndpoint, self).handle_response(response)
2626
return self.process_response_model(response.body, ListActiveNumbersResponse)

sinch/domains/numbers/endpoints/available/activate_number_endpoint.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from sinch.core.enums import HTTPAuthentication, HTTPMethods
23
from sinch.core.models.http_response import HTTPResponse
34
from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint
@@ -16,6 +17,11 @@ class ActivateNumberEndpoint(NumbersEndpoint):
1617
def __init__(self, project_id: str, request_data: ActivateNumberRequest):
1718
super(ActivateNumberEndpoint, self).__init__(project_id, request_data)
1819

20+
def request_body(self) -> str:
21+
# Convert the request data to a dictionary and remove None values
22+
request_data = self.request_data.model_dump(by_alias=True, exclude_none=True)
23+
return json.dumps(request_data)
24+
1925
def handle_response(self, response: HTTPResponse) -> ActivateNumberResponse:
2026
try:
2127
super(ActivateNumberEndpoint, self).handle_response(response)

sinch/domains/numbers/endpoints/available/list_available_numbers_endpoint.py

-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ def __init__(self, project_id: str, request_data: ListAvailableNumbersRequest):
2121
def build_query_params(self) -> dict:
2222
return self.request_data.model_dump(exclude_none=True, by_alias=True)
2323

24-
def request_body(self) -> str:
25-
return ""
26-
2724
def handle_response(self, response: HTTPResponse) -> list[Number]:
2825
"""
2926
Processes the API response and maps it to a response model.

sinch/domains/numbers/endpoints/available/rent_any_number_endpoint.py

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from sinch.core.models.http_response import HTTPResponse
23
from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint
34
from sinch.core.enums import HTTPAuthentication, HTTPMethods
@@ -17,6 +18,10 @@ def __init__(self, project_id: str, request_data: RentAnyNumberRequest):
1718
super(RentAnyNumberEndpoint, self).__init__(project_id, request_data)
1819
self.request_data = request_data
1920

21+
def request_body(self) -> str:
22+
request_data = self.request_data.model_dump(by_alias=True, exclude_none=True)
23+
return json.dumps(request_data)
24+
2025
def handle_response(self, response: HTTPResponse) -> RentAnyNumberResponse:
2126
"""
2227
Handles the response from the API call.

sinch/domains/numbers/endpoints/available/search_for_number_endpoint.py

-6
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ class SearchForNumberEndpoint(NumbersEndpoint):
1717
def __init__(self, project_id: str, request_data: CheckNumberAvailabilityRequest):
1818
super(SearchForNumberEndpoint, self).__init__(project_id, request_data)
1919

20-
def build_query_params(self) -> dict:
21-
return self.request_data.model_dump(exclude_none=True, by_alias=True)
22-
23-
def request_body(self) -> str:
24-
return ""
25-
2620
def handle_response(self, response: HTTPResponse) -> CheckNumberAvailabilityResponse:
2721
"""
2822
Processes the API response and maps it to a response

sinch/domains/numbers/endpoints/numbers_endpoint.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import json
21
from abc import ABC
32
from typing import Type
43
from sinch.core.models.http_response import HTTPResponse
54
from sinch.core.endpoint import HTTPEndpoint
5+
from sinch.core.types import BM
66
from sinch.domains.numbers.exceptions import NumbersException
7-
from sinch.domains.numbers.models.numbers import BM, NotFoundError
7+
from sinch.domains.numbers.models.numbers import NotFoundError
88

99

1010
class NumbersEndpoint(HTTPEndpoint, ABC):
@@ -38,9 +38,7 @@ def request_body(self) -> str:
3838
Returns:
3939
str: The request body as a JSON string.
4040
"""
41-
# Convert the request data to a dictionary and remove None values
42-
request_data = self.request_data.model_dump(by_alias=True, exclude_none=True)
43-
return json.dumps(request_data)
41+
return ""
4442

4543
def process_response_model(self, response_body: dict, response_model: Type[BM]) -> BM:
4644
"""

sinch/domains/numbers/models/numbers.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from datetime import datetime
22
from decimal import Decimal
3-
from typing import Annotated, Literal, Optional, TypeVar, Union
4-
from pydantic import BaseModel, ConfigDict, conlist, Field, StrictBool, StrictInt, StrictStr
3+
from typing import Annotated, Literal, Optional, Union
4+
from pydantic import ConfigDict, conlist, Field, StrictBool, StrictInt, StrictStr
55
from sinch.domains.numbers.models.base_model_numbers import BaseModelConfigRequest, BaseModelConfigResponse
66

7-
BM = TypeVar("BM", bound=BaseModel)
87

98
NumberTypeValues = Union[Literal["MOBILE", "LOCAL", "TOLL_FREE"], StrictStr]
109
CapabilityTypeValuesList = conlist(Union[Literal["SMS", "VOICE"], StrictStr], min_length=1)

tests/e2e/numbers/features/steps/numbers.steps.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from behave import given, when, then
33
from decimal import Decimal
44
from sinch import SinchClient
5-
from sinch.core.pagination import TokenBasedPaginator
65
from sinch.domains.numbers.exceptions import NumberNotFoundException
76
from sinch.domains.numbers.models.available.activate_number_response import ActivateNumberResponse
87
from sinch.domains.numbers.models.available.rent_any_number_response import RentAnyNumberResponse
@@ -230,4 +229,4 @@ def step_when_release_phone_number(context, phone_number):
230229

231230
@then('the response contains details about the phone number "{phone_number}" to be released')
232231
def step_then_response_contains_released_number(context, phone_number):
233-
pass # Placeholder
232+
pass # Placeholder

tests/unit/domains/numbers/models/active/response/test_list_active_numbers_response_model.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,4 @@ def test_list_active_numbers_response_expects_correct_mapping(test_data):
9393

9494
def test_list_active_numbers_response_expects_content_mapping(test_data):
9595
response = ListActiveNumbersResponse(**test_data)
96-
assert response.content == response.active_numbers
96+
assert response.content == response.active_numbers

0 commit comments

Comments
 (0)