From f910f5b0269a8ac127abd2b9b9e70da777274928 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Mon, 26 Jan 2026 11:08:30 +0100 Subject: [PATCH 1/4] add sms typeddict --- sinch/domains/sms/api/v1/batches_apis.py | 19 +++++++++---------- sinch/domains/sms/models/v1/types/__init__.py | 2 ++ .../sms/models/v1/types/media_body_dict.py | 8 ++++++++ 3 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 sinch/domains/sms/models/v1/types/media_body_dict.py diff --git a/sinch/domains/sms/api/v1/batches_apis.py b/sinch/domains/sms/api/v1/batches_apis.py index 87616819..843ad482 100644 --- a/sinch/domains/sms/api/v1/batches_apis.py +++ b/sinch/domains/sms/api/v1/batches_apis.py @@ -30,12 +30,11 @@ ReplaceMediaRequest, ) from sinch.domains.sms.models.v1.shared import ( - MediaBody, TextRequest, BinaryRequest, MediaRequest, ) -from sinch.domains.sms.models.v1.types import DeliveryReportType +from sinch.domains.sms.models.v1.types import DeliveryReportType, MediaBodyDict from sinch.domains.sms.api.v1.internal import ( CancelBatchMessageEndpoint, DryRunEndpoint, @@ -303,7 +302,7 @@ def dry_run_mms( self, to: List[str], from_: str, - body: MediaBody, + body: MediaBodyDict, per_recipient: Optional[bool] = None, number_of_recipients: Optional[int] = None, parameters: Optional[Dict[str, Dict[str, str]]] = None, @@ -325,7 +324,7 @@ def dry_run_mms( :param from_: The sender phone number. (required) :type from_: str :param body: The message body. (required) - :type body: MediaBody + :type body: MediaBodyDict :param per_recipient: Whether to include per recipient details in the response (optional) :type per_recipient: Optional[bool] :param number_of_recipients: Max number of recipients to include per recipient details for in the response (optional) @@ -642,7 +641,7 @@ def replace_mms( batch_id: str, to: List[str], from_: str, - body: MediaBody, + body: MediaBodyDict, delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, @@ -664,7 +663,7 @@ def replace_mms( :param from_: The sender phone number. (required) :type from_: str :param body: The message body. (required) - :type body: MediaBody + :type body: MediaBodyDict :param delivery_report: The delivery report type. (optional) :type delivery_report: Optional[DeliveryReportType] :param send_at: The time to send the message at. (optional) @@ -910,7 +909,7 @@ def send_mms( self, to: List[str], from_: str, - body: MediaBody, + body: MediaBodyDict, delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, @@ -936,7 +935,7 @@ def send_mms( :param from_: The sender phone number. (required) :type from_: str :param body: The message body. (required) - :type body: MediaBody + :type body: MediaBodyDict :param delivery_report: The delivery report type. (optional) :type delivery_report: Optional[DeliveryReportType] :param send_at: The time to send the message at. (optional) @@ -1218,7 +1217,7 @@ def update_mms( from_: Optional[str] = None, to_add: Optional[List[str]] = None, to_remove: Optional[List[str]] = None, - body: Optional[MediaBody] = None, + body: Optional[MediaBodyDict] = None, delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, @@ -1241,7 +1240,7 @@ def update_mms( :param to_remove: The list of phone numbers to remove from the batch. (optional) :type to_remove: Optional[List[str]] :param body: The message body. (optional) - :type body: Optional[MediaBody] + :type body: Optional[MediaBodyDict] :param delivery_report: The delivery report type. (optional) :type delivery_report: Optional[DeliveryReportType] :param send_at: The time to send the message at. (optional) diff --git a/sinch/domains/sms/models/v1/types/__init__.py b/sinch/domains/sms/models/v1/types/__init__.py index f30dd14a..a52cfcc2 100644 --- a/sinch/domains/sms/models/v1/types/__init__.py +++ b/sinch/domains/sms/models/v1/types/__init__.py @@ -8,6 +8,7 @@ DeliveryStatusType, ) from sinch.domains.sms.models.v1.types.encoding_type import EncodingType +from sinch.domains.sms.models.v1.types.media_body_dict import MediaBodyDict from sinch.domains.sms.models.v1.types.recipient_delivery_report_type import ( RecipientDeliveryReportType, ) @@ -18,6 +19,7 @@ "DeliveryReportType", "DeliveryStatusType", "EncodingType", + "MediaBodyDict", "RecipientDeliveryReportType", ] diff --git a/sinch/domains/sms/models/v1/types/media_body_dict.py b/sinch/domains/sms/models/v1/types/media_body_dict.py new file mode 100644 index 00000000..5a3f9b8c --- /dev/null +++ b/sinch/domains/sms/models/v1/types/media_body_dict.py @@ -0,0 +1,8 @@ +from typing import TypedDict +from typing_extensions import NotRequired + + +class MediaBodyDict(TypedDict): + url: str + subject: NotRequired[str] + message: NotRequired[str] From c1c0a9edb9dbf5c056622e87fec6862e86a37e8e Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Mon, 26 Jan 2026 17:19:34 +0100 Subject: [PATCH 2/4] DEVEXP-1240: Conversation API - Messages (Send) E2E --- .../conversation/api/v1/internal/__init__.py | 2 + .../api/v1/internal/messages_endpoints.py | 31 + .../conversation/api/v1/messages_apis.py | 997 +++++++++++++++++- .../conversation/api/v1/utils/__init__.py | 15 + .../api/v1/utils/message_helpers.py | 122 +++ .../v1/messages/categories/app/app_message.py | 28 +- .../categories/calendar/calendar_message.py | 4 +- .../messages/categories/call/call_message.py | 4 +- .../messages/categories/card/card_message.py | 6 +- .../categories/card/card_message_field.py | 4 +- .../categories/card/message_properties.py | 4 +- .../categories/carousel/carousel_message.py | 6 +- .../carousel/carousel_message_field.py | 4 +- ...hannel_specific_contact_message_message.py | 4 +- .../channel_specific_message.py | 6 +- .../channel_specific_message_content.py | 14 +- .../kakaotalk/buttons/kakaotalk_button.py | 4 +- .../kakaotalk/commerce/kakaotalk_carousel.py | 4 +- .../commerce/kakaotalk_carousel_head.py | 4 +- .../commerce/kakaotalk_carousel_tail.py | 4 +- .../kakaotalk_channel_specific_message.py | 4 +- .../commerce/kakaotalk_commerce_image.py | 4 +- .../commerce/kakaotalk_commerce_message.py | 4 +- .../kakaotalk_regular_price_commerce.py | 4 +- .../kakaotalk/coupons/kakaotalk_coupon.py | 4 +- .../whatsapp/flows/flow_action_payload.py | 4 +- .../flows/whatsapp_interactive_body.py | 4 +- .../whatsapp_interactive_document_header.py | 4 +- .../flows/whatsapp_interactive_footer.py | 4 +- .../whatsapp_interactive_header_media.py | 4 +- .../whatsapp_interactive_image_header.py | 4 +- .../flows/whatsapp_interactive_text_header.py | 4 +- .../whatsapp_interactive_video_header.py | 4 +- .../whatsapp_interactive_nfm_reply.py | 4 +- .../whatsapp_interactive_nfm_reply_message.py | 4 +- .../whatsapp/payment/boleto.py | 4 +- .../whatsapp/payment/dynamic_pix.py | 4 +- .../whatsapp/payment/order_item.py | 4 +- .../whatsapp/payment/payment_link.py | 4 +- .../whatsapp/payment/payment_order.py | 4 +- .../payment/payment_order_details_content.py | 4 +- .../payment/payment_order_status_content.py | 4 +- .../payment/payment_order_status_order.py | 4 +- .../whatsapp/whatsapp_common_props.py | 4 +- .../categories/choice/choice_message.py | 6 +- .../categories/choice/choice_message_field.py | 4 +- .../choice}/choice_option.py | 6 +- .../categories/choice/choice_options.py | 4 +- .../choiceresponse/choice_response_message.py | 4 +- .../v1/messages/categories/common/reply_to.py | 4 +- .../categories/contact/contact_message.py | 22 +- .../contactinfo/contact_info_message.py | 4 +- .../contactinfo/contact_info_message_field.py | 4 +- .../categories/fallback/fallback_message.py | 4 +- .../types => categories/list}/list_item.py | 2 +- .../categories/list/list_item_choice.py | 4 +- .../categories/list/list_item_product.py | 4 +- .../messages/categories/list/list_message.py | 4 +- .../categories/list/list_message_field.py | 4 +- .../list/list_message_properties.py | 4 +- .../categories/location/location_message.py | 4 +- .../location/location_message_field.py | 4 +- .../categories/media/media_message_field.py | 4 +- .../categories/media/media_properties.py | 4 +- .../mediacard/media_card_message.py | 4 +- .../product_response_message.py | 4 +- .../sharelocation/share_location_message.py | 4 +- .../categories/template/template_message.py | 4 +- .../template_reference_channel_specific.py | 4 +- .../template/template_reference_field.py | 4 +- .../messages/categories/text/text_message.py | 4 +- .../categories/text/text_message_field.py | 4 +- .../v1/messages/categories/url/url_message.py | 4 +- .../v1/messages/internal/base/__init__.py | 6 +- .../internal/base/base_model_configuration.py | 18 +- .../v1/messages/internal/request/__init__.py | 16 + .../internal/request/message_id_request.py | 4 +- .../v1/messages/internal/request/recipient.py | 38 + .../internal/request/send_message_request.py | 102 ++ .../request/send_message_request_body.py | 43 + .../update_message_metadata_request.py | 4 +- .../v1/messages/response/message_response.py | 8 +- .../v1/messages/response/types/__init__.py | 10 +- .../response/types/payment_settings.py | 4 +- .../response/types/send_message_response.py | 15 + .../models/v1/messages/shared/__init__.py | 4 - .../models/v1/messages/shared/address_info.py | 4 +- .../models/v1/messages/shared/agent.py | 4 +- .../shared/app_message_common_props.py | 4 +- .../v1/messages/shared/channel_identity.py | 4 +- .../shared/channel_identity_request.py | 24 + .../models/v1/messages/shared/choice_item.py | 4 +- .../shared/contact_message_common_props.py | 4 +- .../models/v1/messages/shared/coordinates.py | 4 +- .../models/v1/messages/shared/email_info.py | 4 +- .../models/v1/messages/shared/list_section.py | 6 +- .../shared/message_response_common_props.py | 4 +- .../models/v1/messages/shared/name_info.py | 4 +- .../v1/messages/shared/organization_info.py | 4 +- .../v1/messages/shared/phone_number_info.py | 4 +- .../models/v1/messages/shared/product_item.py | 4 +- .../models/v1/messages/shared/reason.py | 4 +- .../models/v1/messages/shared/url_info.py | 4 +- .../models/v1/messages/types/__init__.py | 58 + .../messages/types/calendar_message_dict.py | 11 + .../v1/messages/types/call_message_dict.py | 6 + .../v1/messages/types/card_message_dict.py | 24 + .../messages/types/carousel_message_dict.py | 14 + .../v1/messages/types/choice_message_dict.py | 18 + .../v1/messages/types/choice_option_dict.py | 34 + .../types/contact_info_message_dict.py | 51 + .../v1/messages/types/coordinates_dict.py | 6 + .../v1/messages/types/list_message_dict.py | 51 + .../messages/types/location_message_dict.py | 12 + .../messages/types/media_properties_dict.py | 8 + .../v1/messages/types/message_content_type.py | 7 + .../messages/types/message_properties_dict.py | 6 + .../v1/messages/types/message_queue_type.py | 7 + .../types/metadata_update_strategy_type.py | 7 + .../types/processing_strategy_type.py | 7 + .../v1/messages/types/recipient_dict.py | 20 + .../types/send_message_request_body_dict.py | 46 + .../types/share_location_message_dict.py | 6 + .../messages/types/template_message_dict.py | 20 + .../v1/messages/types/text_message_dict.py | 5 + .../v1/messages/types/url_message_dict.py | 6 + .../features/steps/conversation.steps.py | 10 +- 127 files changed, 2056 insertions(+), 245 deletions(-) create mode 100644 sinch/domains/conversation/api/v1/utils/__init__.py create mode 100644 sinch/domains/conversation/api/v1/utils/message_helpers.py rename sinch/domains/conversation/models/v1/messages/{response/types => categories/channelspecific}/channel_specific_message_content.py (100%) rename sinch/domains/conversation/models/v1/messages/{response/types => categories/choice}/choice_option.py (100%) rename sinch/domains/conversation/models/v1/messages/{response/types => categories/list}/list_item.py (100%) create mode 100644 sinch/domains/conversation/models/v1/messages/internal/request/recipient.py create mode 100644 sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py create mode 100644 sinch/domains/conversation/models/v1/messages/internal/request/send_message_request_body.py create mode 100644 sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py create mode 100644 sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/call_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/card_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/carousel_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/choice_option_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/coordinates_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/list_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/location_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/media_properties_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/message_content_type.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/message_properties_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/message_queue_type.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/metadata_update_strategy_type.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/processing_strategy_type.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/recipient_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/send_message_request_body_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/share_location_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/template_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/text_message_dict.py create mode 100644 sinch/domains/conversation/models/v1/messages/types/url_message_dict.py diff --git a/sinch/domains/conversation/api/v1/internal/__init__.py b/sinch/domains/conversation/api/v1/internal/__init__.py index 4d862310..bc4a7083 100644 --- a/sinch/domains/conversation/api/v1/internal/__init__.py +++ b/sinch/domains/conversation/api/v1/internal/__init__.py @@ -2,10 +2,12 @@ DeleteMessageEndpoint, GetMessageEndpoint, UpdateMessageMetadataEndpoint, + SendMessageEndpoint, ) __all__ = [ "DeleteMessageEndpoint", "GetMessageEndpoint", "UpdateMessageMetadataEndpoint", + "SendMessageEndpoint", ] diff --git a/sinch/domains/conversation/api/v1/internal/messages_endpoints.py b/sinch/domains/conversation/api/v1/internal/messages_endpoints.py index 2027f955..d28bcd4c 100644 --- a/sinch/domains/conversation/api/v1/internal/messages_endpoints.py +++ b/sinch/domains/conversation/api/v1/internal/messages_endpoints.py @@ -4,9 +4,11 @@ from sinch.domains.conversation.models.v1.messages.internal.request import ( MessageIdRequest, UpdateMessageMetadataRequest, + SendMessageRequest, ) from sinch.domains.conversation.models.v1.messages.response.types import ( ConversationMessageResponse, + SendMessageResponse, ) from sinch.domains.conversation.api.v1.internal.base import ( ConversationEndpoint, @@ -124,3 +126,32 @@ def handle_response( return self.process_response_model( response.body, ConversationMessageResponse ) + + +class SendMessageEndpoint(ConversationEndpoint): + ENDPOINT_URL = "{origin}/v1/projects/{project_id}/messages:send" + HTTP_METHOD = HTTPMethods.POST.value + HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value + + def __init__(self, project_id: str, request_data: SendMessageRequest): + super(SendMessageEndpoint, self).__init__(project_id, request_data) + self.project_id = project_id + self.request_data = request_data + + def request_body(self): + path_params = self._get_path_params_from_url() + request_data_dict = self.request_data.model_dump( + mode="json", by_alias=True, exclude_none=True, exclude=path_params + ) + return json.dumps(request_data_dict) + + def handle_response(self, response: HTTPResponse) -> SendMessageResponse: + try: + super(SendMessageEndpoint, self).handle_response(response) + except ConversationException as e: + raise ConversationException( + message=e.args[0], + response=e.http_response, + is_from_server=e.is_from_server, + ) + return self.process_response_model(response.body, SendMessageResponse) diff --git a/sinch/domains/conversation/api/v1/messages_apis.py b/sinch/domains/conversation/api/v1/messages_apis.py index 41e3c3fc..65f69191 100644 --- a/sinch/domains/conversation/api/v1/messages_apis.py +++ b/sinch/domains/conversation/api/v1/messages_apis.py @@ -1,21 +1,73 @@ -from typing import Optional +from typing import Any, Dict, List, Optional, Union from sinch.domains.conversation.models.v1.messages.internal.request import ( MessageIdRequest, UpdateMessageMetadataRequest, + SendMessageRequest, + SendMessageRequestBody, ) from sinch.domains.conversation.models.v1.messages.response.types import ( ConversationMessageResponse, + SendMessageResponse, ) from sinch.domains.conversation.models.v1.messages.types import ( MessagesSourceType, + ConversationChannelType, + ProcessingStrategyType, + MetadataUpdateStrategyType, + MessageQueueType, + MessageContentType, + CardMessageDict, + CarouselMessageDict, + ChoiceMessageDict, + ContactInfoMessageDict, + ListMessageDict, + LocationMessageDict, + MediaPropertiesDict, + TemplateMessageDict, + RecipientDict, + ChannelRecipientIdentityDict, + SendMessageRequestBodyDict, +) +from sinch.domains.conversation.models.v1.messages.categories.text import ( + TextMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.card import ( + CardMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.carousel import ( + CarouselMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.choice import ( + ChoiceMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.contactinfo import ( + ContactInfoMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.list import ( + ListMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.location import ( + LocationMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.media import ( + MediaProperties, +) +from sinch.domains.conversation.models.v1.messages.categories.template import ( + TemplateMessage, ) from sinch.domains.conversation.api.v1.internal import ( DeleteMessageEndpoint, GetMessageEndpoint, UpdateMessageMetadataEndpoint, + SendMessageEndpoint, ) from sinch.domains.conversation.api.v1.base import BaseConversation +from sinch.domains.conversation.api.v1.utils import ( + build_recipient_dict, + coerce_recipient, + split_send_kwargs, +) class Messages(BaseConversation): @@ -114,3 +166,946 @@ def update( **kwargs, ) return self._request(UpdateMessageMetadataEndpoint, request_data) + + def _send_message_variant( + self, + app_id: str, + contact_id: Optional[str], + recipient_identities: Optional[List[ChannelRecipientIdentityDict]], + message_field: str, + message: object, + message_cls: type, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + - Builds Recipient Dictionary from contact_id or recipient_identities + - Normalizes recipient dict -> Recipient model + - Normalizes message dict -> message_cls(**message) + - Builds SendMessageRequest(message=..., recipient=..., app_id=...) and sends the request + """ + recipient_dict = build_recipient_dict( + contact_id=contact_id, recipient_identities=recipient_identities + ) + recipient_model = coerce_recipient(recipient_dict) + if isinstance(message, dict): + message = message_cls(**message) + + message_kwargs, request_kwargs = split_send_kwargs(kwargs) + send_message_request_body = SendMessageRequestBody( + **{message_field: message}, + **message_kwargs, + ) + request_data = SendMessageRequest( + app_id=app_id, + recipient=recipient_model, + message=send_message_request_body, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **request_kwargs, + ) + return self._request(SendMessageEndpoint, request_data) + + def send( + self, + app_id: str, + recipient: Union[RecipientDict, dict], + message: Union[SendMessageRequestBodyDict, dict], + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param recipient: The recipient of the message. Can be a Recipient object or a dict (identified_by/contact_id). + :type recipient: Union[RecipientDict, dict] + :param message: The message content to send. Can be a SendMessageRequestBodyDict or a dict. + :type message: Union[SendMessageRequestBodyDict, dict] + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the request. + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + recipient = coerce_recipient(recipient) + # Coerce message to SendMessageRequestBody if it's a dict + if isinstance(message, dict): + message = SendMessageRequestBody(**message) + message_kwargs, request_kwargs = split_send_kwargs(kwargs) + # message kwargs are applied directly to the message model (if provided as dict) + if message_kwargs: + message = SendMessageRequestBody( + **message.model_dump(), **message_kwargs + ) + request_data = SendMessageRequest( + app_id=app_id, + recipient=recipient, + message=message, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **request_kwargs, + ) + return self._request(SendMessageEndpoint, request_data) + + def send_text_message( + self, + app_id: str, + text: str, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a text message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param text: The text content of the message. + :type text: str + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="text_message", + message=TextMessage(text=text), + message_cls=TextMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_card_message( + self, + app_id: str, + card_message: CardMessageDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a card message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param card_message: The card message content. + :type card_message: CardMessageDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="card_message", + message=card_message, + message_cls=CardMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_carousel_message( + self, + app_id: str, + carousel_message: CarouselMessageDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a carousel message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param carousel_message: The carousel message content. + :type carousel_message: CarouselMessageDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="carousel_message", + message=carousel_message, + message_cls=CarouselMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_choice_message( + self, + app_id: str, + choice_message: ChoiceMessageDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a choice message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param choice_message: The choice message content. + :type choice_message: ChoiceMessageDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="choice_message", + message=choice_message, + message_cls=ChoiceMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_contact_info_message( + self, + app_id: str, + contact_info_message: ContactInfoMessageDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a contact info message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param contact_info_message: The contact info message content. + :type contact_info_message: ContactInfoMessageDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="contact_info_message", + message=contact_info_message, + message_cls=ContactInfoMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_list_message( + self, + app_id: str, + list_message: ListMessageDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a list message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param list_message: The list message content. + :type list_message: ListMessageDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="list_message", + message=list_message, + message_cls=ListMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_location_message( + self, + app_id: str, + location_message: LocationMessageDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a location message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param location_message: The location message content. + :type location_message: LocationMessageDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="location_message", + message=location_message, + message_cls=LocationMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_media_message( + self, + app_id: str, + media_message: MediaPropertiesDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a media message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param media_message: The media message content. + :type media_message: MediaPropertiesDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="media_message", + message=media_message, + message_cls=MediaProperties, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) + + def send_template_message( + self, + app_id: str, + template_message: TemplateMessageDict, + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, + ttl: Optional[Union[str, int]] = None, + callback_url: Optional[str] = None, + channel_priority_order: Optional[List[ConversationChannelType]] = None, + channel_properties: Optional[Dict[str, str]] = None, + message_metadata: Optional[str] = None, + conversation_metadata: Optional[Dict[str, Any]] = None, + queue: Optional[MessageQueueType] = None, + processing_strategy: Optional[ProcessingStrategyType] = None, + correlation_id: Optional[str] = None, + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = None, + message_content_type: Optional[MessageContentType] = None, + **kwargs, + ) -> SendMessageResponse: + """ + Send a template message from a Conversation app to a contact associated with that app. + If the recipient is not associated with an existing contact, a new contact will be created. + The message is added to the active conversation with the contact if a conversation already exists. + If no active conversation exists a new one is started automatically. + + :param app_id: The ID of the Conversation API app sending the message. + :type app_id: str + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + :param template_message: The template message content. + :type template_message: TemplateMessageDict + :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. + :type ttl: Optional[Union[str, int]] + :param callback_url: Overwrites the default callback url for delivery receipts for this message. + :type callback_url: Optional[str] + :param channel_priority_order: Explicitly define the channels and order in which they are tried when sending the message. + :type channel_priority_order: Optional[List[ConversationChannelType]] + :param channel_properties: Channel-specific properties. The key in the map must point to a valid channel property key. + :type channel_properties: Optional[Dict[str, str]] + :param message_metadata: Metadata that should be associated with the message. Up to 1024 characters long. + :type message_metadata: Optional[str] + :param conversation_metadata: Metadata that will be associated with the conversation. Up to 2048 characters long. + :type conversation_metadata: Optional[Dict[str, Any]] + :param queue: Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'. + :type queue: Optional[MessageQueueType] + :param processing_strategy: Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'. + :type processing_strategy: Optional[ProcessingStrategyType] + :param correlation_id: An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long. + :type correlation_id: Optional[str] + :param conversation_metadata_update_strategy: Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'. + :type conversation_metadata_update_strategy: Optional[MetadataUpdateStrategyType] + :param message_content_type: Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'. + :type message_content_type: Optional[MessageContentType] + :param **kwargs: Additional parameters for the message body (e.g., agent, etc.). + :type **kwargs: dict + + :returns: SendMessageResponse + :rtype: SendMessageResponse + + For detailed documentation, visit https://developers.sinch.com/docs/conversation/. + """ + return self._send_message_variant( + app_id=app_id, + contact_id=contact_id, + recipient_identities=recipient_identities, + message_field="template_message", + message=template_message, + message_cls=TemplateMessage, + ttl=ttl, + callback_url=callback_url, + channel_priority_order=channel_priority_order, + channel_properties=channel_properties, + message_metadata=message_metadata, + conversation_metadata=conversation_metadata, + queue=queue, + processing_strategy=processing_strategy, + correlation_id=correlation_id, + conversation_metadata_update_strategy=conversation_metadata_update_strategy, + message_content_type=message_content_type, + **kwargs, + ) diff --git a/sinch/domains/conversation/api/v1/utils/__init__.py b/sinch/domains/conversation/api/v1/utils/__init__.py new file mode 100644 index 00000000..ef5df4d6 --- /dev/null +++ b/sinch/domains/conversation/api/v1/utils/__init__.py @@ -0,0 +1,15 @@ +""" +Utility functions for Conversation API message operations. +""" + +from sinch.domains.conversation.api.v1.utils.message_helpers import ( + build_recipient_dict, + coerce_recipient, + split_send_kwargs, +) + +__all__ = [ + "build_recipient_dict", + "coerce_recipient", + "split_send_kwargs", +] diff --git a/sinch/domains/conversation/api/v1/utils/message_helpers.py b/sinch/domains/conversation/api/v1/utils/message_helpers.py new file mode 100644 index 00000000..f0f62601 --- /dev/null +++ b/sinch/domains/conversation/api/v1/utils/message_helpers.py @@ -0,0 +1,122 @@ +""" +Helper functions for building and processing message requests. + +This module contains pure utility functions that handle common operations +for message sending, such as recipient validation, type coercion, and +parameter splitting. +""" + +from typing import List, Optional, Union + +from sinch.domains.conversation.models.v1.messages.internal.request.recipient import ( + ChannelRecipientIdentity, + IdentifiedBy, + Recipient, +) +from sinch.domains.conversation.models.v1.messages.internal.request.send_message_request_body import ( + SendMessageRequestBody, +) +from sinch.domains.conversation.models.v1.messages.types import ( + ChannelRecipientIdentityDict, + RecipientDict, +) + + +def build_recipient_dict( + contact_id: Optional[str] = None, + recipient_identities: Optional[List[ChannelRecipientIdentityDict]] = None, +) -> RecipientDict: + """ + Build a RecipientDict from optional contact_id or recipient_identities. + + Validates that exactly one of the parameters is provided and returns + the appropriate dictionary structure. + + :param contact_id: The contact ID of the recipient. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] + + :returns: A RecipientDict with either contact_id or channel_identities. + :rtype: RecipientDict + + :raises ValueError: If both or neither parameters are provided. + """ + has_contact_id = contact_id is not None + has_identities = recipient_identities is not None + + if has_contact_id and has_identities: + raise ValueError( + "Cannot specify both 'contact_id' and 'recipient_identities'. " + "Provide exactly one." + ) + if not has_contact_id and not has_identities: + raise ValueError( + "Must provide either 'contact_id' or 'recipient_identities'." + ) + + return ( + {"contact_id": contact_id} + if has_contact_id + else {"channel_identities": recipient_identities} + ) + + +def coerce_recipient(recipient: Union[Recipient, dict]) -> Recipient: + """ + Coerce a recipient input to a Recipient model instance. + + Handles multiple input formats: + - Recipient model instance (returns as-is) + - Simplified dict: {"channel_identities": [...]} + - Simplified dict: {"contact_id": "..."} + - Full form dict: {"identified_by": {"channel_identities": [...]}} + + :param recipient: The recipient as a Recipient model or dict. + :type recipient: Union[Recipient, dict] + + :returns: A Recipient model instance. + :rtype: Recipient + """ + if isinstance(recipient, dict): + # Allow passing recipient dict in simplified form: + # - {"channel_identities": [...]} -> converts to {"identified_by": {"channel_identities": [...]}} + # - {"contact_id": "..."} + # - Or full form: {"identified_by": {"channel_identities": [...]}} + if ( + "channel_identities" in recipient + and "identified_by" not in recipient + ): + channel_identities = [ + ChannelRecipientIdentity(**ci) if isinstance(ci, dict) else ci + for ci in recipient["channel_identities"] + ] + return Recipient( + identified_by=IdentifiedBy( + channel_identities=channel_identities + ) + ) + return Recipient(**recipient) + return recipient + + +def split_send_kwargs(kwargs: dict) -> tuple[dict, dict]: + """ + Split kwargs into message-level and request-level parameters. + + Separates keyword arguments into two groups: + - message_kwargs: Fields that belong under the `message` field + - request_kwargs: Fields that belong on the SendMessageRequest itself + + :param kwargs: Dictionary of keyword arguments to split. + :type kwargs: dict + + :returns: A tuple of (message_kwargs, request_kwargs). + :rtype: tuple[dict, dict] + """ + message_fields = set(SendMessageRequestBody.model_fields.keys()) + message_kwargs = {k: v for k, v in kwargs.items() if k in message_fields} + request_kwargs = { + k: v for k, v in kwargs.items() if k not in message_fields + } + return message_kwargs, request_kwargs diff --git a/sinch/domains/conversation/models/v1/messages/categories/app/app_message.py b/sinch/domains/conversation/models/v1/messages/categories/app/app_message.py index 23ef0d9c..4e0cc5ed 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/app/app_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/app/app_message.py @@ -27,52 +27,44 @@ TextMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) from sinch.domains.conversation.models.v1.messages.shared.app_message_common_props import ( AppMessageCommonProps, ) -class CardAppMessage(AppMessageCommonProps, BaseModelConfigurationResponse): +class CardAppMessage(AppMessageCommonProps, BaseModelConfiguration): card_message: Optional[CardMessage] = None -class CarouselAppMessage( - AppMessageCommonProps, BaseModelConfigurationResponse -): +class CarouselAppMessage(AppMessageCommonProps, BaseModelConfiguration): carousel_message: Optional[CarouselMessage] = None -class ChoiceAppMessage(AppMessageCommonProps, BaseModelConfigurationResponse): +class ChoiceAppMessage(AppMessageCommonProps, BaseModelConfiguration): choice_message: Optional[ChoiceMessage] = None -class LocationAppMessage( - AppMessageCommonProps, BaseModelConfigurationResponse -): +class LocationAppMessage(AppMessageCommonProps, BaseModelConfiguration): location_message: Optional[LocationMessage] = None -class MediaAppMessage(AppMessageCommonProps, BaseModelConfigurationResponse): +class MediaAppMessage(AppMessageCommonProps, BaseModelConfiguration): media_message: Optional[MediaProperties] = None -class TemplateAppMessage( - AppMessageCommonProps, BaseModelConfigurationResponse -): +class TemplateAppMessage(AppMessageCommonProps, BaseModelConfiguration): template_message: Optional[TemplateMessage] = None -class TextAppMessage(AppMessageCommonProps, BaseModelConfigurationResponse): +class TextAppMessage(AppMessageCommonProps, BaseModelConfiguration): text_message: Optional[TextMessage] = None -class ListAppMessage(AppMessageCommonProps, BaseModelConfigurationResponse): +class ListAppMessage(AppMessageCommonProps, BaseModelConfiguration): list_message: Optional[ListMessage] = None -class ContactInfoAppMessage( - AppMessageCommonProps, BaseModelConfigurationResponse -): +class ContactInfoAppMessage(AppMessageCommonProps, BaseModelConfiguration): contact_info_message: Optional[ContactInfoMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/calendar/calendar_message.py b/sinch/domains/conversation/models/v1/messages/categories/calendar/calendar_message.py index 8d83bc54..36e119de 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/calendar/calendar_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/calendar/calendar_message.py @@ -2,11 +2,11 @@ from datetime import datetime from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class CalendarMessage(BaseModelConfigurationResponse): +class CalendarMessage(BaseModelConfiguration): title: StrictStr = Field( ..., description="The title is shown close to the button that leads to open a user calendar.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/call/call_message.py b/sinch/domains/conversation/models/v1/messages/categories/call/call_message.py index 79fbd1b0..0969866f 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/call/call_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/call/call_message.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class CallMessage(BaseModelConfigurationResponse): +class CallMessage(BaseModelConfiguration): phone_number: StrictStr = Field( default=..., description="Phone number in E.164 with leading +." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/card/card_message.py b/sinch/domains/conversation/models/v1/messages/categories/card/card_message.py index 3cf1e9ea..c8de5a25 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/card/card_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/card/card_message.py @@ -6,18 +6,18 @@ from sinch.domains.conversation.models.v1.messages.categories.media import ( MediaProperties, ) -from sinch.domains.conversation.models.v1.messages.response.types.choice_option import ( +from sinch.domains.conversation.models.v1.messages.categories.choice.choice_option import ( ChoiceOption, ) from sinch.domains.conversation.models.v1.messages.categories.card.message_properties import ( MessageProperties, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class CardMessage(BaseModelConfigurationResponse): +class CardMessage(BaseModelConfiguration): choices: Optional[conlist(ChoiceOption)] = Field( default=None, description="You may include choices in your Card Message. The number of choices is limited to 10.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/card/card_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/card/card_message_field.py index 77e35c77..0e80ad30 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/card/card_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/card/card_message_field.py @@ -3,9 +3,9 @@ CardMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class CardMessageField(BaseModelConfigurationResponse): +class CardMessageField(BaseModelConfiguration): card_message: Optional[CardMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/card/message_properties.py b/sinch/domains/conversation/models/v1/messages/categories/card/message_properties.py index ddff7028..78912593 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/card/message_properties.py +++ b/sinch/domains/conversation/models/v1/messages/categories/card/message_properties.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class MessageProperties(BaseModelConfigurationResponse): +class MessageProperties(BaseModelConfiguration): whatsapp_header: Optional[StrictStr] = Field( default=None, description=( diff --git a/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message.py b/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message.py index 8d4939a5..7026560d 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message.py @@ -3,15 +3,15 @@ from sinch.domains.conversation.models.v1.messages.categories.card.card_message import ( CardMessage, ) -from sinch.domains.conversation.models.v1.messages.response.types.choice_option import ( +from sinch.domains.conversation.models.v1.messages.categories.choice.choice_option import ( ChoiceOption, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class CarouselMessage(BaseModelConfigurationResponse): +class CarouselMessage(BaseModelConfiguration): cards: conlist(CardMessage) = Field( default=..., description="A list of up to 10 cards." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message_field.py index 41020788..4f671d1d 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/carousel/carousel_message_field.py @@ -3,9 +3,9 @@ CarouselMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class CarouselMessageField(BaseModelConfigurationResponse): +class CarouselMessageField(BaseModelConfiguration): carousel_message: Optional[CarouselMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_contact_message_message.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_contact_message_message.py index 8f44f48b..3131c8df 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_contact_message_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_contact_message_message.py @@ -4,11 +4,11 @@ WhatsAppInteractiveNfmReplyMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ChannelSpecificContactMessageMessage(BaseModelConfigurationResponse): +class ChannelSpecificContactMessageMessage(BaseModelConfiguration): message_type: Literal["nfm_reply"] = Field( ..., description="The message type." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_message.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_message.py index 138f2bf6..fd939112 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_message.py @@ -2,15 +2,15 @@ from sinch.domains.conversation.models.v1.messages.types.channel_specific_message_type import ( ChannelSpecificMessageType, ) -from sinch.domains.conversation.models.v1.messages.response.types.channel_specific_message_content import ( +from sinch.domains.conversation.models.v1.messages.categories.channelspecific.channel_specific_message_content import ( ChannelSpecificMessageContent, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ChannelSpecificMessage(BaseModelConfigurationResponse): +class ChannelSpecificMessage(BaseModelConfiguration): message_type: ChannelSpecificMessageType = Field( ..., description="The type of the channel specific message." ) diff --git a/sinch/domains/conversation/models/v1/messages/response/types/channel_specific_message_content.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_message_content.py similarity index 100% rename from sinch/domains/conversation/models/v1/messages/response/types/channel_specific_message_content.py rename to sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_message_content.py index 4bb50086..d7e5eac7 100644 --- a/sinch/domains/conversation/models/v1/messages/response/types/channel_specific_message_content.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/channel_specific_message_content.py @@ -1,4 +1,11 @@ from typing import Union + +from sinch.domains.conversation.models.v1.messages.categories.channelspecific.kakaotalk.commerce.kakaotalk_carousel_commerce_channel_specific_message import ( + KakaoTalkCarouselCommerceChannelSpecificMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.channelspecific.kakaotalk.commerce.kakaotalk_commerce_channel_specific_message import ( + KakaoTalkCommerceChannelSpecificMessage, +) from sinch.domains.conversation.models.v1.messages.categories.channelspecific.whatsapp.flows.flow_channel_specific_message import ( FlowChannelSpecificMessage, ) @@ -8,13 +15,6 @@ from sinch.domains.conversation.models.v1.messages.categories.channelspecific.whatsapp.payment.payment_order_status_channel_specific_message import ( PaymentOrderStatusChannelSpecificMessage, ) -from sinch.domains.conversation.models.v1.messages.categories.channelspecific.kakaotalk.commerce.kakaotalk_commerce_channel_specific_message import ( - KakaoTalkCommerceChannelSpecificMessage, -) -from sinch.domains.conversation.models.v1.messages.categories.channelspecific.kakaotalk.commerce.kakaotalk_carousel_commerce_channel_specific_message import ( - KakaoTalkCarouselCommerceChannelSpecificMessage, -) - ChannelSpecificMessageContent = Union[ FlowChannelSpecificMessage, diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/buttons/kakaotalk_button.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/buttons/kakaotalk_button.py index f52cf731..4e898ef4 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/buttons/kakaotalk_button.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/buttons/kakaotalk_button.py @@ -1,8 +1,8 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkButton(BaseModelConfigurationResponse): +class KakaoTalkButton(BaseModelConfiguration): name: StrictStr = Field(..., description="Text displayed on the button") diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel.py index 6fac4cc7..2b6fd81c 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel.py @@ -6,11 +6,11 @@ KakaoTalkCommerceMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkCarousel(BaseModelConfigurationResponse): +class KakaoTalkCarousel(BaseModelConfiguration): head: Optional[KakaoTalkCarouselHead] = Field( default=None, description="Carousel introduction" ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_head.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_head.py index 05ed6d5b..7870c02b 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_head.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_head.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkCarouselHead(BaseModelConfigurationResponse): +class KakaoTalkCarouselHead(BaseModelConfiguration): header: StrictStr = Field( ..., description="Carousel introduction title", max_length=20 ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_tail.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_tail.py index 956b3c0e..5a02ecda 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_tail.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_carousel_tail.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkCarouselTail(BaseModelConfigurationResponse): +class KakaoTalkCarouselTail(BaseModelConfiguration): link_mo: StrictStr = Field( ..., description="URL opened on a mobile device" ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_channel_specific_message.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_channel_specific_message.py index 15d3f7ef..674e7fdf 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_channel_specific_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_channel_specific_message.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictBool from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkChannelSpecificMessage(BaseModelConfigurationResponse): +class KakaoTalkChannelSpecificMessage(BaseModelConfiguration): push_alarm: Optional[StrictBool] = Field( default=True, description="Set to `true` if a push alarm should be sent to a device.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_image.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_image.py index a1c9a486..ca762987 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_image.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_image.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkCommerceImage(BaseModelConfigurationResponse): +class KakaoTalkCommerceImage(BaseModelConfiguration): image_url: StrictStr = Field(..., description="URL to the product image") image_link: Optional[StrictStr] = Field( default=None, description="URL opened when a user clicks on the image" diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_message.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_message.py index 5ae37bad..fe386706 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_commerce_message.py @@ -13,11 +13,11 @@ KakaoTalkCommerceImage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkCommerceMessage(BaseModelConfigurationResponse): +class KakaoTalkCommerceMessage(BaseModelConfiguration): buttons: conlist(KakaoTalkButton) = Field(..., description="Buttons list") additional_content: Optional[StrictStr] = Field( default=None, description="Additional information", max_length=34 diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_regular_price_commerce.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_regular_price_commerce.py index 46af8903..4fd71fe3 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_regular_price_commerce.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/commerce/kakaotalk_regular_price_commerce.py @@ -1,11 +1,11 @@ from typing import Literal from pydantic import Field, StrictStr, StrictInt from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkRegularPriceCommerce(BaseModelConfigurationResponse): +class KakaoTalkRegularPriceCommerce(BaseModelConfiguration): type: Literal["REGULAR_PRICE_COMMERCE"] = Field( "REGULAR_PRICE_COMMERCE", description="Commerce with regular price" ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/coupons/kakaotalk_coupon.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/coupons/kakaotalk_coupon.py index 3b9dc38d..e9db07ae 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/coupons/kakaotalk_coupon.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/kakaotalk/coupons/kakaotalk_coupon.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class KakaoTalkCoupon(BaseModelConfigurationResponse): +class KakaoTalkCoupon(BaseModelConfiguration): description: Optional[StrictStr] = Field( default=None, description="Coupon description" ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/flow_action_payload.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/flow_action_payload.py index e3743c8c..0c4c624c 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/flow_action_payload.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/flow_action_payload.py @@ -1,11 +1,11 @@ from typing import Any, Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class FlowActionPayload(BaseModelConfigurationResponse): +class FlowActionPayload(BaseModelConfiguration): screen: Optional[StrictStr] = Field( default=None, description="The ID of the screen displayed first. This must be an entry screen.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_body.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_body.py index 4c9f0cc8..50ab1ff9 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_body.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_body.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveBody(BaseModelConfigurationResponse): +class WhatsAppInteractiveBody(BaseModelConfiguration): text: StrictStr = Field( ..., description="The content of the message (1024 characters maximum). Emojis and Markdown are supported.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_document_header.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_document_header.py index 059c22eb..11d8be41 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_document_header.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_document_header.py @@ -4,11 +4,11 @@ WhatsAppInteractiveHeaderMedia, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveDocumentHeader(BaseModelConfigurationResponse): +class WhatsAppInteractiveDocumentHeader(BaseModelConfiguration): type: Literal["document"] = Field( ..., description="The document associated with the header." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_footer.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_footer.py index 449c66dd..0c7f570a 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_footer.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_footer.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveFooter(BaseModelConfigurationResponse): +class WhatsAppInteractiveFooter(BaseModelConfiguration): text: StrictStr = Field( ..., description="The footer content (60 characters maximum). Emojis, Markdown and links are supported.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_header_media.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_header_media.py index 7ab870eb..a16d83b8 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_header_media.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_header_media.py @@ -1,8 +1,8 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveHeaderMedia(BaseModelConfigurationResponse): +class WhatsAppInteractiveHeaderMedia(BaseModelConfiguration): link: StrictStr = Field(..., description="URL for the media.") diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_image_header.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_image_header.py index f1887210..2c9c45d1 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_image_header.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_image_header.py @@ -4,11 +4,11 @@ WhatsAppInteractiveHeaderMedia, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveImageHeader(BaseModelConfigurationResponse): +class WhatsAppInteractiveImageHeader(BaseModelConfiguration): type: Literal["image"] = Field( ..., description="The image associated with the header." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_text_header.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_text_header.py index 3aa24c5c..994dcc71 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_text_header.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_text_header.py @@ -1,11 +1,11 @@ from typing import Literal from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveTextHeader(BaseModelConfigurationResponse): +class WhatsAppInteractiveTextHeader(BaseModelConfiguration): type: Literal["text"] = Field(..., description="The text of the header.") text: StrictStr = Field( ..., diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_video_header.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_video_header.py index d5d76785..de16a9c1 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_video_header.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/flows/whatsapp_interactive_video_header.py @@ -4,11 +4,11 @@ WhatsAppInteractiveHeaderMedia, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveVideoHeader(BaseModelConfigurationResponse): +class WhatsAppInteractiveVideoHeader(BaseModelConfiguration): type: Literal["video"] = Field( ..., description="The video associated with the header." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply.py index 4321ce60..225115d7 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply.py @@ -3,11 +3,11 @@ WhatsAppInteractiveNfmReplyNameType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveNfmReply(BaseModelConfigurationResponse): +class WhatsAppInteractiveNfmReply(BaseModelConfiguration): name: WhatsAppInteractiveNfmReplyNameType = Field( ..., description="The nfm reply message type." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply_message.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply_message.py index 4068cd89..9f6b72c1 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/nfmreply/whatsapp_interactive_nfm_reply_message.py @@ -4,11 +4,11 @@ WhatsAppInteractiveNfmReply, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppInteractiveNfmReplyMessage(BaseModelConfigurationResponse): +class WhatsAppInteractiveNfmReplyMessage(BaseModelConfiguration): type: Literal["nfm_reply"] = Field( description="The interactive message type." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/boleto.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/boleto.py index f6353c2c..ef46a9a8 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/boleto.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/boleto.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class Boleto(BaseModelConfigurationResponse): +class Boleto(BaseModelConfiguration): digitable_line: StrictStr = Field( ..., description="The Boleto digitable line which will be copied to the clipboard when the user taps the Boleto button.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/dynamic_pix.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/dynamic_pix.py index 1e09f0c2..8e43f16d 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/dynamic_pix.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/dynamic_pix.py @@ -3,11 +3,11 @@ PixKeyType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class DynamicPix(BaseModelConfigurationResponse): +class DynamicPix(BaseModelConfiguration): code: StrictStr = Field( ..., description="The dynamic Pix code to be used by the buyer to pay." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/order_item.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/order_item.py index 59b1afe9..a3d9a732 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/order_item.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/order_item.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr, StrictInt from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class OrderItem(BaseModelConfigurationResponse): +class OrderItem(BaseModelConfiguration): retailer_id: StrictStr = Field( ..., description="Unique ID of the retailer." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_link.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_link.py index a93d5484..c621eb66 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_link.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_link.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class PaymentLink(BaseModelConfigurationResponse): +class PaymentLink(BaseModelConfiguration): uri: StrictStr = Field( ..., description="The payment link to be used by the buyer to pay." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order.py index 30bbcb30..96401378 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order.py @@ -5,11 +5,11 @@ OrderItem, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class PaymentOrder(BaseModelConfigurationResponse): +class PaymentOrder(BaseModelConfiguration): items: conlist(OrderItem) = Field( ..., description="The items list for this order." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_details_content.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_details_content.py index 3cbdfac6..67beb888 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_details_content.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_details_content.py @@ -13,11 +13,11 @@ PaymentOrder, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class PaymentOrderDetailsContent(BaseModelConfigurationResponse): +class PaymentOrderDetailsContent(BaseModelConfiguration): type: PaymentOrderType = Field( ..., description="The country/currency associated with the payment message.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_content.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_content.py index 8ef61f11..544a62a2 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_content.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_content.py @@ -3,11 +3,11 @@ PaymentOrderStatusOrder, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class PaymentOrderStatusContent(BaseModelConfigurationResponse): +class PaymentOrderStatusContent(BaseModelConfiguration): reference_id: StrictStr = Field( ..., description="Unique ID used to query the current payment status." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_order.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_order.py index 14d384ee..ee91a90a 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_order.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/payment/payment_order_status_order.py @@ -4,11 +4,11 @@ PaymentOrderStatusType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class PaymentOrderStatusOrder(BaseModelConfigurationResponse): +class PaymentOrderStatusOrder(BaseModelConfiguration): status: PaymentOrderStatusType = Field( ..., description="The new payment message status." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/whatsapp_common_props.py b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/whatsapp_common_props.py index 1d340106..6433db6b 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/whatsapp_common_props.py +++ b/sinch/domains/conversation/models/v1/messages/categories/channelspecific/whatsapp/whatsapp_common_props.py @@ -8,11 +8,11 @@ WhatsAppInteractiveFooter, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class WhatsAppCommonProps(BaseModelConfigurationResponse): +class WhatsAppCommonProps(BaseModelConfiguration): header: Optional[WhatsAppInteractiveHeader] = Field( default=None, description="The header of the interactive message." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py index e45f644f..511d451e 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py @@ -1,10 +1,10 @@ from typing import Optional from pydantic import Field, conlist -from sinch.domains.conversation.models.v1.messages.response.types.choice_option import ( +from sinch.domains.conversation.models.v1.messages.categories.choice.choice_option import ( ChoiceOption, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) from sinch.domains.conversation.models.v1.messages.categories.text import ( TextMessage, @@ -14,7 +14,7 @@ ) -class ChoiceMessage(BaseModelConfigurationResponse): +class ChoiceMessage(BaseModelConfiguration): choices: conlist(ChoiceOption) = Field( default=..., description="The number of choices is limited to 10." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_field.py index 5ba83892..0ed3fc0c 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_field.py @@ -3,9 +3,9 @@ ChoiceMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ChoiceMessageField(BaseModelConfigurationResponse): +class ChoiceMessageField(BaseModelConfiguration): choice_message: Optional[ChoiceMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/response/types/choice_option.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py similarity index 100% rename from sinch/domains/conversation/models/v1/messages/response/types/choice_option.py rename to sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py index d2ddf127..cb4e385b 100644 --- a/sinch/domains/conversation/models/v1/messages/response/types/choice_option.py +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py @@ -1,14 +1,14 @@ from typing import Union + from sinch.domains.conversation.models.v1.messages.categories.choice.choice_options import ( + CalendarChoiceMessage, CallChoiceMessage, LocationChoiceMessage, + ShareLocationChoiceMessage, TextChoiceMessage, UrlChoiceMessage, - CalendarChoiceMessage, - ShareLocationChoiceMessage, ) - ChoiceOption = Union[ CallChoiceMessage, LocationChoiceMessage, diff --git a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_options.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_options.py index f9ef0547..a8c7f0b1 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_options.py +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_options.py @@ -16,14 +16,14 @@ ShareLocationMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) from sinch.domains.conversation.models.v1.messages.categories.text import ( TextMessage, ) -class ChoiceMessageWithPostback(BaseModelConfigurationResponse): +class ChoiceMessageWithPostback(BaseModelConfiguration): postback_data: Optional[Any] = Field( default=None, description="An optional field. This data will be returned in the ChoiceResponseMessage. The default is message_id_{text, title}.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/choiceresponse/choice_response_message.py b/sinch/domains/conversation/models/v1/messages/categories/choiceresponse/choice_response_message.py index 094b949c..4b447a63 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/choiceresponse/choice_response_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/choiceresponse/choice_response_message.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ChoiceResponseMessage(BaseModelConfigurationResponse): +class ChoiceResponseMessage(BaseModelConfiguration): message_id: StrictStr = Field( ..., description="The message id containing the choice." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/common/reply_to.py b/sinch/domains/conversation/models/v1/messages/categories/common/reply_to.py index b81e0994..f3a6c582 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/common/reply_to.py +++ b/sinch/domains/conversation/models/v1/messages/categories/common/reply_to.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ReplyTo(BaseModelConfigurationResponse): +class ReplyTo(BaseModelConfiguration): message_id: StrictStr = Field( default=..., description="Required. The Id of the message that this is a response to", diff --git a/sinch/domains/conversation/models/v1/messages/categories/contact/contact_message.py b/sinch/domains/conversation/models/v1/messages/categories/contact/contact_message.py index f24ad512..f427fe2f 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/contact/contact_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/contact/contact_message.py @@ -28,12 +28,12 @@ ContactMessageCommonProps, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) class ChannelSpecificContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse + ContactMessageCommonProps, BaseModelConfiguration ): channel_specific_message: ChannelSpecificContactMessageMessage = Field( ..., @@ -42,42 +42,38 @@ class ChannelSpecificContactMessage( class ChoiceResponseContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse + ContactMessageCommonProps, BaseModelConfiguration ): choice_response_message: Optional[ChoiceResponseMessage] = None class FallbackContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse + ContactMessageCommonProps, BaseModelConfiguration ): fallback_message: Optional[FallbackMessage] = None class LocationContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse + ContactMessageCommonProps, BaseModelConfiguration ): location_message: Optional[LocationMessage] = None class MediaCardContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse + ContactMessageCommonProps, BaseModelConfiguration ): media_card_message: Optional[MediaCardMessage] = None -class MediaContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse -): +class MediaContactMessage(ContactMessageCommonProps, BaseModelConfiguration): media_message: Optional[MediaProperties] = None class ProductResponseContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse + ContactMessageCommonProps, BaseModelConfiguration ): product_response_message: Optional[ProductResponseMessage] = None -class TextContactMessage( - ContactMessageCommonProps, BaseModelConfigurationResponse -): +class TextContactMessage(ContactMessageCommonProps, BaseModelConfiguration): text_message: Optional[TextMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message.py b/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message.py index e2483c65..66bf447c 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message.py @@ -2,7 +2,7 @@ from datetime import date from pydantic import Field, conlist from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) from sinch.domains.conversation.models.v1.messages.shared.name_info import ( NameInfo, @@ -24,7 +24,7 @@ ) -class ContactInfoMessage(BaseModelConfigurationResponse): +class ContactInfoMessage(BaseModelConfiguration): name: NameInfo = Field(..., description="Name information of the contact.") phone_numbers: conlist(PhoneNumberInfo) = Field( description="Phone numbers of the contact (at least one required).", diff --git a/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message_field.py index efe3ef4c..9c25f070 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/contactinfo/contact_info_message_field.py @@ -1,11 +1,11 @@ from typing import Optional from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) from sinch.domains.conversation.models.v1.messages.categories.contactinfo.contact_info_message import ( ContactInfoMessage, ) -class ContactInfoMessageField(BaseModelConfigurationResponse): +class ContactInfoMessageField(BaseModelConfiguration): contact_info_message: Optional[ContactInfoMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/fallback/fallback_message.py b/sinch/domains/conversation/models/v1/messages/categories/fallback/fallback_message.py index 83ca3d71..ab556e9c 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/fallback/fallback_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/fallback/fallback_message.py @@ -2,11 +2,11 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.shared.reason import Reason from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class FallbackMessage(BaseModelConfigurationResponse): +class FallbackMessage(BaseModelConfiguration): raw_message: Optional[StrictStr] = Field( default=None, description="Optional. The raw fallback message if provided by the channel.", diff --git a/sinch/domains/conversation/models/v1/messages/response/types/list_item.py b/sinch/domains/conversation/models/v1/messages/categories/list/list_item.py similarity index 100% rename from sinch/domains/conversation/models/v1/messages/response/types/list_item.py rename to sinch/domains/conversation/models/v1/messages/categories/list/list_item.py index ebc54b0b..51455a49 100644 --- a/sinch/domains/conversation/models/v1/messages/response/types/list_item.py +++ b/sinch/domains/conversation/models/v1/messages/categories/list/list_item.py @@ -1,4 +1,5 @@ from typing import Union + from sinch.domains.conversation.models.v1.messages.categories.list.list_item_choice import ( ListItemChoice, ) @@ -6,5 +7,4 @@ ListItemProduct, ) - ListItem = Union[ListItemChoice, ListItemProduct] diff --git a/sinch/domains/conversation/models/v1/messages/categories/list/list_item_choice.py b/sinch/domains/conversation/models/v1/messages/categories/list/list_item_choice.py index 67ebbb2f..33d99d28 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/list/list_item_choice.py +++ b/sinch/domains/conversation/models/v1/messages/categories/list/list_item_choice.py @@ -3,9 +3,9 @@ ChoiceItem, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ListItemChoice(BaseModelConfigurationResponse): +class ListItemChoice(BaseModelConfiguration): choice: ChoiceItem = Field(...) diff --git a/sinch/domains/conversation/models/v1/messages/categories/list/list_item_product.py b/sinch/domains/conversation/models/v1/messages/categories/list/list_item_product.py index 110ecb31..4322937f 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/list/list_item_product.py +++ b/sinch/domains/conversation/models/v1/messages/categories/list/list_item_product.py @@ -3,9 +3,9 @@ ProductItem, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ListItemProduct(BaseModelConfigurationResponse): +class ListItemProduct(BaseModelConfiguration): product: ProductItem = Field(...) diff --git a/sinch/domains/conversation/models/v1/messages/categories/list/list_message.py b/sinch/domains/conversation/models/v1/messages/categories/list/list_message.py index 826a8433..eff78dd2 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/list/list_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/list/list_message.py @@ -10,11 +10,11 @@ ListMessageProperties, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ListMessage(BaseModelConfigurationResponse): +class ListMessage(BaseModelConfiguration): title: StrictStr = Field( default=..., description="A title for the message that is displayed near the products or choices.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/list/list_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/list/list_message_field.py index 1fa02b02..27d0ee84 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/list/list_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/list/list_message_field.py @@ -3,9 +3,9 @@ ListMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ListMessageField(BaseModelConfigurationResponse): +class ListMessageField(BaseModelConfiguration): list_message: Optional[ListMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/list/list_message_properties.py b/sinch/domains/conversation/models/v1/messages/categories/list/list_message_properties.py index b3f780dc..4066c000 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/list/list_message_properties.py +++ b/sinch/domains/conversation/models/v1/messages/categories/list/list_message_properties.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ListMessageProperties(BaseModelConfigurationResponse): +class ListMessageProperties(BaseModelConfiguration): catalog_id: Optional[StrictStr] = Field( default=None, description="Required if sending a product list message. The ID of the catalog to which the products belong.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/location/location_message.py b/sinch/domains/conversation/models/v1/messages/categories/location/location_message.py index c7f1319c..f8b71a3b 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/location/location_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/location/location_message.py @@ -4,11 +4,11 @@ Coordinates, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class LocationMessage(BaseModelConfigurationResponse): +class LocationMessage(BaseModelConfiguration): coordinates: Coordinates = Field(...) label: Optional[StrictStr] = Field( default=None, description="Label or name for the position." diff --git a/sinch/domains/conversation/models/v1/messages/categories/location/location_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/location/location_message_field.py index 7f3def35..3e18afaa 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/location/location_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/location/location_message_field.py @@ -3,9 +3,9 @@ LocationMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class LocationMessageField(BaseModelConfigurationResponse): +class LocationMessageField(BaseModelConfiguration): location_message: Optional[LocationMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/media/media_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/media/media_message_field.py index 4453cde9..fb4653b7 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/media/media_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/media/media_message_field.py @@ -3,9 +3,9 @@ MediaProperties, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class MediaMessageField(BaseModelConfigurationResponse): +class MediaMessageField(BaseModelConfiguration): media_message: Optional[MediaProperties] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/media/media_properties.py b/sinch/domains/conversation/models/v1/messages/categories/media/media_properties.py index d15041d5..298ca6aa 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/media/media_properties.py +++ b/sinch/domains/conversation/models/v1/messages/categories/media/media_properties.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class MediaProperties(BaseModelConfigurationResponse): +class MediaProperties(BaseModelConfiguration): thumbnail_url: Optional[StrictStr] = Field( default=None, description="An optional parameter. Will be used where it is natively supported.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/mediacard/media_card_message.py b/sinch/domains/conversation/models/v1/messages/categories/mediacard/media_card_message.py index 1374064e..411b2ec6 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/mediacard/media_card_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/mediacard/media_card_message.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class MediaCardMessage(BaseModelConfigurationResponse): +class MediaCardMessage(BaseModelConfiguration): caption: Optional[StrictStr] = Field( default=None, description="Caption for the media on supported channels.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/productresponse/product_response_message.py b/sinch/domains/conversation/models/v1/messages/categories/productresponse/product_response_message.py index 8d8a3ebd..c93ec77a 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/productresponse/product_response_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/productresponse/product_response_message.py @@ -4,11 +4,11 @@ ProductItem, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ProductResponseMessage(BaseModelConfigurationResponse): +class ProductResponseMessage(BaseModelConfiguration): products: Optional[conlist(ProductItem)] = Field( default=None, description="The selected products." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/sharelocation/share_location_message.py b/sinch/domains/conversation/models/v1/messages/categories/sharelocation/share_location_message.py index af9b8f91..58a366f5 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/sharelocation/share_location_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/sharelocation/share_location_message.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ShareLocationMessage(BaseModelConfigurationResponse): +class ShareLocationMessage(BaseModelConfiguration): title: StrictStr = Field( ..., description="The title is shown close to the button that leads to open a map to share a location.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/template/template_message.py b/sinch/domains/conversation/models/v1/messages/categories/template/template_message.py index 40eeb8fe..fe003f71 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/template/template_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/template/template_message.py @@ -5,11 +5,11 @@ TemplateReferenceOmniChannel, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class TemplateMessage(BaseModelConfigurationResponse): +class TemplateMessage(BaseModelConfiguration): channel_template: Optional[Dict[str, TemplateReferenceChannelSpecific]] = ( Field( default=None, diff --git a/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_channel_specific.py b/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_channel_specific.py index 93e56cf4..404f39e0 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_channel_specific.py +++ b/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_channel_specific.py @@ -1,11 +1,11 @@ from typing import Dict, Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class TemplateReferenceChannelSpecific(BaseModelConfigurationResponse): +class TemplateReferenceChannelSpecific(BaseModelConfiguration): version: Optional[StrictStr] = Field( default=None, description="Used to specify what version of a template to use. Required when using `omni_channel_override` and `omni_template` fields. This will be used in conjunction with `language_code`. Note that, when referencing omni-channel templates using the [Sinch Customer Dashboard](https://dashboard.sinch.com/), the latest version of a given omni-template can be identified by populating this field with `latest`.", diff --git a/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_field.py b/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_field.py index db375b3d..35fb765c 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/template/template_reference_field.py @@ -3,9 +3,9 @@ TemplateReferenceOmniChannel, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class TemplateReferenceField(BaseModelConfigurationResponse): +class TemplateReferenceField(BaseModelConfiguration): template_reference: Optional[TemplateReferenceOmniChannel] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/text/text_message.py b/sinch/domains/conversation/models/v1/messages/categories/text/text_message.py index cbcc5adb..fb8a266b 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/text/text_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/text/text_message.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class TextMessage(BaseModelConfigurationResponse): +class TextMessage(BaseModelConfiguration): text: StrictStr = Field( ..., description="The text content of the message." ) diff --git a/sinch/domains/conversation/models/v1/messages/categories/text/text_message_field.py b/sinch/domains/conversation/models/v1/messages/categories/text/text_message_field.py index 245e4bfc..f85f620c 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/text/text_message_field.py +++ b/sinch/domains/conversation/models/v1/messages/categories/text/text_message_field.py @@ -1,11 +1,11 @@ from typing import Optional from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) from sinch.domains.conversation.models.v1.messages.categories.text import ( TextMessage, ) -class TextMessageField(BaseModelConfigurationResponse): +class TextMessageField(BaseModelConfiguration): text_message: Optional[TextMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/url/url_message.py b/sinch/domains/conversation/models/v1/messages/categories/url/url_message.py index a861288b..a6ca73d0 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/url/url_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/url/url_message.py @@ -1,10 +1,10 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class UrlMessage(BaseModelConfigurationResponse): +class UrlMessage(BaseModelConfiguration): title: StrictStr = Field( default=..., description="The title shown close to the URL. The title can be clickable in some cases.", diff --git a/sinch/domains/conversation/models/v1/messages/internal/base/__init__.py b/sinch/domains/conversation/models/v1/messages/internal/base/__init__.py index c9983491..c6908a4e 100644 --- a/sinch/domains/conversation/models/v1/messages/internal/base/__init__.py +++ b/sinch/domains/conversation/models/v1/messages/internal/base/__init__.py @@ -1,9 +1,7 @@ from sinch.domains.conversation.models.v1.messages.internal.base.base_model_configuration import ( - BaseModelConfigurationRequest, - BaseModelConfigurationResponse, + BaseModelConfiguration, ) __all__ = [ - "BaseModelConfigurationRequest", - "BaseModelConfigurationResponse", + "BaseModelConfiguration", ] diff --git a/sinch/domains/conversation/models/v1/messages/internal/base/base_model_configuration.py b/sinch/domains/conversation/models/v1/messages/internal/base/base_model_configuration.py index 204ea49d..200cf35e 100644 --- a/sinch/domains/conversation/models/v1/messages/internal/base/base_model_configuration.py +++ b/sinch/domains/conversation/models/v1/messages/internal/base/base_model_configuration.py @@ -3,9 +3,10 @@ from pydantic import BaseModel, ConfigDict -class BaseModelConfigurationRequest(BaseModel): +class BaseModelConfiguration(BaseModel): """ - A base model that allows extra fields and converts snake_case to camelCase. + Base model for all conversation message models. + Both request and response use snake_case in the Conversation API. """ model_config = ConfigDict( @@ -15,24 +16,11 @@ class BaseModelConfigurationRequest(BaseModel): extra="allow", ) - -class BaseModelConfigurationResponse(BaseModel): - """ - A base model that allows extra fields and converts camelCase to snake_case - """ - @staticmethod def _to_snake_case(camel_str: str) -> str: """Helper to convert camelCase string to snake_case.""" return re.sub(r"(? None: """Converts unknown fields from camelCase to snake_case.""" if self.__pydantic_extra__: diff --git a/sinch/domains/conversation/models/v1/messages/internal/request/__init__.py b/sinch/domains/conversation/models/v1/messages/internal/request/__init__.py index 4fc65a1f..da524ea8 100644 --- a/sinch/domains/conversation/models/v1/messages/internal/request/__init__.py +++ b/sinch/domains/conversation/models/v1/messages/internal/request/__init__.py @@ -4,8 +4,24 @@ from sinch.domains.conversation.models.v1.messages.internal.request.update_message_metadata_request import ( UpdateMessageMetadataRequest, ) +from sinch.domains.conversation.models.v1.messages.internal.request.recipient import ( + Recipient, + IdentifiedBy, + ChannelRecipientIdentity, +) +from sinch.domains.conversation.models.v1.messages.internal.request.send_message_request_body import ( + SendMessageRequestBody, +) +from sinch.domains.conversation.models.v1.messages.internal.request.send_message_request import ( + SendMessageRequest, +) __all__ = [ "MessageIdRequest", "UpdateMessageMetadataRequest", + "Recipient", + "IdentifiedBy", + "ChannelRecipientIdentity", + "SendMessageRequestBody", + "SendMessageRequest", ] diff --git a/sinch/domains/conversation/models/v1/messages/internal/request/message_id_request.py b/sinch/domains/conversation/models/v1/messages/internal/request/message_id_request.py index 86b4a1be..7823c8c1 100644 --- a/sinch/domains/conversation/models/v1/messages/internal/request/message_id_request.py +++ b/sinch/domains/conversation/models/v1/messages/internal/request/message_id_request.py @@ -4,11 +4,11 @@ MessagesSourceType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationRequest, + BaseModelConfiguration, ) -class MessageIdRequest(BaseModelConfigurationRequest): +class MessageIdRequest(BaseModelConfiguration): message_id: str = Field(..., description="The unique ID of the message.") messages_source: Optional[MessagesSourceType] = Field( default=None, diff --git a/sinch/domains/conversation/models/v1/messages/internal/request/recipient.py b/sinch/domains/conversation/models/v1/messages/internal/request/recipient.py new file mode 100644 index 00000000..3b42eadc --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/internal/request/recipient.py @@ -0,0 +1,38 @@ +from typing import List, Optional +from pydantic import Field, StrictStr +from sinch.domains.conversation.models.v1.messages.internal.base import ( + BaseModelConfiguration, +) +from sinch.domains.conversation.models.v1.messages.types.conversation_channel_type import ( + ConversationChannelType, +) + + +class ChannelRecipientIdentity(BaseModelConfiguration): + channel: ConversationChannelType = Field( + ..., description="The conversation channel." + ) + identity: StrictStr = Field( + ..., description="The channel recipient identity." + ) + + +class IdentifiedBy(BaseModelConfiguration): + channel_identities: List[ChannelRecipientIdentity] = Field( + ..., + description=( + "A list of specific channel identities. " + "The API will use these identities when sending to specific channels." + ), + ) + + +class Recipient(BaseModelConfiguration): + identified_by: Optional[IdentifiedBy] = Field( + default=None, + description="The identity as specified by the channel. Required if using Dispatch Mode.", + ) + contact_id: Optional[StrictStr] = Field( + default=None, + description="The ID of the contact.", + ) diff --git a/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py b/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py new file mode 100644 index 00000000..be171245 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py @@ -0,0 +1,102 @@ +from typing import Any, Dict, List, Optional, Union + +from pydantic import Field, StrictStr, field_serializer +from sinch.domains.conversation.models.v1.messages.internal.request.recipient import ( + Recipient, +) +from sinch.domains.conversation.models.v1.messages.internal.request.send_message_request_body import ( + SendMessageRequestBody, +) +from sinch.domains.conversation.models.v1.messages.internal.base import ( + BaseModelConfiguration, +) +from sinch.domains.conversation.models.v1.messages.types.conversation_channel_type import ( + ConversationChannelType, +) +from sinch.domains.conversation.models.v1.messages.types.processing_strategy_type import ( + ProcessingStrategyType, +) +from sinch.domains.conversation.models.v1.messages.types.metadata_update_strategy_type import ( + MetadataUpdateStrategyType, +) +from sinch.domains.conversation.models.v1.messages.types.message_queue_type import ( + MessageQueueType, +) +from sinch.domains.conversation.models.v1.messages.types.message_content_type import ( + MessageContentType, +) + + +class SendMessageRequest(BaseModelConfiguration): + app_id: StrictStr = Field( + ..., + description="The ID of the Conversation API app sending the message.", + ) + recipient: Recipient = Field( + ..., + description="The recipient of the message.", + ) + message: SendMessageRequestBody = Field( + ..., + description="The message content to send.", + ) + ttl: Optional[Union[StrictStr, int]] = Field( + default=None, + description="The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'.", + ) + callback_url: Optional[StrictStr] = Field( + default=None, + description="Overwrites the default callback url for delivery receipts for this message.", + ) + channel_priority_order: Optional[List[ConversationChannelType]] = Field( + default=None, + description="Explicitly define the channels and order in which they are tried when sending the message.", + ) + channel_properties: Optional[Dict[str, str]] = Field( + default=None, + description="Channel-specific properties. The key in the map must point to a valid channel property key.", + ) + message_metadata: Optional[StrictStr] = Field( + default=None, + description="Metadata that should be associated with the message. Up to 1024 characters long.", + ) + conversation_metadata: Optional[Dict[str, Any]] = Field( + default=None, + description="Metadata that will be associated with the conversation. Up to 2048 characters long.", + ) + queue: Optional[MessageQueueType] = Field( + default=None, + description="Select the priority type for the message. Can be 'NORMAL_PRIORITY' or 'HIGH_PRIORITY'.", + ) + processing_strategy: Optional[ProcessingStrategyType] = Field( + default=None, + description="Overrides the app's Processing Mode. Can be 'DEFAULT' or 'DISPATCH_ONLY'.", + ) + correlation_id: Optional[StrictStr] = Field( + default=None, + description="An arbitrary identifier that will be propagated to callbacks related to this message. Up to 128 characters long.", + ) + conversation_metadata_update_strategy: Optional[ + MetadataUpdateStrategyType + ] = Field( + default=None, + description="Update strategy for the conversation_metadata field. Can be 'REPLACE' or 'MERGE_PATCH'.", + ) + message_content_type: Optional[MessageContentType] = Field( + default=None, + description="Classifies the message content for use with consent management. Can be 'CONTENT_UNKNOWN', 'CONTENT_MARKETING', or 'CONTENT_NOTIFICATION'.", + ) + + @field_serializer("ttl") + def serialize_ttl(self, value: Optional[Union[str, int]]) -> Optional[str]: + """ + Serialize ttl field to the format expected by the API (string with 's' suffix). + Converts int to string with 's' suffix, or ensures string has 's' suffix. + """ + if value is None: + return None + if isinstance(value, (int, float)): + return f"{int(value)}s" + if isinstance(value, str) and not value.endswith("s"): + return f"{value}s" + return value diff --git a/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request_body.py b/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request_body.py new file mode 100644 index 00000000..b5bb2698 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request_body.py @@ -0,0 +1,43 @@ +from typing import Optional +from sinch.domains.conversation.models.v1.messages.categories.text import ( + TextMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.card.card_message import ( + CardMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.carousel.carousel_message import ( + CarouselMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.choice.choice_message import ( + ChoiceMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.contactinfo.contact_info_message import ( + ContactInfoMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.list.list_message import ( + ListMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.location.location_message import ( + LocationMessage, +) +from sinch.domains.conversation.models.v1.messages.categories.media.media_properties import ( + MediaProperties, +) +from sinch.domains.conversation.models.v1.messages.categories.template.template_message import ( + TemplateMessage, +) +from sinch.domains.conversation.models.v1.messages.internal.base import ( + BaseModelConfiguration, +) + + +class SendMessageRequestBody(BaseModelConfiguration): + text_message: Optional[TextMessage] = None + card_message: Optional[CardMessage] = None + carousel_message: Optional[CarouselMessage] = None + choice_message: Optional[ChoiceMessage] = None + contact_info_message: Optional[ContactInfoMessage] = None + list_message: Optional[ListMessage] = None + location_message: Optional[LocationMessage] = None + media_message: Optional[MediaProperties] = None + template_message: Optional[TemplateMessage] = None diff --git a/sinch/domains/conversation/models/v1/messages/internal/request/update_message_metadata_request.py b/sinch/domains/conversation/models/v1/messages/internal/request/update_message_metadata_request.py index 278e9091..f454ff7e 100644 --- a/sinch/domains/conversation/models/v1/messages/internal/request/update_message_metadata_request.py +++ b/sinch/domains/conversation/models/v1/messages/internal/request/update_message_metadata_request.py @@ -4,11 +4,11 @@ MessagesSourceType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationRequest, + BaseModelConfiguration, ) -class UpdateMessageMetadataRequest(BaseModelConfigurationRequest): +class UpdateMessageMetadataRequest(BaseModelConfiguration): message_id: str = Field(..., description="The unique ID of the message.") metadata: StrictStr = Field( ..., description="Metadata that should be associated with the message." diff --git a/sinch/domains/conversation/models/v1/messages/response/message_response.py b/sinch/domains/conversation/models/v1/messages/response/message_response.py index d62bb794..b6eec633 100644 --- a/sinch/domains/conversation/models/v1/messages/response/message_response.py +++ b/sinch/domains/conversation/models/v1/messages/response/message_response.py @@ -8,17 +8,15 @@ ContactMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class AppMessageResponse( - MessageResponseCommonProps, BaseModelConfigurationResponse -): +class AppMessageResponse(MessageResponseCommonProps, BaseModelConfiguration): app_message: AppMessage class ContactMessageResponse( - MessageResponseCommonProps, BaseModelConfigurationResponse + MessageResponseCommonProps, BaseModelConfiguration ): contact_message: ContactMessage diff --git a/sinch/domains/conversation/models/v1/messages/response/types/__init__.py b/sinch/domains/conversation/models/v1/messages/response/types/__init__.py index 30e0cd54..767b4d0f 100644 --- a/sinch/domains/conversation/models/v1/messages/response/types/__init__.py +++ b/sinch/domains/conversation/models/v1/messages/response/types/__init__.py @@ -1,10 +1,10 @@ from sinch.domains.conversation.models.v1.messages.response.types.app_message import ( AppMessage, ) -from sinch.domains.conversation.models.v1.messages.response.types.channel_specific_message_content import ( +from sinch.domains.conversation.models.v1.messages.categories.channelspecific.channel_specific_message_content import ( ChannelSpecificMessageContent, ) -from sinch.domains.conversation.models.v1.messages.response.types.choice_option import ( +from sinch.domains.conversation.models.v1.messages.categories.choice.choice_option import ( ChoiceOption, ) from sinch.domains.conversation.models.v1.messages.response.types.contact_message import ( @@ -22,7 +22,7 @@ from sinch.domains.conversation.models.v1.messages.response.types.kakaotalk_coupon import ( KakaoTalkCoupon, ) -from sinch.domains.conversation.models.v1.messages.response.types.list_item import ( +from sinch.domains.conversation.models.v1.messages.categories.list.list_item import ( ListItem, ) from sinch.domains.conversation.models.v1.messages.response.types.payment_settings import ( @@ -31,6 +31,9 @@ from sinch.domains.conversation.models.v1.messages.response.types.whatsapp_interactive_header import ( WhatsAppInteractiveHeader, ) +from sinch.domains.conversation.models.v1.messages.response.types.send_message_response import ( + SendMessageResponse, +) __all__ = [ "AppMessage", @@ -43,5 +46,6 @@ "KakaoTalkCoupon", "ListItem", "PaymentSettings", + "SendMessageResponse", "WhatsAppInteractiveHeader", ] diff --git a/sinch/domains/conversation/models/v1/messages/response/types/payment_settings.py b/sinch/domains/conversation/models/v1/messages/response/types/payment_settings.py index 006c6497..83c6991f 100644 --- a/sinch/domains/conversation/models/v1/messages/response/types/payment_settings.py +++ b/sinch/domains/conversation/models/v1/messages/response/types/payment_settings.py @@ -10,11 +10,11 @@ Boleto, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class PaymentSettings(BaseModelConfigurationResponse): +class PaymentSettings(BaseModelConfiguration): dynamic_pix: Optional[DynamicPix] = Field( default=None, description="The dynamic Pix payment settings." ) diff --git a/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py b/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py new file mode 100644 index 00000000..0e146e42 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py @@ -0,0 +1,15 @@ +from pydantic import Field, StrictStr +from sinch.domains.conversation.models.v1.messages.internal.base import ( + BaseModelConfiguration, +) + + +class SendMessageResponse(BaseModelConfiguration): + """ + Response from sending a message. + """ + + message_id: StrictStr = Field( + ..., + description="The ID of the sent message.", + ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/__init__.py b/sinch/domains/conversation/models/v1/messages/shared/__init__.py index 44c3b27e..972cbec4 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/__init__.py +++ b/sinch/domains/conversation/models/v1/messages/shared/__init__.py @@ -17,9 +17,6 @@ from sinch.domains.conversation.models.v1.messages.shared.coordinates import ( Coordinates, ) -from sinch.domains.conversation.models.v1.messages.shared.list_section import ( - ListSection, -) from sinch.domains.conversation.models.v1.messages.shared.product_item import ( ProductItem, ) @@ -34,7 +31,6 @@ "ContactMessageCommonProps", "MessageResponseCommonProps", "Coordinates", - "ListSection", "OmniMessageOverride", "ProductItem", "Reason", diff --git a/sinch/domains/conversation/models/v1/messages/shared/address_info.py b/sinch/domains/conversation/models/v1/messages/shared/address_info.py index ff2fed6b..a7bbc8e0 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/address_info.py +++ b/sinch/domains/conversation/models/v1/messages/shared/address_info.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class AddressInfo(BaseModelConfigurationResponse): +class AddressInfo(BaseModelConfiguration): city: Optional[StrictStr] = Field(default=None, description="City Name") country: Optional[StrictStr] = Field( default=None, description="Country Name" diff --git a/sinch/domains/conversation/models/v1/messages/shared/agent.py b/sinch/domains/conversation/models/v1/messages/shared/agent.py index 62ef947f..f18a3c7b 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/agent.py +++ b/sinch/domains/conversation/models/v1/messages/shared/agent.py @@ -2,11 +2,11 @@ from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.types import AgentType from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class Agent(BaseModelConfigurationResponse): +class Agent(BaseModelConfiguration): display_name: Optional[StrictStr] = Field( default=None, description="Agent's display name" ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/app_message_common_props.py b/sinch/domains/conversation/models/v1/messages/shared/app_message_common_props.py index aa60920e..17091a95 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/app_message_common_props.py +++ b/sinch/domains/conversation/models/v1/messages/shared/app_message_common_props.py @@ -5,14 +5,14 @@ ) from sinch.domains.conversation.models.v1.messages.shared import Agent from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) from sinch.domains.conversation.models.v1.messages.shared.override.omni_message_override import ( OmniMessageOverride, ) -class AppMessageCommonProps(BaseModelConfigurationResponse): +class AppMessageCommonProps(BaseModelConfiguration): explicit_channel_message: Optional[Dict[str, StrictStr]] = Field( default=None, description="Allows you to specify a channel and define a corresponding channel specific message payload that will override the standard Conversation API message types. The key in the map must point to a valid conversation channel as defined in the enum `ConversationChannel`. The message content must be provided in string format. You may use the [transcoding endpoint](https://developers.sinch.com/docs/conversation/api-reference/conversation/tag/Transcoding/) to help create your message. For more information about how to construct an explicit channel message for a particular channel, see that [channel's corresponding documentation](https://developers.sinch.com/docs/conversation/channel-support/) (for example, using explicit channel messages with [the WhatsApp channel](https://developers.sinch.com/docs/conversation/channel-support/whatsapp/message-support/#explicit-channel-messages)).", diff --git a/sinch/domains/conversation/models/v1/messages/shared/channel_identity.py b/sinch/domains/conversation/models/v1/messages/shared/channel_identity.py index 33e411ff..ecf4daa1 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/channel_identity.py +++ b/sinch/domains/conversation/models/v1/messages/shared/channel_identity.py @@ -4,11 +4,11 @@ ConversationChannelType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ChannelIdentity(BaseModelConfigurationResponse): +class ChannelIdentity(BaseModelConfiguration): app_id: Optional[StrictStr] = Field( default=None, description="Required if using a channel that uses app-scoped channel identities. Currently, FB Messenger, Instagram, LINE, and WeChat use app-scoped channel identities, which means contacts will have different channel identities on different Conversation API apps. These can be thought of as virtual identities that are app-specific and, therefore, the app_id must be included in the API call.", diff --git a/sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py b/sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py new file mode 100644 index 00000000..113d7e43 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py @@ -0,0 +1,24 @@ +from typing import Optional +from pydantic import Field, StrictStr +from sinch.domains.conversation.models.v1.messages.types.conversation_channel_type import ( + ConversationChannelType, +) +from sinch.domains.conversation.models.v1.messages.internal.base import ( + BaseModelConfiguration, +) + + +class ChannelIdentityRequest(BaseModelConfiguration): + """ + Channel identity for request models. + """ + + app_id: Optional[StrictStr] = Field( + default=None, + description="Required if using a channel that uses app-scoped channel identities. Currently, FB Messenger, Instagram, LINE, and WeChat use app-scoped channel identities, which means contacts will have different channel identities on different Conversation API apps. These can be thought of as virtual identities that are app-specific and, therefore, the app_id must be included in the API call.", + ) + channel: ConversationChannelType = Field(...) + identity: StrictStr = Field( + default=..., + description="The channel identity. This will differ from channel to channel. For example, a phone number for SMS, WhatsApp, and Viber Business.", + ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/choice_item.py b/sinch/domains/conversation/models/v1/messages/shared/choice_item.py index 9ccd97c8..9e5c0459 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/choice_item.py +++ b/sinch/domains/conversation/models/v1/messages/shared/choice_item.py @@ -4,11 +4,11 @@ MediaProperties, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ChoiceItem(BaseModelConfigurationResponse): +class ChoiceItem(BaseModelConfiguration): title: StrictStr = Field( default=..., description="Required parameter. Title for the choice item.", diff --git a/sinch/domains/conversation/models/v1/messages/shared/contact_message_common_props.py b/sinch/domains/conversation/models/v1/messages/shared/contact_message_common_props.py index bdb0f289..e67161c8 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/contact_message_common_props.py +++ b/sinch/domains/conversation/models/v1/messages/shared/contact_message_common_props.py @@ -3,9 +3,9 @@ ReplyTo, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ContactMessageCommonProps(BaseModelConfigurationResponse): +class ContactMessageCommonProps(BaseModelConfiguration): reply_to: Optional[ReplyTo] = None diff --git a/sinch/domains/conversation/models/v1/messages/shared/coordinates.py b/sinch/domains/conversation/models/v1/messages/shared/coordinates.py index 3c558237..94dbbccb 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/coordinates.py +++ b/sinch/domains/conversation/models/v1/messages/shared/coordinates.py @@ -1,11 +1,11 @@ from typing import Union from pydantic import Field, StrictFloat, StrictInt from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class Coordinates(BaseModelConfigurationResponse): +class Coordinates(BaseModelConfiguration): latitude: Union[StrictFloat, StrictInt] = Field( default=..., description="The latitude." ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/email_info.py b/sinch/domains/conversation/models/v1/messages/shared/email_info.py index bcc0572d..ea83866e 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/email_info.py +++ b/sinch/domains/conversation/models/v1/messages/shared/email_info.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class EmailInfo(BaseModelConfigurationResponse): +class EmailInfo(BaseModelConfiguration): email_address: StrictStr = Field(default=..., description="Email address.") type: Optional[StrictStr] = Field( default=None, description="Email address type. e.g. WORK or HOME." diff --git a/sinch/domains/conversation/models/v1/messages/shared/list_section.py b/sinch/domains/conversation/models/v1/messages/shared/list_section.py index 23b7006e..e6163fd9 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/list_section.py +++ b/sinch/domains/conversation/models/v1/messages/shared/list_section.py @@ -1,14 +1,14 @@ from typing import Optional from pydantic import Field, StrictStr, conlist -from sinch.domains.conversation.models.v1.messages.response.types.list_item import ( +from sinch.domains.conversation.models.v1.messages.categories.list.list_item import ( ListItem, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ListSection(BaseModelConfigurationResponse): +class ListSection(BaseModelConfiguration): title: Optional[StrictStr] = Field( default=None, description="Optional parameter. Title for list section." ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/message_response_common_props.py b/sinch/domains/conversation/models/v1/messages/shared/message_response_common_props.py index 08107012..b930d380 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/message_response_common_props.py +++ b/sinch/domains/conversation/models/v1/messages/shared/message_response_common_props.py @@ -11,11 +11,11 @@ ProcessingModeType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class MessageResponseCommonProps(BaseModelConfigurationResponse): +class MessageResponseCommonProps(BaseModelConfiguration): accept_time: Optional[datetime] = Field( default=None, description="The time Conversation API processed the message.", diff --git a/sinch/domains/conversation/models/v1/messages/shared/name_info.py b/sinch/domains/conversation/models/v1/messages/shared/name_info.py index 337db7c3..006e7137 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/name_info.py +++ b/sinch/domains/conversation/models/v1/messages/shared/name_info.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class NameInfo(BaseModelConfigurationResponse): +class NameInfo(BaseModelConfiguration): full_name: StrictStr = Field( default=..., description="Full name of the contact" ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/organization_info.py b/sinch/domains/conversation/models/v1/messages/shared/organization_info.py index 98158fbb..39deed8f 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/organization_info.py +++ b/sinch/domains/conversation/models/v1/messages/shared/organization_info.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class OrganizationInfo(BaseModelConfigurationResponse): +class OrganizationInfo(BaseModelConfiguration): company: Optional[StrictStr] = Field( default=None, description="Company name" ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/phone_number_info.py b/sinch/domains/conversation/models/v1/messages/shared/phone_number_info.py index 253301de..c5cb58f2 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/phone_number_info.py +++ b/sinch/domains/conversation/models/v1/messages/shared/phone_number_info.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class PhoneNumberInfo(BaseModelConfigurationResponse): +class PhoneNumberInfo(BaseModelConfiguration): phone_number: StrictStr = Field( default=..., description="Phone number with country code included." ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/product_item.py b/sinch/domains/conversation/models/v1/messages/shared/product_item.py index a48feeb4..3a5ed876 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/product_item.py +++ b/sinch/domains/conversation/models/v1/messages/shared/product_item.py @@ -1,11 +1,11 @@ from typing import Optional, Union from pydantic import Field, StrictFloat, StrictInt, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class ProductItem(BaseModelConfigurationResponse): +class ProductItem(BaseModelConfiguration): id: StrictStr = Field( default=..., description="Required parameter. The ID for the product." ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/reason.py b/sinch/domains/conversation/models/v1/messages/shared/reason.py index f3e03bcc..66048aea 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/reason.py +++ b/sinch/domains/conversation/models/v1/messages/shared/reason.py @@ -7,11 +7,11 @@ ReasonSubCodeType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class Reason(BaseModelConfigurationResponse): +class Reason(BaseModelConfiguration): code: Optional[ReasonCodeType] = None description: Optional[StrictStr] = Field( default=None, description="A textual description of the reason." diff --git a/sinch/domains/conversation/models/v1/messages/shared/url_info.py b/sinch/domains/conversation/models/v1/messages/shared/url_info.py index 4fdfd650..d0c425b5 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/url_info.py +++ b/sinch/domains/conversation/models/v1/messages/shared/url_info.py @@ -1,11 +1,11 @@ from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class UrlInfo(BaseModelConfigurationResponse): +class UrlInfo(BaseModelConfiguration): url: StrictStr = Field(default=..., description="The URL to be referenced") type: Optional[StrictStr] = Field( default=None, description="Optional. URL type, e.g. Org or Social" diff --git a/sinch/domains/conversation/models/v1/messages/types/__init__.py b/sinch/domains/conversation/models/v1/messages/types/__init__.py index 9eb79ea8..fb34138a 100644 --- a/sinch/domains/conversation/models/v1/messages/types/__init__.py +++ b/sinch/domains/conversation/models/v1/messages/types/__init__.py @@ -40,6 +40,49 @@ from sinch.domains.conversation.models.v1.messages.types.whatsapp_interactive_nfm_reply_name_type import ( WhatsAppInteractiveNfmReplyNameType, ) +from sinch.domains.conversation.models.v1.messages.types.processing_strategy_type import ( + ProcessingStrategyType, +) +from sinch.domains.conversation.models.v1.messages.types.metadata_update_strategy_type import ( + MetadataUpdateStrategyType, +) +from sinch.domains.conversation.models.v1.messages.types.message_queue_type import ( + MessageQueueType, +) +from sinch.domains.conversation.models.v1.messages.types.message_content_type import ( + MessageContentType, +) +from sinch.domains.conversation.models.v1.messages.types.list_message_dict import ( + ListMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.media_properties_dict import ( + MediaPropertiesDict, +) +from sinch.domains.conversation.models.v1.messages.types.card_message_dict import ( + CardMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.carousel_message_dict import ( + CarouselMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.choice_message_dict import ( + ChoiceMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.contact_info_message_dict import ( + ContactInfoMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.location_message_dict import ( + LocationMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.template_message_dict import ( + TemplateMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.recipient_dict import ( + RecipientDict, + ChannelRecipientIdentityDict, +) +from sinch.domains.conversation.models.v1.messages.types.send_message_request_body_dict import ( + SendMessageRequestBodyDict, +) __all__ = [ "AgentType", @@ -48,6 +91,17 @@ "ProcessingModeType", "CardHeightType", "ChannelSpecificMessageType", + "ListMessageDict", + "MediaPropertiesDict", + "CardMessageDict", + "CarouselMessageDict", + "ChoiceMessageDict", + "ContactInfoMessageDict", + "LocationMessageDict", + "TemplateMessageDict", + "RecipientDict", + "ChannelRecipientIdentityDict", + "SendMessageRequestBodyDict", "MessagesSourceType", "PaymentOrderGoodsType", "PaymentOrderStatusType", @@ -56,4 +110,8 @@ "ReasonCodeType", "ReasonSubCodeType", "WhatsAppInteractiveNfmReplyNameType", + "ProcessingStrategyType", + "MetadataUpdateStrategyType", + "MessageQueueType", + "MessageContentType", ] diff --git a/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py new file mode 100644 index 00000000..1d15244d --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py @@ -0,0 +1,11 @@ +from typing import TypedDict +from typing_extensions import NotRequired + + +class CalendarMessageDict(TypedDict): + title: str + event_start: str + event_end: str + event_title: str + fallback_url: str + event_description: NotRequired[str] diff --git a/sinch/domains/conversation/models/v1/messages/types/call_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/call_message_dict.py new file mode 100644 index 00000000..977bfd5a --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/call_message_dict.py @@ -0,0 +1,6 @@ +from typing import TypedDict + + +class CallMessageDict(TypedDict): + phone_number: str + title: str diff --git a/sinch/domains/conversation/models/v1/messages/types/card_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/card_message_dict.py new file mode 100644 index 00000000..0a9803e7 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/card_message_dict.py @@ -0,0 +1,24 @@ +from typing import List, TypedDict +from typing_extensions import NotRequired + +from sinch.domains.conversation.models.v1.messages.types.card_height_type import ( + CardHeightType, +) +from sinch.domains.conversation.models.v1.messages.types.choice_option_dict import ( + ChoiceOptionDict, +) +from sinch.domains.conversation.models.v1.messages.types.media_properties_dict import ( + MediaPropertiesDict, +) +from sinch.domains.conversation.models.v1.messages.types.message_properties_dict import ( + MessagePropertiesDict, +) + + +class CardMessageDict(TypedDict): + choices: NotRequired[List[ChoiceOptionDict]] + description: NotRequired[str] + height: NotRequired[CardHeightType] + title: NotRequired[str] + media_message: NotRequired[MediaPropertiesDict] + message_properties: NotRequired[MessagePropertiesDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/carousel_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/carousel_message_dict.py new file mode 100644 index 00000000..39613be5 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/carousel_message_dict.py @@ -0,0 +1,14 @@ +from typing import List, TypedDict +from typing_extensions import NotRequired + +from sinch.domains.conversation.models.v1.messages.types.card_message_dict import ( + CardMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.choice_option_dict import ( + ChoiceOptionDict, +) + + +class CarouselMessageDict(TypedDict): + cards: List[CardMessageDict] + choices: NotRequired[List[ChoiceOptionDict]] diff --git a/sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py new file mode 100644 index 00000000..81fdd933 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py @@ -0,0 +1,18 @@ +from typing import List, TypedDict +from typing_extensions import NotRequired + +from sinch.domains.conversation.models.v1.messages.types.choice_option_dict import ( + ChoiceOptionDict, +) +from sinch.domains.conversation.models.v1.messages.types.message_properties_dict import ( + MessagePropertiesDict, +) +from sinch.domains.conversation.models.v1.messages.types.text_message_dict import ( + TextMessageDict, +) + + +class ChoiceMessageDict(TypedDict): + choices: List[ChoiceOptionDict] + text_message: NotRequired[TextMessageDict] + message_properties: NotRequired[MessagePropertiesDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/choice_option_dict.py b/sinch/domains/conversation/models/v1/messages/types/choice_option_dict.py new file mode 100644 index 00000000..cd957119 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/choice_option_dict.py @@ -0,0 +1,34 @@ +from typing import Any, TypedDict +from typing_extensions import NotRequired + +from sinch.domains.conversation.models.v1.messages.types.calendar_message_dict import ( + CalendarMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.call_message_dict import ( + CallMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.location_message_dict import ( + LocationMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.share_location_message_dict import ( + ShareLocationMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.text_message_dict import ( + TextMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.url_message_dict import ( + UrlMessageDict, +) + + +class ChoiceOptionDict(TypedDict): + # Optional metadata returned back to you as postback + postback_data: NotRequired[Any] + + # Exactly one of the following keys is expected per choice: + call_message: NotRequired[CallMessageDict] + location_message: NotRequired[LocationMessageDict] + text_message: NotRequired[TextMessageDict] + url_message: NotRequired[UrlMessageDict] + calendar_message: NotRequired[CalendarMessageDict] + share_location_message: NotRequired[ShareLocationMessageDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py new file mode 100644 index 00000000..622a0cf0 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py @@ -0,0 +1,51 @@ +from typing import List, TypedDict +from typing_extensions import NotRequired + + +class NameInfoDict(TypedDict): + full_name: str + first_name: NotRequired[str] + last_name: NotRequired[str] + middle_name: NotRequired[str] + prefix: NotRequired[str] + suffix: NotRequired[str] + + +class PhoneNumberInfoDict(TypedDict): + phone_number: str + type: NotRequired[str] + + +class AddressInfoDict(TypedDict): + city: NotRequired[str] + country: NotRequired[str] + state: NotRequired[str] + zip: NotRequired[str] + type: NotRequired[str] + country_code: NotRequired[str] + + +class EmailInfoDict(TypedDict): + email_address: str + type: NotRequired[str] + + +class OrganizationInfoDict(TypedDict): + company: NotRequired[str] + department: NotRequired[str] + title: NotRequired[str] + + +class UrlInfoDict(TypedDict): + url: str + type: NotRequired[str] + + +class ContactInfoMessageDict(TypedDict): + name: NameInfoDict + phone_numbers: List[PhoneNumberInfoDict] + addresses: NotRequired[List[AddressInfoDict]] + email_addresses: NotRequired[List[EmailInfoDict]] + organization: NotRequired[OrganizationInfoDict] + urls: NotRequired[List[UrlInfoDict]] + birthday: NotRequired[str] diff --git a/sinch/domains/conversation/models/v1/messages/types/coordinates_dict.py b/sinch/domains/conversation/models/v1/messages/types/coordinates_dict.py new file mode 100644 index 00000000..99eb060f --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/coordinates_dict.py @@ -0,0 +1,6 @@ +from typing import TypedDict, Union + + +class CoordinatesDict(TypedDict): + latitude: Union[int, float] + longitude: Union[int, float] diff --git a/sinch/domains/conversation/models/v1/messages/types/list_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/list_message_dict.py new file mode 100644 index 00000000..0783d4bb --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/list_message_dict.py @@ -0,0 +1,51 @@ +from typing import List, TypedDict, Union +from typing_extensions import NotRequired + +from sinch.domains.conversation.models.v1.messages.types.media_properties_dict import ( + MediaPropertiesDict, +) + + +class ChoiceItemDict(TypedDict): + title: str + description: NotRequired[str] + media: NotRequired[MediaPropertiesDict] + postback_data: NotRequired[str] + + +class ProductItemDict(TypedDict): + id: str + marketplace: str + quantity: NotRequired[int] + item_price: NotRequired[Union[int, float]] + currency: NotRequired[str] + + +class ListItemChoiceDict(TypedDict): + choice: ChoiceItemDict + + +class ListItemProductDict(TypedDict): + product: ProductItemDict + + +ListItemDict = Union[ListItemChoiceDict, ListItemProductDict] + + +class ListSectionDict(TypedDict): + items: List[ListItemDict] + title: NotRequired[str] + + +class ListMessagePropertiesDict(TypedDict): + catalog_id: NotRequired[str] + menu: NotRequired[str] + whatsapp_header: NotRequired[str] + + +class ListMessageDict(TypedDict): + title: str + sections: List[ListSectionDict] + description: NotRequired[str] + media: NotRequired[MediaPropertiesDict] + message_properties: NotRequired[ListMessagePropertiesDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/location_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/location_message_dict.py new file mode 100644 index 00000000..38e945fb --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/location_message_dict.py @@ -0,0 +1,12 @@ +from typing import TypedDict +from typing_extensions import NotRequired + +from sinch.domains.conversation.models.v1.messages.types.coordinates_dict import ( + CoordinatesDict, +) + + +class LocationMessageDict(TypedDict): + coordinates: CoordinatesDict + title: str + label: NotRequired[str] diff --git a/sinch/domains/conversation/models/v1/messages/types/media_properties_dict.py b/sinch/domains/conversation/models/v1/messages/types/media_properties_dict.py new file mode 100644 index 00000000..b55181aa --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/media_properties_dict.py @@ -0,0 +1,8 @@ +from typing import TypedDict +from typing_extensions import NotRequired + + +class MediaPropertiesDict(TypedDict): + url: str + thumbnail_url: NotRequired[str] + filename_override: NotRequired[str] diff --git a/sinch/domains/conversation/models/v1/messages/types/message_content_type.py b/sinch/domains/conversation/models/v1/messages/types/message_content_type.py new file mode 100644 index 00000000..1b7058ea --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/message_content_type.py @@ -0,0 +1,7 @@ +from typing import Literal, Union +from pydantic import StrictStr + +MessageContentType = Union[ + Literal["CONTENT_UNKNOWN", "CONTENT_MARKETING", "CONTENT_NOTIFICATION"], + StrictStr, +] diff --git a/sinch/domains/conversation/models/v1/messages/types/message_properties_dict.py b/sinch/domains/conversation/models/v1/messages/types/message_properties_dict.py new file mode 100644 index 00000000..f7ab8036 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/message_properties_dict.py @@ -0,0 +1,6 @@ +from typing import TypedDict +from typing_extensions import NotRequired + + +class MessagePropertiesDict(TypedDict): + whatsapp_header: NotRequired[str] diff --git a/sinch/domains/conversation/models/v1/messages/types/message_queue_type.py b/sinch/domains/conversation/models/v1/messages/types/message_queue_type.py new file mode 100644 index 00000000..f7f4a28f --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/message_queue_type.py @@ -0,0 +1,7 @@ +from typing import Literal, Union +from pydantic import StrictStr + +MessageQueueType = Union[ + Literal["NORMAL_PRIORITY", "HIGH_PRIORITY"], + StrictStr, +] diff --git a/sinch/domains/conversation/models/v1/messages/types/metadata_update_strategy_type.py b/sinch/domains/conversation/models/v1/messages/types/metadata_update_strategy_type.py new file mode 100644 index 00000000..94fb09b1 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/metadata_update_strategy_type.py @@ -0,0 +1,7 @@ +from typing import Literal, Union +from pydantic import StrictStr + +MetadataUpdateStrategyType = Union[ + Literal["REPLACE", "MERGE_PATCH"], + StrictStr, +] diff --git a/sinch/domains/conversation/models/v1/messages/types/processing_strategy_type.py b/sinch/domains/conversation/models/v1/messages/types/processing_strategy_type.py new file mode 100644 index 00000000..8bb2311e --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/processing_strategy_type.py @@ -0,0 +1,7 @@ +from typing import Literal, Union +from pydantic import StrictStr + +ProcessingStrategyType = Union[ + Literal["DEFAULT", "DISPATCH_ONLY"], + StrictStr, +] diff --git a/sinch/domains/conversation/models/v1/messages/types/recipient_dict.py b/sinch/domains/conversation/models/v1/messages/types/recipient_dict.py new file mode 100644 index 00000000..936c2187 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/recipient_dict.py @@ -0,0 +1,20 @@ +from typing import List, TypedDict, Union +from sinch.domains.conversation.models.v1.messages.types.conversation_channel_type import ( + ConversationChannelType, +) + + +class ChannelRecipientIdentityDict(TypedDict): + channel: ConversationChannelType + identity: str + + +class RecipientIdentifiedByDict(TypedDict): + channel_identities: List[ChannelRecipientIdentityDict] + + +class RecipientContactIdDict(TypedDict): + contact_id: str + + +RecipientDict = Union[RecipientIdentifiedByDict, RecipientContactIdDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/send_message_request_body_dict.py b/sinch/domains/conversation/models/v1/messages/types/send_message_request_body_dict.py new file mode 100644 index 00000000..1f0de272 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/send_message_request_body_dict.py @@ -0,0 +1,46 @@ +from typing import TypedDict +from typing_extensions import NotRequired +from sinch.domains.conversation.models.v1.messages.types.card_message_dict import ( + CardMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.carousel_message_dict import ( + CarouselMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.choice_message_dict import ( + ChoiceMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.contact_info_message_dict import ( + ContactInfoMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.list_message_dict import ( + ListMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.location_message_dict import ( + LocationMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.media_properties_dict import ( + MediaPropertiesDict, +) +from sinch.domains.conversation.models.v1.messages.types.template_message_dict import ( + TemplateMessageDict, +) +from sinch.domains.conversation.models.v1.messages.types.text_message_dict import ( + TextMessageDict, +) + + +class SendMessageRequestBodyDict(TypedDict, total=False): + """ + TypedDict for the message body in send message requests. + At least one message type must be provided. + """ + + text_message: NotRequired[TextMessageDict] + card_message: NotRequired[CardMessageDict] + carousel_message: NotRequired[CarouselMessageDict] + choice_message: NotRequired[ChoiceMessageDict] + contact_info_message: NotRequired[ContactInfoMessageDict] + list_message: NotRequired[ListMessageDict] + location_message: NotRequired[LocationMessageDict] + media_message: NotRequired[MediaPropertiesDict] + template_message: NotRequired[TemplateMessageDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/share_location_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/share_location_message_dict.py new file mode 100644 index 00000000..5c4b975c --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/share_location_message_dict.py @@ -0,0 +1,6 @@ +from typing import TypedDict + + +class ShareLocationMessageDict(TypedDict): + title: str + fallback_url: str diff --git a/sinch/domains/conversation/models/v1/messages/types/template_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/template_message_dict.py new file mode 100644 index 00000000..2782a6ea --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/template_message_dict.py @@ -0,0 +1,20 @@ +from typing import Dict, TypedDict +from typing_extensions import NotRequired + + +class TemplateReferenceChannelSpecificDict(TypedDict): + template_id: str + version: NotRequired[str] + language_code: NotRequired[str] + parameters: NotRequired[Dict[str, str]] + + +class TemplateReferenceOmniChannelDict(TemplateReferenceChannelSpecificDict): + version: str + + +class TemplateMessageDict(TypedDict): + channel_template: NotRequired[ + Dict[str, TemplateReferenceChannelSpecificDict] + ] + omni_template: NotRequired[TemplateReferenceOmniChannelDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/text_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/text_message_dict.py new file mode 100644 index 00000000..f3c3330a --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/text_message_dict.py @@ -0,0 +1,5 @@ +from typing import TypedDict + + +class TextMessageDict(TypedDict): + text: str diff --git a/sinch/domains/conversation/models/v1/messages/types/url_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/url_message_dict.py new file mode 100644 index 00000000..cb289c25 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/url_message_dict.py @@ -0,0 +1,6 @@ +from typing import TypedDict + + +class UrlMessageDict(TypedDict): + title: str + url: str diff --git a/tests/e2e/conversation/features/steps/conversation.steps.py b/tests/e2e/conversation/features/steps/conversation.steps.py index 8aa657ca..e330ca95 100644 --- a/tests/e2e/conversation/features/steps/conversation.steps.py +++ b/tests/e2e/conversation/features/steps/conversation.steps.py @@ -69,12 +69,18 @@ def step_validate_update_message_response(context): @when('I send a request to send a message to a contact') def step_send_message(context): - pass + context.message_response = context.messages.send_text_message( + app_id='01W4FFL35P4NC4K35CONVAPP001', + text='Hello', + contact_id='01W4FFL35P4NC4K35CONTACT001' + ) @then('the response contains the id of the message') def step_validate_send_message_response(context): - pass + assert context.message_response is not None, 'Message response should not be None' + assert hasattr(context.message_response, 'message_id'), 'Message response should have message_id attribute' + assert context.message_response.message_id == '01W4FFL35P4NC4K35MESSAGE001', f'Expected message_id to be "01W4FFL35P4NC4K35MESSAGE001", got "{context.message_response.message_id}"' @when('I send a request to list the existing messages') From 924e1a3752e1e04093a87763396744c9ded16536 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Tue, 27 Jan 2026 15:18:21 +0100 Subject: [PATCH 3/4] solve PR comments --- .../categories/choice/choice_message.py | 8 +++---- .../choice/choice_message_properties.py | 15 ++++++++++++ .../internal/request/send_message_request.py | 12 ++++++---- .../v1/messages/response/message_response.py | 8 +++---- .../response/types/send_message_response.py | 10 ++++---- .../models/v1/messages/shared/__init__.py | 6 ++--- .../shared/channel_identity_request.py | 24 ------------------- ...ommon_props.py => message_common_props.py} | 2 +- .../messages/types/calendar_message_dict.py | 5 ++-- .../v1/messages/types/choice_message_dict.py | 8 +++---- .../types/choice_message_properties_dict.py | 11 +++++++++ .../types/contact_info_message_dict.py | 3 ++- 12 files changed, 59 insertions(+), 53 deletions(-) create mode 100644 sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_properties.py delete mode 100644 sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py rename sinch/domains/conversation/models/v1/messages/shared/{message_response_common_props.py => message_common_props.py} (97%) create mode 100644 sinch/domains/conversation/models/v1/messages/types/choice_message_properties_dict.py diff --git a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py index 511d451e..789c3af7 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message.py @@ -1,5 +1,8 @@ from typing import Optional from pydantic import Field, conlist +from sinch.domains.conversation.models.v1.messages.categories.choice.choice_message_properties import ( + ChoiceMessageProperties, +) from sinch.domains.conversation.models.v1.messages.categories.choice.choice_option import ( ChoiceOption, ) @@ -9,9 +12,6 @@ from sinch.domains.conversation.models.v1.messages.categories.text import ( TextMessage, ) -from sinch.domains.conversation.models.v1.messages.categories.card.message_properties import ( - MessageProperties, -) class ChoiceMessage(BaseModelConfiguration): @@ -19,4 +19,4 @@ class ChoiceMessage(BaseModelConfiguration): default=..., description="The number of choices is limited to 10." ) text_message: Optional[TextMessage] = None - message_properties: Optional[MessageProperties] = None + message_properties: Optional[ChoiceMessageProperties] = None diff --git a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_properties.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_properties.py new file mode 100644 index 00000000..14e61940 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_message_properties.py @@ -0,0 +1,15 @@ +from typing import Optional +from pydantic import Field, StrictStr +from sinch.domains.conversation.models.v1.messages.internal.base import ( + BaseModelConfiguration, +) + + +class ChoiceMessageProperties(BaseModelConfiguration): + whatsapp_footer: Optional[StrictStr] = Field( + default=None, + description=( + "Optional. Sets the text for the footer of a WhatsApp reply button or URL button message. " + "Ignored for other channels." + ), + ) diff --git a/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py b/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py index be171245..accf6f61 100644 --- a/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py +++ b/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional, Union -from pydantic import Field, StrictStr, field_serializer +from pydantic import Field, StrictInt, StrictStr, field_serializer from sinch.domains.conversation.models.v1.messages.internal.request.recipient import ( Recipient, ) @@ -40,7 +40,7 @@ class SendMessageRequest(BaseModelConfiguration): ..., description="The message content to send.", ) - ttl: Optional[Union[StrictStr, int]] = Field( + ttl: Optional[Union[StrictStr, StrictInt]] = Field( default=None, description="The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'.", ) @@ -88,15 +88,17 @@ class SendMessageRequest(BaseModelConfiguration): ) @field_serializer("ttl") - def serialize_ttl(self, value: Optional[Union[str, int]]) -> Optional[str]: + def serialize_ttl( + self, value: Optional[Union[StrictStr, StrictInt]] + ) -> Optional[str]: """ Serialize ttl field to the format expected by the API (string with 's' suffix). Converts int to string with 's' suffix, or ensures string has 's' suffix. """ if value is None: return None - if isinstance(value, (int, float)): - return f"{int(value)}s" + if isinstance(value, int): + return f"{value}s" if isinstance(value, str) and not value.endswith("s"): return f"{value}s" return value diff --git a/sinch/domains/conversation/models/v1/messages/response/message_response.py b/sinch/domains/conversation/models/v1/messages/response/message_response.py index b6eec633..75393428 100644 --- a/sinch/domains/conversation/models/v1/messages/response/message_response.py +++ b/sinch/domains/conversation/models/v1/messages/response/message_response.py @@ -1,5 +1,5 @@ from sinch.domains.conversation.models.v1.messages.shared import ( - MessageResponseCommonProps, + MessageCommonProps, ) from sinch.domains.conversation.models.v1.messages.response.types.app_message import ( AppMessage, @@ -12,11 +12,9 @@ ) -class AppMessageResponse(MessageResponseCommonProps, BaseModelConfiguration): +class AppMessageResponse(MessageCommonProps, BaseModelConfiguration): app_message: AppMessage -class ContactMessageResponse( - MessageResponseCommonProps, BaseModelConfiguration -): +class ContactMessageResponse(MessageCommonProps, BaseModelConfiguration): contact_message: ContactMessage diff --git a/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py b/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py index 0e146e42..ae727e69 100644 --- a/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py +++ b/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py @@ -1,3 +1,5 @@ +from datetime import datetime +from typing import Optional from pydantic import Field, StrictStr from sinch.domains.conversation.models.v1.messages.internal.base import ( BaseModelConfiguration, @@ -5,10 +7,10 @@ class SendMessageResponse(BaseModelConfiguration): - """ - Response from sending a message. - """ - + accepted_time: Optional[datetime] = Field( + default=None, + description="Timestamp when the Conversation API accepted the message for delivery to the referenced contact.", + ) message_id: StrictStr = Field( ..., description="The ID of the sent message.", diff --git a/sinch/domains/conversation/models/v1/messages/shared/__init__.py b/sinch/domains/conversation/models/v1/messages/shared/__init__.py index 972cbec4..2bf6844a 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/__init__.py +++ b/sinch/domains/conversation/models/v1/messages/shared/__init__.py @@ -11,8 +11,8 @@ from sinch.domains.conversation.models.v1.messages.shared.contact_message_common_props import ( ContactMessageCommonProps, ) -from sinch.domains.conversation.models.v1.messages.shared.message_response_common_props import ( - MessageResponseCommonProps, +from sinch.domains.conversation.models.v1.messages.shared.message_common_props import ( + MessageCommonProps, ) from sinch.domains.conversation.models.v1.messages.shared.coordinates import ( Coordinates, @@ -29,7 +29,7 @@ "ChannelIdentity", "ChoiceItem", "ContactMessageCommonProps", - "MessageResponseCommonProps", + "MessageCommonProps", "Coordinates", "OmniMessageOverride", "ProductItem", diff --git a/sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py b/sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py deleted file mode 100644 index 113d7e43..00000000 --- a/sinch/domains/conversation/models/v1/messages/shared/channel_identity_request.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Optional -from pydantic import Field, StrictStr -from sinch.domains.conversation.models.v1.messages.types.conversation_channel_type import ( - ConversationChannelType, -) -from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfiguration, -) - - -class ChannelIdentityRequest(BaseModelConfiguration): - """ - Channel identity for request models. - """ - - app_id: Optional[StrictStr] = Field( - default=None, - description="Required if using a channel that uses app-scoped channel identities. Currently, FB Messenger, Instagram, LINE, and WeChat use app-scoped channel identities, which means contacts will have different channel identities on different Conversation API apps. These can be thought of as virtual identities that are app-specific and, therefore, the app_id must be included in the API call.", - ) - channel: ConversationChannelType = Field(...) - identity: StrictStr = Field( - default=..., - description="The channel identity. This will differ from channel to channel. For example, a phone number for SMS, WhatsApp, and Viber Business.", - ) diff --git a/sinch/domains/conversation/models/v1/messages/shared/message_response_common_props.py b/sinch/domains/conversation/models/v1/messages/shared/message_common_props.py similarity index 97% rename from sinch/domains/conversation/models/v1/messages/shared/message_response_common_props.py rename to sinch/domains/conversation/models/v1/messages/shared/message_common_props.py index b930d380..b79df300 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/message_response_common_props.py +++ b/sinch/domains/conversation/models/v1/messages/shared/message_common_props.py @@ -15,7 +15,7 @@ ) -class MessageResponseCommonProps(BaseModelConfiguration): +class MessageCommonProps(BaseModelConfiguration): accept_time: Optional[datetime] = Field( default=None, description="The time Conversation API processed the message.", diff --git a/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py index 1d15244d..77e67dc4 100644 --- a/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py +++ b/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py @@ -1,11 +1,12 @@ +from datetime import datetime from typing import TypedDict from typing_extensions import NotRequired class CalendarMessageDict(TypedDict): title: str - event_start: str - event_end: str + event_start: datetime + event_end: datetime event_title: str fallback_url: str event_description: NotRequired[str] diff --git a/sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py index 81fdd933..8bf24db8 100644 --- a/sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py +++ b/sinch/domains/conversation/models/v1/messages/types/choice_message_dict.py @@ -1,12 +1,12 @@ from typing import List, TypedDict from typing_extensions import NotRequired +from sinch.domains.conversation.models.v1.messages.types.choice_message_properties_dict import ( + ChoiceMessagePropertiesDict, +) from sinch.domains.conversation.models.v1.messages.types.choice_option_dict import ( ChoiceOptionDict, ) -from sinch.domains.conversation.models.v1.messages.types.message_properties_dict import ( - MessagePropertiesDict, -) from sinch.domains.conversation.models.v1.messages.types.text_message_dict import ( TextMessageDict, ) @@ -15,4 +15,4 @@ class ChoiceMessageDict(TypedDict): choices: List[ChoiceOptionDict] text_message: NotRequired[TextMessageDict] - message_properties: NotRequired[MessagePropertiesDict] + message_properties: NotRequired[ChoiceMessagePropertiesDict] diff --git a/sinch/domains/conversation/models/v1/messages/types/choice_message_properties_dict.py b/sinch/domains/conversation/models/v1/messages/types/choice_message_properties_dict.py new file mode 100644 index 00000000..db9d6ddd --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/choice_message_properties_dict.py @@ -0,0 +1,11 @@ +from typing import TypedDict +from typing_extensions import NotRequired + + +class ChoiceMessagePropertiesDict(TypedDict): + """ + Additional properties for ChoiceMessage (whatsapp_footer). + CardMessage uses MessagePropertiesDict with whatsapp_header. + """ + + whatsapp_footer: NotRequired[str] diff --git a/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py b/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py index 622a0cf0..e98b0c96 100644 --- a/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py +++ b/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py @@ -1,3 +1,4 @@ +from datetime import date from typing import List, TypedDict from typing_extensions import NotRequired @@ -48,4 +49,4 @@ class ContactInfoMessageDict(TypedDict): email_addresses: NotRequired[List[EmailInfoDict]] organization: NotRequired[OrganizationInfoDict] urls: NotRequired[List[UrlInfoDict]] - birthday: NotRequired[str] + birthday: NotRequired[date] From 006b3c835688dcffa441da07e87327253d5ca4ac Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Wed, 28 Jan 2026 17:20:49 +0100 Subject: [PATCH 4/4] solve PR comments II --- .../conversation/api/v1/messages_apis.py | 17 ++++++--- .../categories/choice/choice_option.py | 37 ++++++++++++++++++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/sinch/domains/conversation/api/v1/messages_apis.py b/sinch/domains/conversation/api/v1/messages_apis.py index 65f69191..90bc7b84 100644 --- a/sinch/domains/conversation/api/v1/messages_apis.py +++ b/sinch/domains/conversation/api/v1/messages_apis.py @@ -25,7 +25,6 @@ LocationMessageDict, MediaPropertiesDict, TemplateMessageDict, - RecipientDict, ChannelRecipientIdentityDict, SendMessageRequestBodyDict, ) @@ -230,8 +229,11 @@ def _send_message_variant( def send( self, app_id: str, - recipient: Union[RecipientDict, dict], message: Union[SendMessageRequestBodyDict, dict], + contact_id: Optional[str] = None, + recipient_identities: Optional[ + List[ChannelRecipientIdentityDict] + ] = None, ttl: Optional[Union[str, int]] = None, callback_url: Optional[str] = None, channel_priority_order: Optional[List[ConversationChannelType]] = None, @@ -255,10 +257,12 @@ def send( :param app_id: The ID of the Conversation API app sending the message. :type app_id: str - :param recipient: The recipient of the message. Can be a Recipient object or a dict (identified_by/contact_id). - :type recipient: Union[RecipientDict, dict] :param message: The message content to send. Can be a SendMessageRequestBodyDict or a dict. :type message: Union[SendMessageRequestBodyDict, dict] + :param contact_id: The contact ID of the recipient. Either contact_id or recipient_identities must be provided. + :type contact_id: Optional[str] + :param recipient_identities: List of channel identities for the recipient. Either contact_id or recipient_identities must be provided. + :type recipient_identities: Optional[List[ChannelRecipientIdentityDict]] :param ttl: The timeout allotted for sending the message. Can be seconds (int) or a string like '10s'. :type ttl: Optional[Union[str, int]] :param callback_url: Overwrites the default callback url for delivery receipts for this message. @@ -289,7 +293,10 @@ def send( For detailed documentation, visit https://developers.sinch.com/docs/conversation/. """ - recipient = coerce_recipient(recipient) + recipient_dict = build_recipient_dict( + contact_id=contact_id, recipient_identities=recipient_identities + ) + recipient = coerce_recipient(recipient_dict) # Coerce message to SendMessageRequestBody if it's a dict if isinstance(message, dict): message = SendMessageRequestBody(**message) diff --git a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py index cb4e385b..0110db24 100644 --- a/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py @@ -1,15 +1,17 @@ -from typing import Union +from typing import Annotated, Union, get_args +from pydantic import BeforeValidator from sinch.domains.conversation.models.v1.messages.categories.choice.choice_options import ( CalendarChoiceMessage, CallChoiceMessage, + ChoiceMessageWithPostback, LocationChoiceMessage, ShareLocationChoiceMessage, TextChoiceMessage, UrlChoiceMessage, ) -ChoiceOption = Union[ +ChoiceOptionUnion = Union[ CallChoiceMessage, LocationChoiceMessage, TextChoiceMessage, @@ -17,3 +19,34 @@ CalendarChoiceMessage, ShareLocationChoiceMessage, ] + + +def _choice_message_type_keys() -> frozenset[str]: + """Message-type keys derived from Union members (spec: choiceTypes oneOf).""" + base_fields = set(ChoiceMessageWithPostback.model_fields) + keys = set() + for model in get_args(ChoiceOptionUnion): + keys.update(model.model_fields.keys() - base_fields) + return frozenset(keys) + + +_CHOICE_MESSAGE_TYPE_KEYS = _choice_message_type_keys() + + +def _validate_exactly_one_choice_message_key(value: object) -> object: + """Ensure each choice dict has exactly one message-type key.""" + if not isinstance(value, dict): + return value + keys = _CHOICE_MESSAGE_TYPE_KEYS + count = sum(1 for k in keys if value.get(k) is not None) + if count != 1: + raise ValueError( + f"Each choice must have exactly one of: {', '.join(sorted(keys))}." + ) + return value + + +ChoiceOption = Annotated[ + ChoiceOptionUnion, + BeforeValidator(_validate_exactly_one_choice_message_key), +]