From 6637bece35a6ba0150f06b728901d7fbef000b49 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 29 Jan 2026 10:46:31 +0100 Subject: [PATCH 1/5] DEVEXP-794: Conversation Messages - Snippets --- .../messages/send_text_message/snippet.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 examples/snippets/conversation/messages/send_text_message/snippet.py diff --git a/examples/snippets/conversation/messages/send_text_message/snippet.py b/examples/snippets/conversation/messages/send_text_message/snippet.py new file mode 100644 index 0000000..a5c4844 --- /dev/null +++ b/examples/snippets/conversation/messages/send_text_message/snippet.py @@ -0,0 +1,43 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +# The ID of the Conversation App to send the message from +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "SMS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] +# The phone number of the sender in E.164 format (e.g. +46701234567) +channel_properties = { + "SMS_SENDER": "SINCH_PHONE_NUMBER" +} + +response = sinch_client.conversation.messages.send_text_message( + app_id=app_id, + text="[Python SDK: Conversation] Sample text message", + recipient_identities=recipient_identities, + channel_properties=channel_properties, + channel_properties=channel_properties +) + +print(f"Successfully sent text message.\n{response}") From 95c1c844a8b90d967794f73f440143bb48e8eac1 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Fri, 30 Jan 2026 09:38:45 +0100 Subject: [PATCH 2/5] DEVEXP-794: Conversation Messages - Snippets --- .../conversation/messages/send/snippet.py | 38 ++++++++++++++ .../messages/send_card_message/snippet.py | 41 +++++++++++++++ .../messages/send_carousel_message/snippet.py | 51 +++++++++++++++++++ .../messages/send_choice_message/snippet.py | 44 ++++++++++++++++ .../send_contact_info_message/snippet.py | 41 +++++++++++++++ .../messages/send_list_message/snippet.py | 50 ++++++++++++++++++ .../messages/send_location_message/snippet.py | 41 +++++++++++++++ .../messages/send_media_message/snippet.py | 40 +++++++++++++++ .../messages/send_template_message/snippet.py | 43 ++++++++++++++++ .../messages/send_text_message/snippet.py | 8 +-- 10 files changed, 390 insertions(+), 7 deletions(-) create mode 100644 examples/snippets/conversation/messages/send/snippet.py create mode 100644 examples/snippets/conversation/messages/send_card_message/snippet.py create mode 100644 examples/snippets/conversation/messages/send_carousel_message/snippet.py create mode 100644 examples/snippets/conversation/messages/send_choice_message/snippet.py create mode 100644 examples/snippets/conversation/messages/send_contact_info_message/snippet.py create mode 100644 examples/snippets/conversation/messages/send_list_message/snippet.py create mode 100644 examples/snippets/conversation/messages/send_location_message/snippet.py create mode 100644 examples/snippets/conversation/messages/send_media_message/snippet.py create mode 100644 examples/snippets/conversation/messages/send_template_message/snippet.py diff --git a/examples/snippets/conversation/messages/send/snippet.py b/examples/snippets/conversation/messages/send/snippet.py new file mode 100644 index 0000000..da262d1 --- /dev/null +++ b/examples/snippets/conversation/messages/send/snippet.py @@ -0,0 +1,38 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +# The ID of the Conversation App to send the message from +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + {"channel": "RCS", "identity": "RECIPIENT_PHONE_NUMBER"} +] + +response = sinch_client.conversation.messages.send( + app_id=app_id, + message={ + "text_message": { + "text": "[Python SDK: Conversation Message] Sample text message" + } + }, + recipient_identities=recipient_identities +) + +print(f"Successfully sent message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_card_message/snippet.py b/examples/snippets/conversation/messages/send_card_message/snippet.py new file mode 100644 index 0000000..1c59249 --- /dev/null +++ b/examples/snippets/conversation/messages/send_card_message/snippet.py @@ -0,0 +1,41 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +card_message = { + "title": "Card title", + "description": "Optional card description", +} + +response = sinch_client.conversation.messages.send_card_message( + app_id=app_id, + card_message=card_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent card message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_carousel_message/snippet.py b/examples/snippets/conversation/messages/send_carousel_message/snippet.py new file mode 100644 index 0000000..f606a2c --- /dev/null +++ b/examples/snippets/conversation/messages/send_carousel_message/snippet.py @@ -0,0 +1,51 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +carousel_message = { + "cards": [ + { + "title": "Card 1", + "description": "First card description", + "choices": [{"text_message": {"text": "Option 1"}}], + }, + { + "title": "Card 2", + "description": "Second card description", + "choices": [{"url_message": {"title": "Link", "url": "https://example.com"}}], + }, + ], +} + +response = sinch_client.conversation.messages.send_carousel_message( + app_id=app_id, + carousel_message=carousel_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent carousel message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_choice_message/snippet.py b/examples/snippets/conversation/messages/send_choice_message/snippet.py new file mode 100644 index 0000000..539e953 --- /dev/null +++ b/examples/snippets/conversation/messages/send_choice_message/snippet.py @@ -0,0 +1,44 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +choice_message = { + "text_message": {"text": "Choose an option:"}, + "choices": [ + {"text_message": {"text": "Option A"}, "postback_data": "option_a"}, + {"text_message": {"text": "Option B"}, "postback_data": "option_b"}, + ], +} + +response = sinch_client.conversation.messages.send_choice_message( + app_id=app_id, + choice_message=choice_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent choice message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_contact_info_message/snippet.py b/examples/snippets/conversation/messages/send_contact_info_message/snippet.py new file mode 100644 index 0000000..f19354d --- /dev/null +++ b/examples/snippets/conversation/messages/send_contact_info_message/snippet.py @@ -0,0 +1,41 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +contact_info_message = { + "name": {"full_name": "John Doe"}, + "phone_numbers": [{"phone_number": "+1234567890"}], +} + +response = sinch_client.conversation.messages.send_contact_info_message( + app_id=app_id, + contact_info_message=contact_info_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent contact info message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_list_message/snippet.py b/examples/snippets/conversation/messages/send_list_message/snippet.py new file mode 100644 index 0000000..e9f7164 --- /dev/null +++ b/examples/snippets/conversation/messages/send_list_message/snippet.py @@ -0,0 +1,50 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +list_message = { + "title": "Choose an option", + "description": "Select from the list below", + "sections": [ + { + "title": "Section 1", + "items": [ + {"choice": {"title": "Option A", "postback_data": "option_a"}}, + {"choice": {"title": "Option B", "postback_data": "option_b"}}, + ], + }, + ], +} + +response = sinch_client.conversation.messages.send_list_message( + app_id=app_id, + list_message=list_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent list message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_location_message/snippet.py b/examples/snippets/conversation/messages/send_location_message/snippet.py new file mode 100644 index 0000000..c910c48 --- /dev/null +++ b/examples/snippets/conversation/messages/send_location_message/snippet.py @@ -0,0 +1,41 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +location_message = { + "title": "Our office", + "coordinates": {"latitude": 59.3293, "longitude": 18.0686}, +} + +response = sinch_client.conversation.messages.send_location_message( + app_id=app_id, + location_message=location_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent location message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_media_message/snippet.py b/examples/snippets/conversation/messages/send_media_message/snippet.py new file mode 100644 index 0000000..cb89a5d --- /dev/null +++ b/examples/snippets/conversation/messages/send_media_message/snippet.py @@ -0,0 +1,40 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +media_message = { + "url": "https://example.com/image.jpg", +} + +response = sinch_client.conversation.messages.send_media_message( + app_id=app_id, + media_message=media_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent media message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_template_message/snippet.py b/examples/snippets/conversation/messages/send_template_message/snippet.py new file mode 100644 index 0000000..1d23fea --- /dev/null +++ b/examples/snippets/conversation/messages/send_template_message/snippet.py @@ -0,0 +1,43 @@ +""" +Sinch Python Snippet + +TODO: Update links when v2 is released. +This snippet is available at https://github.com/sinch/sinch-sdk-python/blob/v2.0/docs/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", + conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" +) + +app_id = "CONVERSATION_APP_ID" +# The phone number of the recipient in E.164 format (e.g. +46701234567) +recipient_identities = [ + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } +] + +template_message = { + "omni_template": { + "template_id": "YOUR_TEMPLATE_ID", + "version": "1", + }, +} + +response = sinch_client.conversation.messages.send_template_message( + app_id=app_id, + template_message=template_message, + recipient_identities=recipient_identities +) + +print(f"Successfully sent template message.\n{response}") diff --git a/examples/snippets/conversation/messages/send_text_message/snippet.py b/examples/snippets/conversation/messages/send_text_message/snippet.py index a5c4844..6a5bebb 100644 --- a/examples/snippets/conversation/messages/send_text_message/snippet.py +++ b/examples/snippets/conversation/messages/send_text_message/snippet.py @@ -27,17 +27,11 @@ "identity": "RECIPIENT_PHONE_NUMBER" } ] -# The phone number of the sender in E.164 format (e.g. +46701234567) -channel_properties = { - "SMS_SENDER": "SINCH_PHONE_NUMBER" -} response = sinch_client.conversation.messages.send_text_message( app_id=app_id, text="[Python SDK: Conversation] Sample text message", - recipient_identities=recipient_identities, - channel_properties=channel_properties, - channel_properties=channel_properties + recipient_identities=recipient_identities ) print(f"Successfully sent text message.\n{response}") From 6ab1c1a40195d0ae6f59926930849b0df6f27434 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Fri, 30 Jan 2026 10:06:42 +0100 Subject: [PATCH 3/5] apis unit test --- .../v1/test_conversation_messages.py | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 tests/unit/domains/conversation/v1/test_conversation_messages.py diff --git a/tests/unit/domains/conversation/v1/test_conversation_messages.py b/tests/unit/domains/conversation/v1/test_conversation_messages.py new file mode 100644 index 0000000..86ab052 --- /dev/null +++ b/tests/unit/domains/conversation/v1/test_conversation_messages.py @@ -0,0 +1,306 @@ +""" +Unit tests for Conversation Messages API +""" +from unittest.mock import MagicMock +import pytest +from sinch.domains.conversation.conversation import Conversation +from sinch.domains.conversation.api.v1 import Messages +from sinch.domains.conversation.api.v1.internal import ( + DeleteMessageEndpoint, + GetMessageEndpoint, + SendMessageEndpoint, + UpdateMessageMetadataEndpoint, +) +from sinch.domains.conversation.models.v1.messages.internal.request import ( + MessageIdRequest, + UpdateMessageMetadataRequest, + SendMessageRequest, +) +from sinch.domains.conversation.models.v1.messages.response.types import ( + SendMessageResponse, +) + + +@pytest.fixture +def mock_send_message_response(): + return SendMessageResponse( + message_id="01FC66621SND04119Z8PMV1QPQ", + ) + + +@pytest.fixture +def mock_conversation_message_response(): + response = MagicMock() + response.id = "01FC66621GET02119Z8PMV1QPQ" + return response + + +def test_conversation_expects_messages_attribute(mock_sinch_client_conversation): + """Test that Conversation exposes .messages as Messages instance.""" + conversation = Conversation(mock_sinch_client_conversation) + assert isinstance(conversation.messages, Messages) + + +def test_messages_delete_expects_correct_request( + mock_sinch_client_conversation, mocker +): + """Test that delete sends the correct request.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + None + ) + spy_endpoint = mocker.spy(DeleteMessageEndpoint, "__init__") + + message_id = "01FC66621DEL01119Z8PMV1QPQ" + conversation = Conversation(mock_sinch_client_conversation) + conversation.messages.delete(message_id=message_id) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert kwargs["project_id"] == "test_project_id" + assert isinstance(kwargs["request_data"], MessageIdRequest) + assert kwargs["request_data"].message_id == message_id + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_get_expects_correct_request( + mock_sinch_client_conversation, mock_conversation_message_response, mocker +): + """Test that get sends the correct request and returns the response.""" + message_id = "01FC66621GET02119Z8PMV1QPQ" + mock_conversation_message_response.id = message_id + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_conversation_message_response + ) + spy_endpoint = mocker.spy(GetMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.get(message_id=message_id) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert kwargs["project_id"] == "test_project_id" + assert isinstance(kwargs["request_data"], MessageIdRequest) + assert kwargs["request_data"].message_id == message_id + assert response.id == message_id + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_update_expects_correct_request( + mock_sinch_client_conversation, mock_conversation_message_response, mocker +): + """Test that update sends the correct request and returns the response.""" + message_id = "01FC66621UPD03119Z8PMV1QPQ" + mock_conversation_message_response.id = message_id + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_conversation_message_response + ) + spy_endpoint = mocker.spy(UpdateMessageMetadataEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.update( + message_id=message_id, + metadata="updated-metadata", + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert kwargs["project_id"] == "test_project_id" + assert isinstance(kwargs["request_data"], UpdateMessageMetadataRequest) + assert kwargs["request_data"].message_id == message_id + assert kwargs["request_data"].metadata == "updated-metadata" + assert response.id == message_id + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_send_expects_correct_request( + mock_sinch_client_conversation, mock_send_message_response, mocker +): + """Test that send sends the correct request and returns SendMessageResponse.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_send_message_response + ) + spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.send( + app_id="APP_ID", + message={"text_message": {"text": "Hello"}}, + recipient_identities=[ + {"channel": "RCS", "identity": "+46701234567"}, + ], + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert kwargs["project_id"] == "test_project_id" + assert isinstance(kwargs["request_data"], SendMessageRequest) + assert kwargs["request_data"].app_id == "APP_ID" + assert kwargs["request_data"].message.text_message is not None + assert kwargs["request_data"].message.text_message.text == "Hello" + assert isinstance(response, SendMessageResponse) + assert response.message_id == "01FC66621SND04119Z8PMV1QPQ" + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_send_text_message_expects_correct_request( + mock_sinch_client_conversation, mock_send_message_response, mocker +): + """Test that send_text_message sends the correct request.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_send_message_response + ) + spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.send_text_message( + app_id="APP_ID", + text="Hello", + recipient_identities=[ + {"channel": "RCS", "identity": "+46701234567"}, + ], + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert isinstance(kwargs["request_data"], SendMessageRequest) + assert kwargs["request_data"].app_id == "APP_ID" + assert kwargs["request_data"].message.text_message is not None + assert kwargs["request_data"].message.text_message.text == "Hello" + assert isinstance(response, SendMessageResponse) + assert response.message_id == "01FC66621SND04119Z8PMV1QPQ" + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_send_card_message_expects_correct_request( + mock_sinch_client_conversation, mock_send_message_response, mocker +): + """Test that send_card_message sends the correct request.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_send_message_response + ) + spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.send_card_message( + app_id="APP_ID", + card_message={"title": "Card title", "description": "Description"}, + recipient_identities=[ + {"channel": "RCS", "identity": "+46701234567"}, + ], + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert isinstance(kwargs["request_data"], SendMessageRequest) + assert kwargs["request_data"].app_id == "APP_ID" + assert kwargs["request_data"].message.card_message is not None + assert kwargs["request_data"].message.card_message.title == "Card title" + assert isinstance(response, SendMessageResponse) + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_send_choice_message_expects_correct_request( + mock_sinch_client_conversation, mock_send_message_response, mocker +): + """Test that send_choice_message sends the correct request.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_send_message_response + ) + spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.send_choice_message( + app_id="APP_ID", + choice_message={ + "text_message": {"text": "Choose:"}, + "choices": [ + {"text_message": {"text": "Option A"}, "postback_data": "a"}, + ], + }, + recipient_identities=[ + {"channel": "RCS", "identity": "+46701234567"}, + ], + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert isinstance(kwargs["request_data"], SendMessageRequest) + assert kwargs["request_data"].app_id == "APP_ID" + assert kwargs["request_data"].message.choice_message is not None + assert kwargs["request_data"].message.choice_message.text_message.text == "Choose:" + assert len(kwargs["request_data"].message.choice_message.choices) == 1 + assert isinstance(response, SendMessageResponse) + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_send_with_contact_id_expects_correct_request( + mock_sinch_client_conversation, mock_send_message_response, mocker +): + """Test that send with contact_id builds recipient correctly.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_send_message_response + ) + spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.send( + app_id="APP_ID", + message={"text_message": {"text": "Hi"}}, + contact_id="CONTACT_123", + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert isinstance(kwargs["request_data"], SendMessageRequest) + assert kwargs["request_data"].app_id == "APP_ID" + assert kwargs["request_data"].recipient.contact_id == "CONTACT_123" + assert isinstance(response, SendMessageResponse) + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_send_media_message_expects_correct_request( + mock_sinch_client_conversation, mock_send_message_response, mocker +): + """Test that send_media_message sends the correct request.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_send_message_response + ) + spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.send_media_message( + app_id="APP_ID", + media_message={"url": "https://example.com/image.jpg"}, + recipient_identities=[ + {"channel": "RCS", "identity": "+46701234567"}, + ], + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert kwargs["request_data"].message.media_message is not None + assert kwargs["request_data"].message.media_message.url == "https://example.com/image.jpg" + assert isinstance(response, SendMessageResponse) + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + +def test_messages_delete_with_messages_source_expects_correct_request( + mock_sinch_client_conversation, mocker +): + """Test that delete with messages_source sends the correct request.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + None + ) + spy_endpoint = mocker.spy(DeleteMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + message_id = "01FC66621DL205119Z8PMV1QPQ" + conversation.messages.delete( + message_id=message_id, + messages_source="DISPATCH_SOURCE", + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert kwargs["request_data"].message_id == message_id + assert kwargs["request_data"].messages_source == "DISPATCH_SOURCE" From 7e6a27dd85e4b99bdc35525a388a0083f1f7a1bb Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Fri, 30 Jan 2026 10:35:52 +0100 Subject: [PATCH 4/5] update Migration Guide --- MIGRATION_GUIDE.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 430f6a8..b825958 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -93,6 +93,39 @@ sinch_client = SinchClient( sinch_client.configuration.conversation_region = "eu" ``` +### [`Conversation`](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/conversation) + +#### Replacement models + +##### Messages (send, get, delete, list) + +| Old class | New class | +|-----------|-----------| +| `sinch.domains.conversation.models.message.requests.SendConversationMessageRequest` | `send()`: pass `app_id`, `message` (dict or [`SendMessageRequestBodyDict`](sinch/domains/conversation/models/v1/messages/types/send_message_request_body_dict.py)), and either `contact_id` or `recipient_identities`. Internally uses [`SendMessageRequest`](sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py), [`SendMessageRequestBody`](sinch/domains/conversation/models/v1/messages/internal/request/send_message_request_body.py). For typed payloads use `send_text_message()`, `send_card_message()`, etc. +| `sinch.domains.conversation.models.message.responses.SendConversationMessageResponse` | [`SendMessageResponse`](sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py) (`message_id`, optional `accepted_time` as `datetime`) | +| `sinch.domains.conversation.models.message.requests.GetConversationMessageRequest` | `get(message_id, messages_source=None, **kwargs)`. Internally uses [`MessageIdRequest`](sinch/domains/conversation/models/v1/messages/internal/request/message_id_request.py). | +| `sinch.domains.conversation.models.message.responses.GetConversationMessageResponse` | [`ConversationMessageResponse`](sinch/domains/conversation/models/v1/messages/response/types/__init__.py) (Union of app/contact message response types) | +| `sinch.domains.conversation.models.message.requests.DeleteConversationMessageRequest` | `delete(message_id, messages_source=None, **kwargs)`. Internally uses [`MessageIdRequest`](sinch/domains/conversation/models/v1/messages/internal/request/message_id_request.py). | +| `sinch.domains.conversation.models.message.responses.DeleteConversationMessageResponse` | `None` (method returns `None`) | +| `sinch.domains.conversation.models.message.requests.ListConversationMessagesRequest` | `list()` with individual parameters: `conversation_id`, `contact_id`, `app_id`, `page_size`, `page_token`, `view`, `messages_source`, `only_recipient_originated` (signature aligned with V1 where available) | +| `sinch.domains.conversation.models.message.responses.ListConversationMessagesResponse` | Response type for `list()` (messages list, next_page_token) | + +#### Replacement APIs + +The Conversation domain API access remains `sinch_client.conversation`; message operations are under `sinch_client.conversation.messages`. Recipient is specified with exactly one of `contact_id` or `recipient_identities` (list of `{channel, identity}`). + +##### Messages API + +| Old method | New method in `conversation.messages` | +|------------|----------------------------------------| +| `send()` with `SendConversationMessageRequest` | Use convenience methods: `send_text_message()`, `send_card_message()`, `send_carousel_message()`, `send_choice_message()`, `send_contact_info_message()`, `send_list_message()`, `send_location_message()`, `send_media_message()`, `send_template_message()`
Or `send()` with `app_id`, `message` (dict or `SendMessageRequestBodyDict`), and either `contact_id` or `recipient_identities` | +| `get()` with `GetConversationMessageRequest` | `get()` with `message_id: str` parameter | +| `delete()` with `DeleteConversationMessageRequest` | `delete()` with `message_id: str` parameter | +| `list()` with `ListConversationMessagesRequest` | In Progress | +| — | **New in V2:** `update()` with `message_id`, `metadata`, and optional `messages_source`| + +
+ ### [`SMS`](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/sms) #### Replacement models From d447bfab524a2e2ae2518e746f4630652ccfffc0 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Fri, 30 Jan 2026 17:41:11 +0100 Subject: [PATCH 5/5] solve PR comments --- .../conversation/messages/send/snippet.py | 5 +- .../messages/send_card_message/snippet.py | 5 + .../messages/send_carousel_message/snippet.py | 1 + .../messages/send_choice_message/snippet.py | 1 + .../send_contact_info_message/snippet.py | 1 + .../messages/send_list_message/snippet.py | 1 + .../messages/send_location_message/snippet.py | 1 + .../messages/send_media_message/snippet.py | 1 + .../messages/send_template_message/snippet.py | 3 +- .../v1/test_conversation_messages.py | 94 +++++++++---------- 10 files changed, 64 insertions(+), 49 deletions(-) diff --git a/examples/snippets/conversation/messages/send/snippet.py b/examples/snippets/conversation/messages/send/snippet.py index da262d1..970c9c9 100644 --- a/examples/snippets/conversation/messages/send/snippet.py +++ b/examples/snippets/conversation/messages/send/snippet.py @@ -22,7 +22,10 @@ app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ - {"channel": "RCS", "identity": "RECIPIENT_PHONE_NUMBER"} + { + "channel": "RCS", + "identity": "RECIPIENT_PHONE_NUMBER" + } ] response = sinch_client.conversation.messages.send( diff --git a/examples/snippets/conversation/messages/send_card_message/snippet.py b/examples/snippets/conversation/messages/send_card_message/snippet.py index 1c59249..5300eaf 100644 --- a/examples/snippets/conversation/messages/send_card_message/snippet.py +++ b/examples/snippets/conversation/messages/send_card_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ @@ -30,6 +31,10 @@ card_message = { "title": "Card title", "description": "Optional card description", + "choices": [ + {"text_message": {"text": "Yes"}, "postback_data": "yes"}, + {"text_message": {"text": "No"}, "postback_data": "no"}, + ] } response = sinch_client.conversation.messages.send_card_message( diff --git a/examples/snippets/conversation/messages/send_carousel_message/snippet.py b/examples/snippets/conversation/messages/send_carousel_message/snippet.py index f606a2c..bb8ecb4 100644 --- a/examples/snippets/conversation/messages/send_carousel_message/snippet.py +++ b/examples/snippets/conversation/messages/send_carousel_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ diff --git a/examples/snippets/conversation/messages/send_choice_message/snippet.py b/examples/snippets/conversation/messages/send_choice_message/snippet.py index 539e953..315e13d 100644 --- a/examples/snippets/conversation/messages/send_choice_message/snippet.py +++ b/examples/snippets/conversation/messages/send_choice_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ diff --git a/examples/snippets/conversation/messages/send_contact_info_message/snippet.py b/examples/snippets/conversation/messages/send_contact_info_message/snippet.py index f19354d..70f84c5 100644 --- a/examples/snippets/conversation/messages/send_contact_info_message/snippet.py +++ b/examples/snippets/conversation/messages/send_contact_info_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ diff --git a/examples/snippets/conversation/messages/send_list_message/snippet.py b/examples/snippets/conversation/messages/send_list_message/snippet.py index e9f7164..3b7010a 100644 --- a/examples/snippets/conversation/messages/send_list_message/snippet.py +++ b/examples/snippets/conversation/messages/send_list_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ diff --git a/examples/snippets/conversation/messages/send_location_message/snippet.py b/examples/snippets/conversation/messages/send_location_message/snippet.py index c910c48..451d0d8 100644 --- a/examples/snippets/conversation/messages/send_location_message/snippet.py +++ b/examples/snippets/conversation/messages/send_location_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ diff --git a/examples/snippets/conversation/messages/send_media_message/snippet.py b/examples/snippets/conversation/messages/send_media_message/snippet.py index cb89a5d..df7aa97 100644 --- a/examples/snippets/conversation/messages/send_media_message/snippet.py +++ b/examples/snippets/conversation/messages/send_media_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ diff --git a/examples/snippets/conversation/messages/send_template_message/snippet.py b/examples/snippets/conversation/messages/send_template_message/snippet.py index 1d23fea..cb604a7 100644 --- a/examples/snippets/conversation/messages/send_template_message/snippet.py +++ b/examples/snippets/conversation/messages/send_template_message/snippet.py @@ -18,6 +18,7 @@ conversation_region=os.environ.get("SINCH_CONVERSATION_REGION") or "MY_CONVERSATION_REGION" ) +# The ID of the Conversation App to send the message from app_id = "CONVERSATION_APP_ID" # The phone number of the recipient in E.164 format (e.g. +46701234567) recipient_identities = [ @@ -29,7 +30,7 @@ template_message = { "omni_template": { - "template_id": "YOUR_TEMPLATE_ID", + "template_id": "TEMPLATE_ID", "version": "1", }, } diff --git a/tests/unit/domains/conversation/v1/test_conversation_messages.py b/tests/unit/domains/conversation/v1/test_conversation_messages.py index 86ab052..144d30d 100644 --- a/tests/unit/domains/conversation/v1/test_conversation_messages.py +++ b/tests/unit/domains/conversation/v1/test_conversation_messages.py @@ -62,6 +62,28 @@ def test_messages_delete_expects_correct_request( mock_sinch_client_conversation.configuration.transport.request.assert_called_once() +def test_messages_delete_with_messages_source_expects_correct_request( + mock_sinch_client_conversation, mocker +): + """Test that delete with messages_source sends the correct request.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + None + ) + spy_endpoint = mocker.spy(DeleteMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + message_id = "01FC66621DL205119Z8PMV1QPQ" + conversation.messages.delete( + message_id=message_id, + messages_source="DISPATCH_SOURCE", + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert kwargs["request_data"].message_id == message_id + assert kwargs["request_data"].messages_source == "DISPATCH_SOURCE" + + def test_messages_get_expects_correct_request( mock_sinch_client_conversation, mock_conversation_message_response, mocker ): @@ -142,6 +164,31 @@ def test_messages_send_expects_correct_request( mock_sinch_client_conversation.configuration.transport.request.assert_called_once() +def test_messages_send_with_contact_id_expects_correct_request( + mock_sinch_client_conversation, mock_send_message_response, mocker +): + """Test that send with contact_id builds recipient correctly.""" + mock_sinch_client_conversation.configuration.transport.request.return_value = ( + mock_send_message_response + ) + spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") + + conversation = Conversation(mock_sinch_client_conversation) + response = conversation.messages.send( + app_id="APP_ID", + message={"text_message": {"text": "Hi"}}, + contact_id="CONTACT_123", + ) + + spy_endpoint.assert_called_once() + _, kwargs = spy_endpoint.call_args + assert isinstance(kwargs["request_data"], SendMessageRequest) + assert kwargs["request_data"].app_id == "APP_ID" + assert kwargs["request_data"].recipient.contact_id == "CONTACT_123" + assert isinstance(response, SendMessageResponse) + mock_sinch_client_conversation.configuration.transport.request.assert_called_once() + + def test_messages_send_text_message_expects_correct_request( mock_sinch_client_conversation, mock_send_message_response, mocker ): @@ -233,31 +280,6 @@ def test_messages_send_choice_message_expects_correct_request( mock_sinch_client_conversation.configuration.transport.request.assert_called_once() -def test_messages_send_with_contact_id_expects_correct_request( - mock_sinch_client_conversation, mock_send_message_response, mocker -): - """Test that send with contact_id builds recipient correctly.""" - mock_sinch_client_conversation.configuration.transport.request.return_value = ( - mock_send_message_response - ) - spy_endpoint = mocker.spy(SendMessageEndpoint, "__init__") - - conversation = Conversation(mock_sinch_client_conversation) - response = conversation.messages.send( - app_id="APP_ID", - message={"text_message": {"text": "Hi"}}, - contact_id="CONTACT_123", - ) - - spy_endpoint.assert_called_once() - _, kwargs = spy_endpoint.call_args - assert isinstance(kwargs["request_data"], SendMessageRequest) - assert kwargs["request_data"].app_id == "APP_ID" - assert kwargs["request_data"].recipient.contact_id == "CONTACT_123" - assert isinstance(response, SendMessageResponse) - mock_sinch_client_conversation.configuration.transport.request.assert_called_once() - - def test_messages_send_media_message_expects_correct_request( mock_sinch_client_conversation, mock_send_message_response, mocker ): @@ -282,25 +304,3 @@ def test_messages_send_media_message_expects_correct_request( assert kwargs["request_data"].message.media_message.url == "https://example.com/image.jpg" assert isinstance(response, SendMessageResponse) mock_sinch_client_conversation.configuration.transport.request.assert_called_once() - - -def test_messages_delete_with_messages_source_expects_correct_request( - mock_sinch_client_conversation, mocker -): - """Test that delete with messages_source sends the correct request.""" - mock_sinch_client_conversation.configuration.transport.request.return_value = ( - None - ) - spy_endpoint = mocker.spy(DeleteMessageEndpoint, "__init__") - - conversation = Conversation(mock_sinch_client_conversation) - message_id = "01FC66621DL205119Z8PMV1QPQ" - conversation.messages.delete( - message_id=message_id, - messages_source="DISPATCH_SOURCE", - ) - - spy_endpoint.assert_called_once() - _, kwargs = spy_endpoint.call_args - assert kwargs["request_data"].message_id == message_id - assert kwargs["request_data"].messages_source == "DISPATCH_SOURCE"