Skip to content

Commit 847e4dc

Browse files
committed
DEVEXP-786: SMS Batches - E2E tests
1 parent 7cb66e2 commit 847e4dc

35 files changed

Lines changed: 1818 additions & 173 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,14 @@ jobs:
7878
cp sinch-sdk-mockserver/features/numbers/webhooks.feature ./tests/e2e/numbers/features/
7979
cp sinch-sdk-mockserver/features/sms/delivery-reports.feature ./tests/e2e/sms/features/
8080
cp sinch-sdk-mockserver/features/sms/delivery-reports_servicePlanId.feature ./tests/e2e/sms/features/
81+
cp sinch-sdk-mockserver/features/sms/batches.feature ./tests/e2e/sms/features/
82+
cp sinch-sdk-mockserver/features/sms/batches_servicePlanId.feature ./tests/e2e/sms/features/
8183
8284
- name: Wait for mock server
8385
run: .github/scripts/wait-for-mockserver.sh
8486
shell: bash
8587

8688
- name: Run e2e tests sync
8789
run: |
88-
behave tests/e2e/**/features
90+
python -m behave tests/e2e/numbers/features
91+
python -m behave tests/e2e/sms/features

sinch/domains/sms/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from sinch.domains.sms.api.v1.delivery_reports_apis import DeliveryReports
2+
23
# from sinch.domains.sms.api.v1.groups_apis import Groups
34
# from sinch.domains.sms.api.v1.inbounds_apis import Inbounds
45
# from sinch.domains.sms.api.v1.webhooks_apis import Webhooks
5-
# from sinch.domains.sms.api.v1.batches_apis import Batches
6+
from sinch.domains.sms.api.v1.batches_apis import Batches
67

78

89
class SMS:
@@ -12,4 +13,4 @@ def __init__(self, sinch):
1213
# self.groups = Groups(sinch)
1314
# self.inbounds = Inbounds(sinch)
1415
# self.webhooks = Webhooks(sinch)
15-
# self.batches = Batches(sinch)
16+
self.batches = Batches(sinch)
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
from datetime import datetime
2+
from typing import Optional, List
3+
from pydantic import TypeAdapter, BaseModel
4+
from sinch.core.pagination import Paginator, SMSPaginator
5+
from sinch.domains.sms.models.v1.response.dry_run_response import (
6+
DryRunResponse,
7+
)
8+
from sinch.domains.sms.models.v1.internal import (
9+
BatchIdRequest,
10+
DeliveryFeedbackRequest,
11+
DryRunRequest,
12+
ListBatchesRequest,
13+
ReplaceBatchRequest,
14+
SendSMSRequest,
15+
UpdateBatchMessageRequest,
16+
)
17+
from sinch.domains.sms.api.v1.internal import (
18+
CancelBatchMessageEndpoint,
19+
DryRunEndpoint,
20+
GetBatchMessageEndpoint,
21+
ListBatchesEndpoint,
22+
ReplaceBatchEndpoint,
23+
SendSMSEndpoint,
24+
DeliveryFeedbackEndpoint,
25+
UpdateBatchMessageEndpoint,
26+
)
27+
from sinch.domains.sms.api.v1.base import BaseSms
28+
from sinch.domains.sms.models.v1.types import BatchResponse
29+
30+
31+
class Batches(BaseSms):
32+
def cancel(self, batch_id: str, **kwargs) -> BatchResponse:
33+
request_data = BatchIdRequest(batch_id=batch_id, **kwargs)
34+
return self._request(CancelBatchMessageEndpoint, request_data)
35+
36+
def dry_run(
37+
self,
38+
request: Optional[DryRunRequest] = None,
39+
per_recipient: Optional[bool] = None,
40+
number_of_recipients: Optional[int] = None,
41+
**kwargs,
42+
) -> DryRunResponse:
43+
# DryRunRequest is a Union type, so we need to use TypeAdapter to validate
44+
adapter = TypeAdapter(DryRunRequest)
45+
46+
# Check if we have any overrides (kwargs or explicit per_recipient/number_of_recipients)
47+
has_overrides = (
48+
bool(kwargs)
49+
or per_recipient is not None
50+
or number_of_recipients is not None
51+
)
52+
53+
if (
54+
request is not None
55+
and isinstance(request, BaseModel)
56+
and not has_overrides
57+
):
58+
request_data = request
59+
else:
60+
# Build input data from all sources and merge overrides
61+
input_data = {}
62+
if request is not None:
63+
if isinstance(request, BaseModel):
64+
input_data = request.model_dump(exclude_none=True)
65+
66+
# Merge overrides: kwargs, per_recipient, number_of_recipients
67+
input_data.update(kwargs)
68+
if per_recipient is not None:
69+
input_data["per_recipient"] = per_recipient
70+
if number_of_recipients is not None:
71+
input_data["number_of_recipients"] = number_of_recipients
72+
73+
request_data = adapter.validate_python(input_data)
74+
75+
return self._request(DryRunEndpoint, request_data)
76+
77+
def get(self, batch_id: str, **kwargs) -> BatchResponse:
78+
request_data = BatchIdRequest(batch_id=batch_id, **kwargs)
79+
return self._request(GetBatchMessageEndpoint, request_data)
80+
81+
def list(
82+
self,
83+
page: Optional[int] = None,
84+
page_size: Optional[int] = None,
85+
start_date: Optional[datetime] = None,
86+
end_date: Optional[datetime] = None,
87+
var_from: Optional[List[str]] = None,
88+
client_reference: Optional[str] = None,
89+
**kwargs,
90+
) -> Paginator[BatchResponse]:
91+
# Use service_plan_id for SMS auth, project_id for project auth
92+
if self._sinch.configuration.authentication_method == "sms_auth":
93+
path_identifier = self._sinch.configuration.service_plan_id
94+
else:
95+
path_identifier = self._sinch.configuration.project_id
96+
97+
endpoint = ListBatchesEndpoint(
98+
project_id=path_identifier,
99+
request_data=ListBatchesRequest(
100+
page=page,
101+
page_size=page_size,
102+
start_date=start_date,
103+
end_date=end_date,
104+
var_from=var_from,
105+
client_reference=client_reference,
106+
**kwargs,
107+
),
108+
)
109+
endpoint.set_authentication_method(self._sinch)
110+
111+
return SMSPaginator(sinch=self._sinch, endpoint=endpoint)
112+
113+
def replace(
114+
self,
115+
batch_id: str,
116+
request: Optional[ReplaceBatchRequest] = None,
117+
**kwargs,
118+
) -> BatchResponse:
119+
adapter = TypeAdapter(ReplaceBatchRequest)
120+
121+
input_data = {}
122+
if request is not None:
123+
if isinstance(request, BaseModel):
124+
input_data = request.model_dump(exclude_none=True)
125+
126+
input_data.update(kwargs)
127+
input_data["batch_id"] = batch_id
128+
129+
request_data = adapter.validate_python(input_data)
130+
131+
return self._request(ReplaceBatchEndpoint, request_data)
132+
133+
def send(
134+
self, request: Optional[SendSMSRequest] = None, **kwargs
135+
) -> BatchResponse:
136+
# SendSMSRequest is a Union type, so we need to use TypeAdapter to validate
137+
adapter = TypeAdapter(SendSMSRequest)
138+
139+
# If request is provided and is already a BaseModel instance, use it directly
140+
# Otherwise, validate the input (either request dict or kwargs)
141+
if request is not None and isinstance(request, BaseModel):
142+
request_data = request
143+
else:
144+
# Validate either the request dict or kwargs
145+
request_data = adapter.validate_python(
146+
request if request is not None else kwargs
147+
)
148+
149+
return self._request(SendSMSEndpoint, request_data)
150+
151+
def send_delivery_feedback(
152+
self, batch_id: str, recipients: List[str], **kwargs
153+
) -> None:
154+
request_data = DeliveryFeedbackRequest(
155+
batch_id=batch_id, recipients=recipients, **kwargs
156+
)
157+
return self._request(DeliveryFeedbackEndpoint, request_data)
158+
159+
def update(
160+
self,
161+
batch_id: str,
162+
request: Optional[UpdateBatchMessageRequest] = None,
163+
**kwargs,
164+
) -> BatchResponse:
165+
adapter = TypeAdapter(UpdateBatchMessageRequest)
166+
167+
input_data = {}
168+
if request is not None:
169+
if isinstance(request, BaseModel):
170+
input_data = request.model_dump(exclude_none=True)
171+
elif isinstance(request, dict):
172+
input_data = dict(request)
173+
174+
input_data.update(kwargs)
175+
input_data["batch_id"] = batch_id
176+
177+
request_data = adapter.validate_python(input_data)
178+
179+
return self._request(UpdateBatchMessageEndpoint, request_data)

sinch/domains/sms/api/v1/internal/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
from sinch.domains.sms.api.v1.internal.batches_endpoints import (
2+
CancelBatchMessageEndpoint,
3+
DryRunEndpoint,
4+
GetBatchMessageEndpoint,
5+
ListBatchesEndpoint,
6+
ReplaceBatchEndpoint,
7+
SendSMSEndpoint,
8+
DeliveryFeedbackEndpoint,
9+
UpdateBatchMessageEndpoint,
10+
)
111
from sinch.domains.sms.api.v1.internal.delivery_reports_endpoints import (
212
GetBatchDeliveryReportEndpoint,
313
GetRecipientDeliveryReportEndpoint,
@@ -6,6 +16,14 @@
616

717

818
__all__ = [
19+
"CancelBatchMessageEndpoint",
20+
"DryRunEndpoint",
21+
"GetBatchMessageEndpoint",
22+
"ListBatchesEndpoint",
23+
"ReplaceBatchEndpoint",
24+
"SendSMSEndpoint",
25+
"DeliveryFeedbackEndpoint",
26+
"UpdateBatchMessageEndpoint",
927
"GetBatchDeliveryReportEndpoint",
1028
"GetRecipientDeliveryReportEndpoint",
1129
"ListDeliveryReportsEndpoint",

sinch/domains/sms/api/v1/internal/base/sms_endpoint.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import ABC
2-
from typing import Type
2+
from typing import Type, Union, get_origin
3+
from pydantic import TypeAdapter
34
from sinch.core.models.http_response import HTTPResponse
45
from sinch.core.endpoint import HTTPEndpoint
56
from sinch.core.types import BM
@@ -61,13 +62,21 @@ def process_response_model(
6162
6263
Args:
6364
response_body (dict): The raw response body.
64-
response_model (type): The Pydantic model class to map the response.
65+
response_model (type): The Pydantic model class or Union type to map the response.
6566
6667
Returns:
6768
Parsed response object.
6869
"""
6970
try:
70-
return response_model.model_validate(response_body)
71+
# Check if response_model is a Union type
72+
origin = get_origin(response_model)
73+
if origin is Union:
74+
# Use TypeAdapter for Union types
75+
adapter = TypeAdapter(response_model)
76+
return adapter.validate_python(response_body)
77+
else:
78+
# Use standard model_validate for regular Pydantic models
79+
return response_model.model_validate(response_body)
7180
except Exception as e:
7281
raise ValueError(f"Invalid response structure: {e}") from e
7382

0 commit comments

Comments
 (0)