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..90bc7b84 100644 --- a/sinch/domains/conversation/api/v1/messages_apis.py +++ b/sinch/domains/conversation/api/v1/messages_apis.py @@ -1,21 +1,72 @@ -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, + 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 +165,954 @@ 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, + 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, + 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 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. + :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_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) + 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..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,22 +1,22 @@ 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_message_properties import ( + ChoiceMessageProperties, +) +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, ) -from sinch.domains.conversation.models.v1.messages.categories.card.message_properties import ( - MessageProperties, -) -class ChoiceMessage(BaseModelConfigurationResponse): +class ChoiceMessage(BaseModelConfiguration): choices: conlist(ChoiceOption) = Field( 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_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/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/categories/choice/choice_option.py b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py new file mode 100644 index 00000000..0110db24 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/categories/choice/choice_option.py @@ -0,0 +1,52 @@ +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, +) + +ChoiceOptionUnion = Union[ + CallChoiceMessage, + LocationChoiceMessage, + TextChoiceMessage, + UrlChoiceMessage, + 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), +] 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..accf6f61 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py @@ -0,0 +1,104 @@ +from typing import Any, Dict, List, Optional, Union + +from pydantic import Field, StrictInt, 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, StrictInt]] = 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[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): + 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/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..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, @@ -8,17 +8,13 @@ ContactMessage, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class AppMessageResponse( - MessageResponseCommonProps, BaseModelConfigurationResponse -): +class AppMessageResponse(MessageCommonProps, BaseModelConfiguration): app_message: AppMessage -class ContactMessageResponse( - MessageResponseCommonProps, BaseModelConfigurationResponse -): +class ContactMessageResponse(MessageCommonProps, 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/choice_option.py b/sinch/domains/conversation/models/v1/messages/response/types/choice_option.py deleted file mode 100644 index d2ddf127..00000000 --- a/sinch/domains/conversation/models/v1/messages/response/types/choice_option.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Union -from sinch.domains.conversation.models.v1.messages.categories.choice.choice_options import ( - CallChoiceMessage, - LocationChoiceMessage, - TextChoiceMessage, - UrlChoiceMessage, - CalendarChoiceMessage, - ShareLocationChoiceMessage, -) - - -ChoiceOption = Union[ - CallChoiceMessage, - LocationChoiceMessage, - TextChoiceMessage, - UrlChoiceMessage, - CalendarChoiceMessage, - ShareLocationChoiceMessage, -] 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..ae727e69 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/response/types/send_message_response.py @@ -0,0 +1,17 @@ +from datetime import datetime +from typing import Optional +from pydantic import Field, StrictStr +from sinch.domains.conversation.models.v1.messages.internal.base import ( + BaseModelConfiguration, +) + + +class SendMessageResponse(BaseModelConfiguration): + 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 44c3b27e..2bf6844a 100644 --- a/sinch/domains/conversation/models/v1/messages/shared/__init__.py +++ b/sinch/domains/conversation/models/v1/messages/shared/__init__.py @@ -11,15 +11,12 @@ 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, ) -from sinch.domains.conversation.models.v1.messages.shared.list_section import ( - ListSection, -) from sinch.domains.conversation.models.v1.messages.shared.product_item import ( ProductItem, ) @@ -32,9 +29,8 @@ "ChannelIdentity", "ChoiceItem", "ContactMessageCommonProps", - "MessageResponseCommonProps", + "MessageCommonProps", "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/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_common_props.py similarity index 95% 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 08107012..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 @@ -11,11 +11,11 @@ ProcessingModeType, ) from sinch.domains.conversation.models.v1.messages.internal.base import ( - BaseModelConfigurationResponse, + BaseModelConfiguration, ) -class MessageResponseCommonProps(BaseModelConfigurationResponse): +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/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..77e67dc4 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/calendar_message_dict.py @@ -0,0 +1,12 @@ +from datetime import datetime +from typing import TypedDict +from typing_extensions import NotRequired + + +class CalendarMessageDict(TypedDict): + title: 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/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..8bf24db8 --- /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_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.text_message_dict import ( + TextMessageDict, +) + + +class ChoiceMessageDict(TypedDict): + choices: List[ChoiceOptionDict] + text_message: NotRequired[TextMessageDict] + 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/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..e98b0c96 --- /dev/null +++ b/sinch/domains/conversation/models/v1/messages/types/contact_info_message_dict.py @@ -0,0 +1,52 @@ +from datetime import date +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[date] 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/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] 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')