Skip to content

Commit 96997cf

Browse files
committed
DEVEXP-733: Refactor Token Paginator
1 parent f597eca commit 96997cf

File tree

4 files changed

+69
-207
lines changed

4 files changed

+69
-207
lines changed

sinch/core/pagination.py

-49
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from abc import ABC, abstractmethod
2-
from collections import namedtuple
32
from typing import Generic
43
from sinch.core.types import BM
54

@@ -81,11 +80,6 @@ def content(self):
8180
def iterator(self):
8281
pass
8382

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):
87-
pass
88-
8983
@abstractmethod
9084
def next_page(self):
9185
pass
@@ -172,44 +166,6 @@ def iterator(self):
172166
break
173167
paginator = next_page_instance
174168

175-
def get_content(self):
176-
"""Returns structured pagination metadata along with the first page's content (sync)."""
177-
next_page_instance = self.next_page()
178-
return self._get_content(next_page_instance, sync=True)
179-
180-
def _get_content(self, next_page_instance, sync=True):
181-
"""Core logic for `get_content()`, shared between sync and async versions."""
182-
PagedListResponse = namedtuple(
183-
"PagedResponse", ["result", "has_next_page", "next_page_info", "next_page"]
184-
)
185-
186-
next_page_info = {
187-
"result": self.content(),
188-
"result.next": (
189-
self.content() + (next_page_instance.content() if next_page_instance else [])
190-
),
191-
"has_next_page": self.has_next_page,
192-
"has_next_page.next": bool(next_page_instance and next_page_instance.has_next_page),
193-
}
194-
195-
next_page_wrapper = self._get_next_page_wrapper(next_page_instance, sync)
196-
197-
return PagedListResponse(
198-
result=self.content(),
199-
has_next_page=self.has_next_page,
200-
next_page_info=next_page_info,
201-
next_page=next_page_wrapper
202-
)
203-
204-
def _get_next_page_wrapper(self, next_page_instance, sync):
205-
"""Returns a function for fetching the next page."""
206-
if sync:
207-
return lambda: next_page_instance.get_content() if next_page_instance else None
208-
else:
209-
async def async_next_page_wrapper():
210-
return await next_page_instance.get_content() if next_page_instance else None
211-
return async_next_page_wrapper
212-
213169
def _calculate_next_page(self):
214170
self.has_next_page = bool(getattr(self.result, "next_page_token", None))
215171

@@ -244,11 +200,6 @@ async def iterator(self):
244200
break
245201
paginator = next_page_instance
246202

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-
252203
@classmethod
253204
async def _initialize(cls, sinch, endpoint):
254205
result = await sinch.configuration.transport.request(endpoint)

tests/conftest.py

-24
Original file line numberDiff line numberDiff line change
@@ -186,30 +186,6 @@ def token_based_pagination_request_data():
186186
)
187187

188188

189-
@pytest.fixture
190-
def first_token_based_pagination_response():
191-
return TokenBasedPaginationResponse(
192-
pig_dogs=["Walaszek", "Połać"],
193-
next_page_token="za30%wsze"
194-
)
195-
196-
197-
@pytest.fixture
198-
def second_token_based_pagination_response():
199-
return TokenBasedPaginationResponse(
200-
pig_dogs=["Bartosz", "Piotr"],
201-
next_page_token="ka#556"
202-
)
203-
204-
205-
@pytest.fixture
206-
def third_token_based_pagination_response():
207-
return TokenBasedPaginationResponse(
208-
pig_dogs=["Madrid", "Spain"],
209-
next_page_token=""
210-
)
211-
212-
213189
@pytest.fixture
214190
def int_based_pagination_request_data():
215191
return IntBasedPaginationRequest(

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,13 @@ def step_when_list_phone_numbers(context):
165165
number_type='LOCAL'
166166
)
167167
# Get the first page
168-
context.response = response.get_content()
168+
context.response = response.content()
169169

170170

171171
@then('the response contains "{count}" phone numbers')
172172
def step_then_response_contains_x_phone_numbers(context, count):
173-
assert len(context.response.result) == int(count), \
174-
f'Expected {count}, got {len(context.response.data)}'
173+
assert len(context.response) == int(count), \
174+
f'Expected {count}, got {len(context.response)}'
175175

176176
@when("I send a request to list all the phone numbers")
177177
def step_when_list_all_phone_numbers(context):

tests/unit/test_pagination.py

+66-131
Original file line numberDiff line numberDiff line change
@@ -136,170 +136,105 @@ async def test_page_int_iterator_async_using_auto_pagination(
136136
assert not int_based_paginator.result.pig_dogs
137137

138138

139+
# Helper function to initialize token paginators
140+
def initialize_token_paginator(endpoint_mock, request_data, responses, is_async=False):
141+
client = AsyncMock() if is_async else Mock()
142+
client.configuration.transport.request.side_effect = responses
143+
144+
endpoint_mock.request_data = request_data
145+
146+
if is_async:
147+
return AsyncTokenBasedPaginator._initialize(sinch=client, endpoint=endpoint_mock)
148+
return TokenBasedPaginator(sinch=client, endpoint=endpoint_mock)
149+
150+
EXPECTED_PHONE_NUMBERS = [
151+
'+12345678901', '+12345678902', '+12345678903', '+12345678904', '+12345678905'
152+
]
153+
154+
139155
def test_page_token_iterator_sync_using_manual_pagination(
140-
token_based_pagination_request_data,
141-
first_token_based_pagination_response,
142-
second_token_based_pagination_response,
143-
third_token_based_pagination_response
156+
token_based_pagination_request_data,
157+
mock_pagination_active_number_responses
144158
):
145-
endpoint = Mock()
146-
endpoint.request_data = token_based_pagination_request_data
147-
sinch_client = Mock()
148-
149-
sinch_client.configuration.transport.request.side_effect = [
150-
first_token_based_pagination_response,
151-
second_token_based_pagination_response,
152-
third_token_based_pagination_response
153-
]
154-
token_based_paginator = TokenBasedPaginator._initialize(
155-
sinch=sinch_client,
156-
endpoint=endpoint
159+
token_based_paginator = initialize_token_paginator(
160+
endpoint_mock=Mock(),
161+
request_data=token_based_pagination_request_data,
162+
responses=mock_pagination_active_number_responses
157163
)
158-
assert token_based_paginator
164+
assert token_based_paginator is not None
159165

160166
page_counter = 1
167+
active_numbers_list = [num.phone_number for num in token_based_paginator.content()]
168+
161169
while token_based_paginator.has_next_page:
162170
token_based_paginator = token_based_paginator.next_page()
163171
page_counter += 1
164172
assert isinstance(token_based_paginator, TokenBasedPaginator)
173+
active_numbers_list.extend(num.phone_number for num in token_based_paginator.content())
165174

166175
assert page_counter == 3
176+
assert active_numbers_list == EXPECTED_PHONE_NUMBERS
167177

168178

169-
def test_page_token_iterator_numbers_sync_using_auto_pagination_expects_iter(token_based_pagination_request_data,
170-
mock_pagination_active_number_responses):
171-
""" Test that the pagination iterates correctly through multiple items. """
172-
173-
sinch_client = Mock()
174-
sinch_client.configuration.transport.request.side_effect = mock_pagination_active_number_responses
175-
endpoint = Mock()
176-
endpoint.request_data = token_based_pagination_request_data
177-
178-
token_based_paginator = TokenBasedPaginator(
179-
sinch=sinch_client,
180-
endpoint=endpoint
181-
)
182-
assert token_based_paginator
183-
184-
number_counter = 0
185-
for _ in token_based_paginator.iterator():
186-
number_counter += 1
187-
assert number_counter == 5
188-
189-
190-
def test_page_token_iterator_sync_using_list_expects_correct_metadata(token_based_pagination_request_data,
191-
mock_pagination_active_number_responses):
192-
"""Test `list()` correctly structures pagination metadata with proper `.content` handling."""
193-
194-
endpoint = Mock()
195-
endpoint.request_data = token_based_pagination_request_data
196-
sinch_client = Mock()
197-
198-
sinch_client.configuration.transport.request.side_effect = mock_pagination_active_number_responses
199-
200-
token_based_paginator = TokenBasedPaginator(
201-
sinch=sinch_client,
202-
endpoint=endpoint
179+
def test_page_token_iterator_sync_using_auto_pagination(
180+
token_based_pagination_request_data,
181+
mock_pagination_active_number_responses
182+
):
183+
token_based_paginator = initialize_token_paginator(
184+
endpoint_mock=Mock(),
185+
request_data=token_based_pagination_request_data,
186+
responses=mock_pagination_active_number_responses
203187
)
204-
assert token_based_paginator
188+
assert token_based_paginator is not None
205189

206-
list_response = token_based_paginator.get_content()
190+
active_numbers_list = [num.phone_number for num in token_based_paginator.iterator()]
207191

208-
page_counter = 0
209-
reached_last_page = False
210-
211-
while not reached_last_page:
212-
page_counter += 1
213-
if list_response.has_next_page:
214-
list_response = list_response.next_page()
215-
else:
216-
reached_last_page = True
217-
218-
assert page_counter == 3
192+
assert len(active_numbers_list) == len(EXPECTED_PHONE_NUMBERS)
193+
assert active_numbers_list == EXPECTED_PHONE_NUMBERS
219194

220195

196+
@pytest.mark.asyncio
221197
async def test_page_token_iterator_async_using_manual_pagination(
222198
token_based_pagination_request_data,
223-
first_token_based_pagination_response,
224-
second_token_based_pagination_response,
225-
third_token_based_pagination_response
199+
mock_pagination_active_number_responses
226200
):
227-
endpoint = Mock()
228-
endpoint.request_data = token_based_pagination_request_data
229-
sinch_client = AsyncMock()
230-
231-
sinch_client.configuration.transport.request.side_effect = [
232-
first_token_based_pagination_response,
233-
second_token_based_pagination_response,
234-
third_token_based_pagination_response
235-
]
236-
token_based_paginator = await AsyncTokenBasedPaginator._initialize(
237-
sinch=sinch_client,
238-
endpoint=endpoint
201+
async_token_based_paginator = await initialize_token_paginator(
202+
endpoint_mock=AsyncMock(),
203+
request_data=token_based_pagination_request_data,
204+
responses=mock_pagination_active_number_responses,
205+
is_async=True
239206
)
240-
assert token_based_paginator
207+
assert async_token_based_paginator is not None
241208

209+
active_numbers_list = [num.phone_number for num in async_token_based_paginator.content()]
242210
page_counter = 1
243-
while token_based_paginator.has_next_page:
244-
token_based_paginator = await token_based_paginator.next_page()
211+
212+
while async_token_based_paginator.has_next_page:
213+
async_token_based_paginator = await async_token_based_paginator.next_page()
245214
page_counter += 1
246-
assert isinstance(token_based_paginator, AsyncTokenBasedPaginator)
215+
assert isinstance(async_token_based_paginator, AsyncTokenBasedPaginator)
216+
active_numbers_list.extend(num.phone_number for num in async_token_based_paginator.content())
247217

248218
assert page_counter == 3
219+
assert active_numbers_list == EXPECTED_PHONE_NUMBERS
249220

250221

251-
async def test_page_token_iterator_async_using_list_expects_correct_metadata(
252-
token_based_pagination_request_data,
253-
mock_pagination_active_number_responses
254-
):
255-
"""Test async`list()` correctly structures pagination metadata with proper `.content` handling."""
256-
257-
endpoint = Mock()
258-
endpoint.request_data = token_based_pagination_request_data
259-
sinch_client = AsyncMock()
260-
261-
sinch_client.configuration.transport.request.side_effect = mock_pagination_active_number_responses
262-
263-
async_token_based_paginator = await AsyncTokenBasedPaginator._initialize(
264-
sinch=sinch_client,
265-
endpoint=endpoint
266-
)
267-
assert async_token_based_paginator
268-
269-
list_response = await async_token_based_paginator.get_content()
270-
271-
page_counter = 0
272-
reached_last_page = False
273-
274-
while not reached_last_page:
275-
if list_response.has_next_page:
276-
list_response = await list_response.next_page()
277-
page_counter += 1
278-
else:
279-
reached_last_page = True
280-
281-
assert page_counter == 2
282-
283-
222+
@pytest.mark.asyncio
284223
async def test_page_token_iterator_numbers_async_using_auto_pagination_expects_iter(
285224
token_based_pagination_request_data,
286225
mock_pagination_active_number_responses
287226
):
288-
"""Test that the async pagination iterates correctly through multiple items."""
289-
290-
sinch_client = AsyncMock()
291-
sinch_client.configuration.transport.request.side_effect = mock_pagination_active_number_responses
292-
endpoint = Mock()
293-
endpoint.request_data = token_based_pagination_request_data
294-
295-
async_token_based_paginator = await AsyncTokenBasedPaginator._initialize(
296-
sinch=sinch_client,
297-
endpoint=endpoint
227+
async_token_based_paginator = await initialize_token_paginator(
228+
endpoint_mock=AsyncMock(),
229+
request_data=token_based_pagination_request_data,
230+
responses=mock_pagination_active_number_responses,
231+
is_async=True
298232
)
299-
assert async_token_based_paginator
233+
assert async_token_based_paginator is not None
300234

301-
number_counter = 0
302-
async for _ in async_token_based_paginator.iterator():
303-
number_counter += 1
235+
active_numbers_list = [
236+
num.phone_number async for num in async_token_based_paginator.iterator()
237+
]
304238

305-
assert number_counter == 5
239+
assert len(active_numbers_list) == len(EXPECTED_PHONE_NUMBERS)
240+
assert active_numbers_list == EXPECTED_PHONE_NUMBERS

0 commit comments

Comments
 (0)