Skip to content

Commit e08fbf8

Browse files
committed
feat: added Pydantic models
- Added models for Active numbers - list function - Added unit tests Signed-off-by: Jessica Matsuoka <jessica.akemi.matsuoka@sinch.com>
1 parent 316fadf commit e08fbf8

14 files changed

+256
-98
lines changed

sinch/domains/numbers/active_numbers.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
from sinch.core.pagination import TokenBasedPaginator, AsyncTokenBasedPaginator
2-
from sinch.domains.numbers.endpoints.active.list_active_numbers_for_project import ListActiveNumbersEndpoint
2+
from sinch.domains.numbers.endpoints.active.list_active_numbers_endpoint import ListActiveNumbersEndpoint
33
from sinch.domains.numbers.endpoints.active.update_number_configuration import UpdateNumberConfigurationEndpoint
44
from sinch.domains.numbers.endpoints.active.get_number_configuration import GetNumberConfigurationEndpoint
55
from sinch.domains.numbers.endpoints.active.release_number_from_project import ReleaseNumberFromProjectEndpoint
66

7+
from sinch.domains.numbers.models.active.list_active_numbers_request import ListActiveNumbersRequest
8+
from sinch.domains.numbers.models.active.list_active_numbers_response import ListActiveNumbersResponse
9+
710
from sinch.domains.numbers.models.active.requests import (
8-
ListActiveNumbersRequest, GetNumberConfigurationRequest,
9-
UpdateNumberConfigurationRequest, ReleaseNumberFromProjectRequest
11+
GetNumberConfigurationRequest, UpdateNumberConfigurationRequest, ReleaseNumberFromProjectRequest
1012
)
1113

1214
from sinch.domains.numbers.models.active.responses import (
13-
ListActiveNumbersResponse, UpdateNumberConfigurationResponse,
14-
GetNumberConfigurationResponse, ReleaseNumberFromProjectResponse
15+
UpdateNumberConfigurationResponse, GetNumberConfigurationResponse, ReleaseNumberFromProjectResponse
1516
)
1617

1718

@@ -129,4 +130,4 @@ async def list(
129130
page_token=page_token
130131
)
131132
)
132-
)
133+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from sinch.core.models.http_response import HTTPResponse
2+
from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint
3+
from sinch.core.enums import HTTPAuthentication, HTTPMethods
4+
from sinch.domains.numbers.models.active.list_active_numbers_request import ListActiveNumbersRequest
5+
from sinch.domains.numbers.models.active.list_active_numbers_response import ListActiveNumbersResponse
6+
7+
8+
class ListActiveNumbersEndpoint(NumbersEndpoint):
9+
ENDPOINT_URL = "{origin}/v1/projects/{project_id}/activeNumbers"
10+
HTTP_METHOD = HTTPMethods.GET.value
11+
HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value
12+
13+
def __init__(self, project_id: str, request_data: ListActiveNumbersRequest):
14+
super(ListActiveNumbersEndpoint, self).__init__(project_id, request_data)
15+
self.project_id = project_id
16+
self.request_data = request_data
17+
18+
def build_query_params(self) -> dict:
19+
# Serialize fields
20+
query_params = self.request_data.model_dump(exclude_none=True, by_alias=True)
21+
return query_params
22+
23+
def request_body(self):
24+
pass
25+
26+
def handle_response(self, response: HTTPResponse) -> ListActiveNumbersResponse:
27+
super(ListActiveNumbersEndpoint, self).handle_response(response)
28+
return self.process_response_model(response.body, ListActiveNumbersResponse)

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

-68
This file was deleted.

sinch/domains/numbers/endpoints/numbers_endpoint.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
from sinch.domains.numbers.exceptions import NumbersException
88
from sinch.domains.numbers.models.numbers import NotFoundError
99

10+
BM = TypeVar("BM", bound=BaseModel)
11+
1012

1113
class NumbersEndpoint(HTTPEndpoint, ABC):
1214

13-
def __init__(self, project_id: str, request_data: object):
15+
def __init__(self, project_id: str, request_data: BM):
1416
super().__init__(project_id, request_data)
1517

1618
def build_url(self, sinch) -> str:
@@ -34,8 +36,6 @@ def request_body(self):
3436
request_data = self.request_data.model_dump(by_alias=True, exclude_none=True)
3537
return json.dumps(request_data)
3638

37-
BM = TypeVar("BM", bound=BaseModel)
38-
3939
def process_response_model(self, response_body: dict, response_model: Type[BM]) -> BM:
4040
"""
4141
Processes the response body and maps it to a response model.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Optional, Union, Literal
2+
from pydantic import Field, StrictInt, StrictStr, field_validator
3+
from sinch.domains.numbers.models.base_model_numbers import BaseModelConfigRequest
4+
from sinch.domains.numbers.models.numbers import CapabilityType, NumberType, NumberSearchPatternType
5+
6+
7+
class ListActiveNumbersRequest(BaseModelConfigRequest):
8+
region_code: StrictStr = Field(alias="regionCode")
9+
number_type: NumberType = Field(alias="type")
10+
page_size: Optional[StrictInt] = Field(default=None, alias="size")
11+
capabilities: Optional[CapabilityType] = Field(default=None)
12+
number_search_pattern: Optional[NumberSearchPatternType] = (
13+
Field(default=None, alias="numberPattern.searchPattern"))
14+
number_pattern: Optional[StrictStr] = Field(default=None, alias="numberPattern.pattern")
15+
page_token: Optional[StrictStr] = Field(default=None, alias="pageToken")
16+
order_by: Optional[Union[Literal["phoneNumber", "displayName"], StrictStr]] = Field(default=None, alias="orderBy")
17+
18+
@field_validator("order_by", mode="before")
19+
@classmethod
20+
def convert_order_by(cls, value):
21+
if isinstance(value, str):
22+
return cls._to_camel_case(value)
23+
return value
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import List, Optional
2+
from pydantic import BaseModel, ConfigDict, Field, StrictStr, StrictInt
3+
from sinch.domains.numbers.models.numbers import ActiveNumber
4+
5+
6+
class ListActiveNumbersResponse(BaseModel):
7+
active_numbers: Optional[List[ActiveNumber]] = Field(default=None, alias="activeNumbers")
8+
next_page_token: Optional[StrictStr] = Field(default=None, alias="nextPageToken")
9+
total_size: Optional[StrictInt] = Field(default=None, alias="totalSize")
10+
11+
model_config = ConfigDict(
12+
populate_by_name=True
13+
)

sinch/domains/numbers/models/active/requests.py

-11
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,6 @@
33
from sinch.core.models.base_model import SinchRequestBaseModel
44

55

6-
@dataclass
7-
class ListActiveNumbersRequest(SinchRequestBaseModel):
8-
region_code: str
9-
number_type: str
10-
page_size: int
11-
capabilities: list
12-
number_search_pattern: str
13-
number_pattern: str
14-
page_token: str
15-
16-
176
@dataclass
187
class GetNumberConfigurationRequest(SinchRequestBaseModel):
198
phone_number: str

sinch/domains/numbers/models/active/responses.py

-9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
from dataclasses import dataclass
2-
from typing import List, Optional
3-
4-
from sinch.core.models.base_model import SinchBaseModel
52
from sinch.domains.numbers.models.active import ActiveNumber
63

74

8-
@dataclass
9-
class ListActiveNumbersResponse(SinchBaseModel):
10-
active_numbers: List[ActiveNumber]
11-
next_page_token: Optional[str] = None
12-
13-
145
@dataclass
156
class UpdateNumberConfigurationResponse(ActiveNumber):
167
pass

sinch/domains/numbers/models/numbers.py

+15
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ class Money(BaseModelConfigResponse):
114114
amount: Decimal
115115

116116

117+
class ActiveNumber(BaseModelConfigResponse):
118+
phone_number: Optional[StrictStr] = Field(default=None, alias="phoneNumber")
119+
project_id: Optional[StrictStr] = Field(default=None, alias="projectId")
120+
display_name: Optional[StrictStr] = Field(default=None, alias="displayName")
121+
region_code: Optional[StrictStr] = Field(default=None, alias="regionCode")
122+
type: Optional[NumberType] = Field(default=None)
123+
capability: Optional[CapabilityType] = Field(default=None)
124+
money: Optional[Money] = Field(default=None)
125+
payment_interval_months: Optional[StrictInt] = Field(default=None, alias="paymentIntervalMonths")
126+
next_charge_date: Optional[datetime] = Field(default=None, alias="nextChargeDate")
127+
expire_at: Optional[datetime] = Field(default=None, alias="expireAt")
128+
sms_configuration: Optional[SmsConfigurationResponse] = Field(default=None, alias="smsConfiguration")
129+
voice_configuration: Optional[VoiceConfigurationResponse] = Field(default=None, alias="voiceConfiguration")
130+
131+
117132
class Number(BaseModelConfigResponse):
118133
phone_number: Optional[StrictStr] = Field(default=None, alias="phoneNumber")
119134
region_code: Optional[StrictStr] = Field(default=None, alias="regionCode")

tests/unit/domains/numbers/endpoints/available/test_list_available_numbers_endpoint.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import pytest
22
from sinch.domains.numbers.endpoints.available.list_available_numbers_endpoint import AvailableNumbersEndpoint
33
from sinch.domains.numbers.models.available.list_available_numbers_request import ListAvailableNumbersRequest
4-
from sinch.domains.numbers.models.available.list_available_numbers_response import ListAvailableNumbersResponse
54
from sinch.core.models.http_response import HTTPResponse
65

76
@pytest.fixture
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
from sinch.domains.numbers.models.active.list_active_numbers_request import ListActiveNumbersRequest
4+
5+
@pytest.mark.parametrize(
6+
"order_by_input, expected_order_by",
7+
[
8+
("phone_number", "phoneNumber"),
9+
("display_name", "displayName"),
10+
("new_field", "newField"),
11+
("newField", "newField")
12+
]
13+
)
14+
15+
def test_list_active_numbers_orderby_field_request_expects_camel_case_input(order_by_input, expected_order_by):
16+
"""
17+
Test that the model correctly parses order_by field.
18+
"""
19+
data = {
20+
"region_code": "US",
21+
"number_type": "MOBILE",
22+
"order_by": order_by_input
23+
}
24+
25+
request = ListActiveNumbersRequest(**data)
26+
27+
assert request.region_code == "US"
28+
assert request.number_type == "MOBILE"
29+
assert request.order_by == expected_order_by
30+
31+
def test_list_active_numbers_request_expects_parsed_input():
32+
"""
33+
Test that the model correctly parses input.
34+
"""
35+
data = {
36+
"region_code": "GB",
37+
"number_type": "LOCAL",
38+
"page_size": 5,
39+
"capabilities": ["SMS", "VOICE"],
40+
"number_search_pattern": "START",
41+
"number_pattern": "5678",
42+
"page_token": "abc123",
43+
"order_by": "phoneNumber"
44+
}
45+
46+
request = ListActiveNumbersRequest(**data)
47+
48+
assert request.region_code == "GB"
49+
assert request.number_type == "LOCAL"
50+
assert request.page_size == 5
51+
assert request.capabilities == ["SMS", "VOICE"]
52+
assert request.number_search_pattern == "START"
53+
assert request.number_pattern == "5678"
54+
assert request.page_token == "abc123"
55+
assert request.order_by == "phoneNumber"
56+
57+
def test_list_available_numbers_request_expects_camel_case_input():
58+
"""
59+
Test that the model correctly handles camelCase input.
60+
"""
61+
data = {
62+
"regionCode": "US",
63+
"number_type": "MOBILE",
64+
}
65+
request = ListActiveNumbersRequest(**data)
66+
assert request.region_code == "US"
67+
assert request.number_type == "MOBILE"
68+
69+
def test_list_active_numbers_request_expects_validation_error_for_missing_field():
70+
"""
71+
Test that missing required fields raise a ValidationError.
72+
"""
73+
data = {}
74+
with pytest.raises(ValidationError):
75+
ListActiveNumbersRequest(**data)

0 commit comments

Comments
 (0)