Skip to content

Commit

Permalink
DEVEXP-733: [Python SDK] Auto pagination of elements
Browse files Browse the repository at this point in the history
- 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 <[email protected]>
  • Loading branch information
matsk-sinch committed Feb 13, 2025
1 parent f535801 commit 210f58e
Show file tree
Hide file tree
Showing 35 changed files with 857 additions and 378 deletions.
1 change: 0 additions & 1 deletion sinch/core/adapters/requests_http_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ def request(self, endpoint: HTTPEndpoint) -> HTTPResponse:
f"Sync HTTP {request_data.http_method} call with headers:"
f" {request_data.headers} and body: {request_data.request_body} to URL: {request_data.url}"
)

response = self.http_session.request(
method=request_data.http_method,
url=request_data.url,
Expand Down
14 changes: 7 additions & 7 deletions sinch/core/clients/sinch_client_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ def __init__(
self.connection_timeout = connection_timeout
self.sms_api_token = sms_api_token
self.service_plan_id = service_plan_id
self.auth_origin = "auth.sinch.com"
self.numbers_origin = "numbers.api.sinch.com"
self.verification_origin = "verification.api.sinch.com"
self.voice_applications_origin = "callingapi.sinch.com"
self._voice_domain = "{}.api.sinch.com"
self.auth_origin = "https://auth.sinch.com"
self.numbers_origin = "https://numbers.api.sinch.com"
self.verification_origin = "https://verification.api.sinch.com"
self.voice_applications_origin = "https://callingapi.sinch.com"
self._voice_domain = "https://{}.api.sinch.com"
self._voice_region = None
self._conversation_region = "eu"
self._conversation_domain = ".conversation.api.sinch.com"
self._sms_region = "us"
self._sms_region_with_service_plan_id = "us"
self._sms_domain = "zt.{}.sms.api.sinch.com"
self._sms_domain_with_service_plan_id = "{}.sms.api.sinch.com"
self._sms_domain = "https://zt.{}.sms.api.sinch.com"
self._sms_domain_with_service_plan_id = "https://{}.sms.api.sinch.com"
self._sms_authentication = HTTPAuthentication.OAUTH.value
self._templates_region = "eu"
self._templates_domain = ".template.api.sinch.com"
Expand Down
118 changes: 106 additions & 12 deletions sinch/core/pagination.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
from abc import ABC, abstractmethod
from collections import namedtuple


class PageIterator:
def __init__(self, paginator):
def __init__(self, paginator, yield_first_page=False):
self.paginator = paginator
self.yield_first_page = yield_first_page
# If yielding the first page, set started to False
self.started = not yield_first_page

def __iter__(self):
return self

def __next__(self):
if not self.started:
self.started = True
return self.paginator

if self.paginator.has_next_page:
return self.paginator.next_page()
else:
Expand Down Expand Up @@ -113,32 +121,118 @@ async def _initialize(cls, sinch, endpoint):
return cls(sinch, endpoint, result)


class TokenBasedPaginator(Paginator):
__doc__ = Paginator.__doc__
class TokenBasedPaginatorBase(Paginator):
"""Base paginator for token-based pagination."""

def __init__(self, sinch, endpoint, yield_first_page=False, result=None):
self._sinch = sinch
self.endpoint = endpoint
# Determines if the first page should be included
self.yield_first_page = yield_first_page
self.result = result or self._sinch.configuration.transport.request(self.endpoint)
self.has_next_page = bool(self.result.next_page_token)

def __repr__(self):
pass

def _calculate_next_page(self):
if self.result.next_page_token:
self.has_next_page = True
else:
self.has_next_page = False
self.has_next_page = bool(self.result.next_page_token)

def next_page(self):
"""Fetches the next page and updates pagination state."""
self.endpoint.request_data.page_token = self.result.next_page_token
self.result = self._sinch.configuration.transport.request(self.endpoint)
self._calculate_next_page()
return self

def auto_paging_iter(self):
return PageIterator(self)
"""Returns an iterator for automatic pagination."""
return PageIterator(self, yield_first_page=self.yield_first_page)

@classmethod
def _initialize(cls, sinch, endpoint):
"""Creates an instance of the paginator skipping first page."""
result = sinch.configuration.transport.request(endpoint)
return cls(sinch, endpoint, result)
return cls(sinch, endpoint, yield_first_page=False, result=result)


class AsyncTokenBasedPaginator(TokenBasedPaginator):
__doc__ = TokenBasedPaginator.__doc__
class TokenBasedPaginator(TokenBasedPaginatorBase):
"""Paginator that skips the first page."""
pass


class TokenBasedPaginatorNumbers(TokenBasedPaginatorBase):
"""
Paginator for handling token-based pagination specifically for phone numbers.
This paginator is designed to iterate through phone numbers automatically or manually, fetching new pages as needed.
It extends the TokenBasedPaginatorBase class and provides additional methods for number-specific pagination.
"""

def __init__(self, sinch, endpoint):
super().__init__(sinch, endpoint, yield_first_page=True)

def numbers_iterator(self):
"""Iterates through numbers individually, fetching new pages as needed."""
while True:
if self.result and self.result.active_numbers:
yield from self.result.active_numbers

if not self.has_next_page:
break

self.next_page()

def list(self):
"""Returns the first page's numbers along with pagination metadata."""

PagedListResponse = namedtuple(
"PagedResponse", ["result", "has_next_page", "next_page_info", "next_page"]
)

next_page_result = self._get_next_page_result()

return PagedListResponse(
result=self.result.active_numbers,
has_next_page=self.has_next_page,
next_page_info=self._build_next_pagination_info(next_page_result),
next_page=self._next_page_wrapper()
)

def _get_next_page_result(self):
"""Fetches the next page result."""
if not self.has_next_page:
return None

current_state = self.result
self.next_page()
next_page_result = self.result
self.result = current_state

return next_page_result

def _build_next_pagination_info(self, next_page_result):
"""Constructs and returns structured pagination metadata."""
return {
"result": self.result.active_numbers,
"result.next": (
self.result.active_numbers + next_page_result.active_numbers
if next_page_result else self.result.active_numbers
),
"has_next_page": self.has_next_page,
"has_next_page.next": bool(next_page_result and next_page_result.next_page_token),
}

def _next_page_wrapper(self):
"""Fetches and returns the next page as a formatted PagedListResponse object."""
def wrapper():
self.next_page()
return self.list()
return wrapper


class AsyncTokenBasedPaginator(TokenBasedPaginatorBase):
"""Asynchronous token-based paginator."""

async def next_page(self):
self.endpoint.request_data.page_token = self.result.next_page_token
Expand All @@ -152,4 +246,4 @@ def auto_paging_iter(self):
@classmethod
async def _initialize(cls, sinch, endpoint):
result = await sinch.configuration.transport.request(endpoint)
return cls(sinch, endpoint, result)
return cls(sinch, endpoint, result=result)
131 changes: 1 addition & 130 deletions sinch/domains/numbers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
from sinch.core.pagination import TokenBasedPaginator, AsyncTokenBasedPaginator
from sinch.domains.numbers.available_numbers import AvailableNumbers
from sinch.domains.numbers.active_numbers import ActiveNumbers, ActiveNumbersWithAsyncPagination
from sinch.domains.numbers.endpoints.callbacks.get_configuration import GetNumbersCallbackConfigurationEndpoint
from sinch.domains.numbers.endpoints.callbacks.update_configuration import UpdateNumbersCallbackConfigurationEndpoint
from sinch.domains.numbers.endpoints.active.list_active_numbers_for_project import ListActiveNumbersEndpoint
from sinch.domains.numbers.endpoints.active.update_number_configuration import UpdateNumberConfigurationEndpoint
from sinch.domains.numbers.endpoints.active.get_number_configuration import GetNumberConfigurationEndpoint
from sinch.domains.numbers.endpoints.active.release_number_from_project import ReleaseNumberFromProjectEndpoint
from sinch.domains.numbers.endpoints.regions.list_available_regions import ListAvailableRegionsEndpoint

from sinch.domains.numbers.models.regions.requests import ListAvailableRegionsForProjectRequest
from sinch.domains.numbers.models.active.requests import (
ListActiveNumbersRequest, GetNumberConfigurationRequest,
UpdateNumberConfigurationRequest, ReleaseNumberFromProjectRequest
)
from sinch.domains.numbers.models.regions.responses import ListAvailableRegionsResponse
from sinch.domains.numbers.models.active.responses import (
ListActiveNumbersResponse, UpdateNumberConfigurationResponse,
GetNumberConfigurationResponse, ReleaseNumberFromProjectResponse
)
from sinch.domains.numbers.models.callbacks.responses import (
GetNumbersCallbackConfigurationResponse,
UpdateNumbersCallbackConfigurationResponse
Expand All @@ -27,123 +15,6 @@
)


class ActiveNumbers:
def __init__(self, sinch):
self._sinch = sinch

def list(
self,
region_code: str,
number_type: str,
number_pattern: str = None,
number_search_pattern: str = None,
capabilities: list = None,
page_size: int = None,
page_token: str = None
) -> ListActiveNumbersResponse:
"""
Search for all active virtual numbers associated with a certain project.
For additional documentation, see https://www.sinch.com and visit our developer portal.
"""
return TokenBasedPaginator._initialize(
sinch=self._sinch,
endpoint=ListActiveNumbersEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=ListActiveNumbersRequest(
region_code=region_code,
number_type=number_type,
page_size=page_size,
capabilities=capabilities,
number_pattern=number_pattern,
number_search_pattern=number_search_pattern,
page_token=page_token
)
)
)

def update(
self,
phone_number: str = None,
display_name: str = None,
sms_configuration: dict = None,
voice_configuration: dict = None,
app_id: str = None
) -> UpdateNumberConfigurationResponse:
"""
Make updates to the configuration of your virtual number.
Update the display name, change the currency type, or reconfigure for either SMS and/or Voice.
For additional documentation, see https://www.sinch.com and visit our developer portal.
"""
return self._sinch.configuration.transport.request(
UpdateNumberConfigurationEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=UpdateNumberConfigurationRequest(
phone_number=phone_number,
display_name=display_name,
sms_configuration=sms_configuration,
voice_configuration=voice_configuration,
app_id=app_id
)
)
)

def get(self, phone_number: str) -> GetNumberConfigurationResponse:
"""
List of configuration settings for your virtual number.
For additional documentation, see https://www.sinch.com and visit our developer portal.
"""
return self._sinch.configuration.transport.request(
GetNumberConfigurationEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=GetNumberConfigurationRequest(
phone_number=phone_number
)
)
)

def release(self, phone_number: str) -> ReleaseNumberFromProjectResponse:
"""
Release numbers you no longer need from your project.
For additional documentation, see https://www.sinch.com and visit our developer portal.
"""
return self._sinch.configuration.transport.request(
ReleaseNumberFromProjectEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=ReleaseNumberFromProjectRequest(
phone_number=phone_number
)
)
)


class ActiveNumbersWithAsyncPagination(ActiveNumbers):
async def list(
self,
region_code: str,
number_type: str,
number_pattern: str = None,
number_search_pattern: str = None,
capabilities: list = None,
page_size: int = None,
page_token: str = None
) -> ListActiveNumbersResponse:
return await AsyncTokenBasedPaginator._initialize(
sinch=self._sinch,
endpoint=ListActiveNumbersEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=ListActiveNumbersRequest(
region_code=region_code,
number_type=number_type,
page_size=page_size,
capabilities=capabilities,
number_pattern=number_pattern,
number_search_pattern=number_search_pattern,
page_token=page_token
)
)
)


class AvailableRegions:
def __init__(self, sinch):
self._sinch = sinch
Expand Down
Loading

0 comments on commit 210f58e

Please sign in to comment.