Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ jobs:
cp sinch-sdk-mockserver/features/sms/delivery-reports_servicePlanId.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/batches.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/batches_servicePlanId.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/inbounds.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/inbounds_servicePlanId.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/webhooks.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/number-lookup/lookups.feature ./tests/e2e/number-lookup/features/
cp sinch-sdk-mockserver/features/conversation/messages.feature ./tests/e2e/conversation/features/
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ All notable changes to the **Sinch Python SDK** are documented in this file.
### SMS

- **[feature]** SMS Groups API: `create`, `list`, `get`, `update`, `replace`, `delete`, and `list_members` operations, with full model, endpoint, and unit test coverage (see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md#groups-api)).
- **[feature]** SMS Inbounds API: `get` and `list` operations, with full model, endpoint, and unit test coverage (see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md#210)).
- **[design]** SMS Sinch Events inbound payload models unified with the Inbounds API: `MOTextSinchEvent`, `MOBinarySinchEvent`, `MOMediaSinchEvent`, `MediaBody`, and `MediaItem` removed from `sinch_events`; use `InboundMessage` (and its variants) from `sinch.domains.sms.models.v1.types` instead (see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md#210)).

---

Expand Down
38 changes: 33 additions & 5 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,7 @@ The Conversation HTTP API still expects the JSON field **`callback_url`**. In V2

#### Replacement APIs

The SMS domain API access remains the same: `sinch.sms.batches` and `sinch.sms.delivery_reports`. However, the underlying models and method signatures have changed.

Note that `sinch.sms.inbounds` is not supported yet and will be available in a future minor version. `sinch.sms.groups` is now available — see [Groups API](#groups-api) below.
The SMS domain API access remains the same: `sinch.sms.batches`, `sinch.sms.delivery_reports`, `sinch.sms.inbounds` and `sinch.sms.groups`. However, the underlying models and method signatures have changed.

##### Batches API

Expand All @@ -232,8 +230,6 @@ Note that `sinch.sms.inbounds` is not supported yet and will be available in a f

##### Groups API

> **Added in v2.1.0.** `sinch.sms.groups` is now fully supported.

###### Replacement models

| Old class | New class |
Expand Down Expand Up @@ -265,6 +261,38 @@ Note that `sinch.sms.inbounds` is not supported yet and will be available in a f
| `delete()` with `DeleteSMSGroupRequest` | `delete()` with `group_id: str` parameter |
| `get_phone_numbers()` / phone number listing | `list_members()` with `group_id: str`. Returns **`Paginator[str]`** |

##### Inbounds API

###### Replacement models

| Old class | New class |
|-----------|-----------|
| `sinch.domains.sms.models.inbounds.requests.ListSMSInboundMessageRequest` | [`sinch.domains.sms.models.v1.internal.ListInboundsRequest`](sinch/domains/sms/models/v1/internal/list_inbounds_request.py) |
| `sinch.domains.sms.models.inbounds.requests.GetSMSInboundMessageRequest` | [`sinch.domains.sms.models.v1.internal.InboundIdRequest`](sinch/domains/sms/models/v1/internal/inbound_id_request.py) |
| `sinch.domains.sms.models.inbounds.responses.SinchListInboundMessagesResponse` | [`sinch.domains.sms.models.v1.internal.ListInboundsResponse`](sinch/domains/sms/models/v1/internal/list_inbounds_response.py) |
| `sinch.domains.sms.models.inbounds.responses.GetInboundMessagesResponse` | [`sinch.domains.sms.models.v1.types.InboundMessage`](sinch/domains/sms/models/v1/types/inbound_message.py) (Union of `MOTextMessage`, `MOBinaryMessage`, `MOMediaMessage`) |

###### Replacement APIs

| Old method | New method in `sms.inbounds` |
|------------|------------------------------|
| `list()` with `ListSMSInboundMessageRequest` | `list()` with individual parameters: `page`, `page_size`, `to`, `start_date`, `end_date`, `client_reference`. Returns **`Paginator[InboundMessage]`** |
| `get()` with `GetSMSInboundMessageRequest` | `get()` with `inbound_id: str` parameter |

##### SMS Sinch Events

The inbound payload models in `sinch_events` have been unified with the Inbounds API models. The following classes have been removed:

| Removed class | Replacement |
|---------------|-------------|
| `sinch.domains.sms.sinch_events.v1.events.MOTextSinchEvent` | [`sinch.domains.sms.models.v1.shared.MOTextMessage`](sinch/domains/sms/models/v1/shared/mo_text_message.py) |
| `sinch.domains.sms.sinch_events.v1.events.MOBinarySinchEvent` | [`sinch.domains.sms.models.v1.shared.MOBinaryMessage`](sinch/domains/sms/models/v1/shared/mo_binary_message.py) |
| `sinch.domains.sms.sinch_events.v1.events.MOMediaSinchEvent` | [`sinch.domains.sms.models.v1.shared.MOMediaMessage`](sinch/domains/sms/models/v1/shared/mo_media_message.py) |
| `sinch.domains.sms.sinch_events.v1.events.MediaBody` | Embedded in `MOMediaMessage` |
| `sinch.domains.sms.sinch_events.v1.events.MediaItem` | Embedded in `MOMediaMessage` |

`IncomingSMSSinchEvent` is now a type alias for [`InboundMessage`](sinch/domains/sms/models/v1/types/inbound_message.py) (discriminated union of `MOTextMessage`, `MOBinaryMessage`, `MOMediaMessage`). Code that previously type-checked against `MOTextSinchEvent` and siblings should switch to their `MO*Message` equivalents.

---

### [`Numbers` (Virtual Numbers)](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/numbers)
Expand Down
29 changes: 29 additions & 0 deletions examples/snippets/sms/inbounds/get/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

# The ID of the inbound message to retrieve
inbound_id = "INBOUND_ID"

response = sinch_client.sms.inbound_messages.get(
inbound_id=inbound_id
)

print(f"Inbound message:\n{response}")
27 changes: 27 additions & 0 deletions examples/snippets/sms/inbounds/list/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)


inbound_messages = sinch_client.sms.inbound_messages.list()

print("List of inbound messages:\n")
for message in inbound_messages.iterator():
print(message)
2 changes: 2 additions & 0 deletions sinch/domains/sms/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from sinch.domains.sms.api.v1.batches_apis import Batches
from sinch.domains.sms.api.v1.delivery_reports_apis import DeliveryReports
from sinch.domains.sms.api.v1.inbounds_apis import Inbounds
from sinch.domains.sms.api.v1.groups_apis import Groups

__all__ = [
"Batches",
"DeliveryReports",
"Inbounds",
"Groups",
]
94 changes: 94 additions & 0 deletions sinch/domains/sms/api/v1/inbounds_apis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from datetime import datetime
from typing import List, Optional

from sinch.core.pagination import Paginator, SMSPaginator
from sinch.domains.sms.api.v1.base.base_sms import BaseSms
from sinch.domains.sms.api.v1.internal.inbounds_endpoints import (
GetInboundEndpoint,
ListInboundsEndpoint,
)
from sinch.domains.sms.models.v1.internal.inbound_id_request import (
InboundIdRequest,
)
from sinch.domains.sms.models.v1.internal.list_inbounds_request import (
ListInboundsRequest,
)
from sinch.domains.sms.models.v1.types.inbound_message import InboundMessage


class Inbounds(BaseSms):
def get(self, inbound_id: str, **kwargs) -> InboundMessage:
"""
This operation retrieves a specific inbound message using the provided inbound ID.

:param inbound_id: The inbound ID found when listing inbound messages. (required)
:type inbound_id: str
:param **kwargs: Additional parameters for the request.
:type **kwargs: dict

:returns: InboundMessage
:rtype: InboundMessage

For detailed documentation, visit https://developers.sinch.com/docs/sms/.
"""
request_data = InboundIdRequest(inbound_id=inbound_id, **kwargs)
return self._request(GetInboundEndpoint, request_data)

def list(
self,
page: Optional[int] = None,
page_size: Optional[int] = None,
to: Optional[List[str]] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
client_reference: Optional[str] = None,
**kwargs,
) -> Paginator[InboundMessage]:
"""
With the list operation,
you can list all inbound messages that you have received. This operation supports pagination. Inbounds are returned in reverse chronological order.

:param page: The page number starting from 0. (optional)
:type page: Optional[int]
:param page_size: Determines the size of a page (optional)
:type page_size: Optional[int]
:param to: Only list messages sent to this destination. Multiple phone numbers formatted as either
[E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) or short codes can be comma separated.
(optional)
:type to: Optional[List[str]]
:param start_date: Only list messages received at or after this date/time. Formatted as
[ISO-8601](https://en.wikipedia.org/wiki/ISO_8601): `YYYY-MM-DDThh:mm:ss.SSSZ`. Default: Now-24
(optional)
:type start_date: Optional[datetime]

:param end_date: Only list messages received before this date/time. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601): `YYYY-MM-DDThh:mm:ss.SSSZ`. (optional)
:type end_date: Optional[datetime]
:param client_reference: Using a client reference in inbound messages requires additional setup on your account.
Contact your [account manager](https://dashboard.sinch.com/settings/account-details) to enable this feature.
Only list inbound messages that are in response to messages with a previously provided client reference.
(optional)
:type client_reference: Optional[str]
:param **kwargs: Additional parameters for the request.
:type **kwargs: dict

:returns: Paginator[InboundMessage]
:rtype: Paginator[InboundMessage]

For detailed documentation, visit https://developers.sinch.com/docs/sms/.
"""
endpoint = ListInboundsEndpoint(
project_id=self._get_path_identifier(),
request_data=ListInboundsRequest(
page=page,
page_size=page_size,
to=to,
start_date=start_date,
end_date=end_date,
client_reference=client_reference,
**kwargs,
),
)

endpoint.set_authentication_method(self._sinch)

return SMSPaginator(sinch=self._sinch, endpoint=endpoint)
62 changes: 62 additions & 0 deletions sinch/domains/sms/api/v1/internal/inbounds_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from sinch.core.enums import HTTPAuthentication, HTTPMethods
from sinch.core.models.http_response import HTTPResponse
from sinch.core.models.utils import model_dump_for_query_params
from sinch.domains.sms.api.v1.exceptions import SmsException
from sinch.domains.sms.api.v1.internal.base.sms_endpoint import SmsEndpoint
from sinch.domains.sms.models.v1.internal.inbound_id_request import (
InboundIdRequest,
)
from sinch.domains.sms.models.v1.internal.list_inbounds_request import (
ListInboundsRequest,
)
from sinch.domains.sms.models.v1.internal.list_inbounds_response import (
ListInboundsResponse,
)
from sinch.domains.sms.models.v1.types.inbound_message import InboundMessage


class GetInboundEndpoint(SmsEndpoint):
ENDPOINT_URL = "{origin}/xms/v1/{project_id}/inbounds/{inbound_id}"
HTTP_METHOD = HTTPMethods.GET.value
HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value

def __init__(self, project_id: str, request_data: InboundIdRequest):
super(GetInboundEndpoint, self).__init__(project_id, request_data)
self.project_id = project_id
self.request_data = request_data

def handle_response(self, response: HTTPResponse) -> InboundMessage:
try:
super(GetInboundEndpoint, self).handle_response(response)
except SmsException as e:
raise SmsException(
message=e.args[0],
response=e.http_response,
is_from_server=e.is_from_server,
)
return self.process_response_model(response.body, InboundMessage)


class ListInboundsEndpoint(SmsEndpoint):
ENDPOINT_URL = "{origin}/xms/v1/{project_id}/inbounds"
HTTP_METHOD = HTTPMethods.GET.value
HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value

def __init__(self, project_id: str, request_data: ListInboundsRequest):
super(ListInboundsEndpoint, self).__init__(project_id, request_data)
self.project_id = project_id
self.request_data = request_data

def build_query_params(self) -> dict:
return model_dump_for_query_params(self.request_data)

def handle_response(self, response: HTTPResponse) -> ListInboundsResponse:
try:
super(ListInboundsEndpoint, self).handle_response(response)
except SmsException as e:
raise SmsException(
message=e.args[0],
response=e.http_response,
is_from_server=e.is_from_server,
)
return self.process_response_model(response.body, ListInboundsResponse)
12 changes: 12 additions & 0 deletions sinch/domains/sms/models/v1/internal/inbound_id_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pydantic import Field

from sinch.domains.sms.models.v1.internal.base import (
BaseModelConfigurationRequest,
)


class InboundIdRequest(BaseModelConfigurationRequest):
inbound_id: str = Field(
default=...,
description="The unique identifier of the inbound message.",
)
15 changes: 15 additions & 0 deletions sinch/domains/sms/models/v1/internal/list_inbounds_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Optional
from datetime import datetime
from pydantic import StrictInt, StrictStr, conlist
from sinch.domains.sms.models.v1.internal.base import (
BaseModelConfigurationRequest,
)


class ListInboundsRequest(BaseModelConfigurationRequest):
page: Optional[StrictInt] = None
page_size: Optional[StrictInt] = None
to: Optional[conlist(StrictStr)] = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
client_reference: Optional[StrictStr] = None
31 changes: 31 additions & 0 deletions sinch/domains/sms/models/v1/internal/list_inbounds_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Optional

from pydantic import Field, StrictInt, conlist

from sinch.domains.sms.models.v1.internal.base import (
BaseModelConfigurationResponse,
)
from sinch.domains.sms.models.v1.types.inbound_message import InboundMessage


class ListInboundsResponse(BaseModelConfigurationResponse):
count: Optional[StrictInt] = Field(
default=None,
description="The total number of inbounds matching the given filters",
)
page: Optional[StrictInt] = Field(
default=None, description="The requested page."
)
inbounds: Optional[conlist(InboundMessage)] = Field(
default=None,
description="The page of inbounds matching the given filters.",
)
page_size: Optional[StrictInt] = Field(
default=None,
description="The number of inbounds returned in this request.",
)

@property
def content(self):
"""Returns the content of the inbounds list."""
return self.inbounds or []
14 changes: 14 additions & 0 deletions sinch/domains/sms/models/v1/shared/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
)
from sinch.domains.sms.models.v1.shared.text_request import TextRequest
from sinch.domains.sms.models.v1.shared.text_response import TextResponse
from sinch.domains.sms.models.v1.shared.base_mo_message import BaseMOMessage
from sinch.domains.sms.models.v1.shared.mo_text_message import MOTextMessage
from sinch.domains.sms.models.v1.shared.mo_binary_message import (
MOBinaryMessage,
)
from sinch.domains.sms.models.v1.shared.mo_media_item import MOMediaItem
from sinch.domains.sms.models.v1.shared.mo_media_body import MOMediaBody
from sinch.domains.sms.models.v1.shared.mo_media_message import MOMediaMessage

__all__ = [
"AddKeyword",
Expand All @@ -30,4 +38,10 @@
"RemoveKeyword",
"TextRequest",
"TextResponse",
"BaseMOMessage",
"MOTextMessage",
"MOBinaryMessage",
"MOMediaItem",
"MOMediaBody",
"MOMediaMessage",
]
Loading
Loading