diff --git a/http_client/src/vonage_http_client/http_client.py b/http_client/src/vonage_http_client/http_client.py index 1f781d3c..c8df1430 100644 --- a/http_client/src/vonage_http_client/http_client.py +++ b/http_client/src/vonage_http_client/http_client.py @@ -225,8 +225,6 @@ def _parse_response(self, response: Response) -> Union[dict, None]: ) self._last_response = response if 200 <= response.status_code < 300: - if response.status_code == 204: - return None try: return response.json() except JSONDecodeError: diff --git a/messages/README.md b/messages/README.md index 5c3d16ab..e25c0985 100644 --- a/messages/README.md +++ b/messages/README.md @@ -6,5 +6,72 @@ This package contains the code to use [Vonage's Messages API](https://developer. It is recommended to use this as part of the main `vonage` package. The examples below assume you've created an instance of the `vonage.Vonage` class called `vonage_client`. +### How to Construct a Message + +In order to send a message, you must construct a message object of the correct type. These are all found under `vonage_messages.models`. + +```python +from vonage_messages.models import Sms + +message = Sms( + from_='Vonage APIs', + to='1234567890', + text='This is a test message sent from the Vonage Python SDK', +) +``` + +This message can now be sent with + +```python +vonage_client.messages.send(message) +``` + +All possible message types from every message channel have their own message model. They are named following this rule: {Channel}{MessageType}, e.g. `Sms`, `MmsImage`, `MessengerAudio`, `WhatsappSticker`, `ViberVideo`, etc. + +The different message models are listed at the bottom of the page. + +Some message types have submodels with additional fields. In this case, import the submodels as well and use them to construct the overall options. + +e.g. + +```python +from vonage_messages import MessengerImage, MessengerOptions, MessengerResource + +messenger = MessengerImage( + to='1234567890', + from_='1234567890', + image=MessengerResource(url='https://example.com/image.jpg'), + messenger=MessengerOptions(category='message_tag', tag='invalid_tag'), +) +``` + ### Send a message +To send a message, access the `Messages.send` method via the main Vonage object, passing in an instance of a subclass of `BaseMessage` like this: + +```python +from vonage import Auth, Vonage +from vonage_messages.models import Sms + +vonage_client = Vonage(Auth(application_id='my-application-id', private_key='my-private-key')) + +message = Sms( + from_='Vonage APIs', + to='1234567890', + text='This is a test message sent from the Vonage Python SDK', +) + +vonage_client.messages.send(message) +``` + +## Message Models + +To send a message, instantiate a message model of the correct type as described above. This is a list of message models that can be used: + +``` +Sms +MmsImage, MmsVcard, MmsAudio, MmsVideo +WhatsappText, WhatsappImage, WhatsappAudio, WhatsappVideo, WhatsappFile, WhatsappTemplate, WhatsappSticker, WhatsappCustom +MessengerText, MessengerImage, MessengerAudio, MessengerVideo, MessengerFile +ViberText, ViberImage, ViberVideo, ViberFile +``` \ No newline at end of file diff --git a/messages/src/vonage_messages/__init__.py b/messages/src/vonage_messages/__init__.py index e1aa8216..11000717 100644 --- a/messages/src/vonage_messages/__init__.py +++ b/messages/src/vonage_messages/__init__.py @@ -1,12 +1,5 @@ from . import models -from .enums import ChannelType, EncodingType, MessageType, WebhookVersion from .messages import Messages +from .responses import SendMessageResponse -__all__ = [ - 'models', - 'Messages', - 'ChannelType', - 'MessageType', - 'WebhookVersion', - 'EncodingType', -] +__all__ = ['models', 'Messages', 'SendMessageResponse'] diff --git a/messages/src/vonage_messages/messages.py b/messages/src/vonage_messages/messages.py index 98ce25c0..e1d32d35 100644 --- a/messages/src/vonage_messages/messages.py +++ b/messages/src/vonage_messages/messages.py @@ -2,7 +2,7 @@ from vonage_http_client.http_client import HttpClient from .models import BaseMessage -from .responses import MessageUuid +from .responses import SendMessageResponse class Messages: @@ -18,19 +18,20 @@ def __init__(self, http_client: HttpClient) -> None: self._http_client = http_client @validate_call - def send(self, message: BaseMessage) -> MessageUuid: + def send(self, message: BaseMessage) -> SendMessageResponse: """Send a message using Vonage's Messages API. Args: - message (BaseMessage): The message to be sent. + message (BaseMessage): The message to be sent as a Pydantic model. + Use the provided models (in `vonage_messages.models`) to create messages and pass them in to this method. Returns: - MessageUuid: The unique identifier of the sent message. + SendMessageResponse: Response model containing the unique identifier of the sent message. + Access the identifier with the `message_uuid` attribute. """ response = self._http_client.post( self._http_client.api_host, '/v1/messages', - message.model_dump(by_alias=True, exclude_none=True), + message.model_dump(by_alias=True, exclude_none=True) or message, ) - - return MessageUuid(**response) + return SendMessageResponse(**response) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 75a192be..92b5b84c 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -1,4 +1,5 @@ from .base_message import BaseMessage +from .enums import ChannelType, EncodingType, MessageType, WebhookVersion from .messenger import ( MessengerAudio, MessengerFile, @@ -46,6 +47,9 @@ __all__ = [ 'BaseMessage', + 'ChannelType', + 'EncodingType', + 'MessageType', 'MessengerAudio', 'MessengerFile', 'MessengerImage', @@ -72,6 +76,7 @@ 'ViberVideo', 'ViberVideoOptions', 'ViberVideoResource', + 'WebhookVersion', 'WhatsappAudio', 'WhatsappAudioResource', 'WhatsappContext', diff --git a/messages/src/vonage_messages/models/base_message.py b/messages/src/vonage_messages/models/base_message.py index 8c33e7f9..ba59d0a0 100644 --- a/messages/src/vonage_messages/models/base_message.py +++ b/messages/src/vonage_messages/models/base_message.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field from vonage_utils.types.phone_number import PhoneNumber -from ..enums import WebhookVersion +from .enums import WebhookVersion class BaseMessage(BaseModel): diff --git a/messages/src/vonage_messages/enums.py b/messages/src/vonage_messages/models/enums.py similarity index 100% rename from messages/src/vonage_messages/enums.py rename to messages/src/vonage_messages/models/enums.py diff --git a/messages/src/vonage_messages/models/messenger.py b/messages/src/vonage_messages/models/messenger.py index 465d1d3d..abb5bc9d 100644 --- a/messages/src/vonage_messages/models/messenger.py +++ b/messages/src/vonage_messages/models/messenger.py @@ -2,8 +2,8 @@ from pydantic import BaseModel, Field, model_validator -from ..enums import ChannelType, MessageType from .base_message import BaseMessage +from .enums import ChannelType, MessageType class MessengerResource(BaseModel): diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index b72c5ac8..dc252c42 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -3,8 +3,8 @@ from pydantic import BaseModel, Field from vonage_utils.types.phone_number import PhoneNumber -from ..enums import ChannelType, MessageType from .base_message import BaseMessage +from .enums import ChannelType, MessageType class MmsResource(BaseModel): diff --git a/messages/src/vonage_messages/models/sms.py b/messages/src/vonage_messages/models/sms.py index 01bc453a..56e89901 100644 --- a/messages/src/vonage_messages/models/sms.py +++ b/messages/src/vonage_messages/models/sms.py @@ -3,8 +3,8 @@ from pydantic import BaseModel, Field from vonage_utils.types.phone_number import PhoneNumber -from ..enums import ChannelType, EncodingType, MessageType from .base_message import BaseMessage +from .enums import ChannelType, EncodingType, MessageType class SmsOptions(BaseModel): diff --git a/messages/src/vonage_messages/models/viber.py b/messages/src/vonage_messages/models/viber.py index 49239bc5..b135f614 100644 --- a/messages/src/vonage_messages/models/viber.py +++ b/messages/src/vonage_messages/models/viber.py @@ -2,8 +2,8 @@ from pydantic import BaseModel, Field, field_validator -from ..enums import ChannelType, MessageType from .base_message import BaseMessage +from .enums import ChannelType, MessageType class ViberAction(BaseModel): diff --git a/messages/src/vonage_messages/models/whatsapp.py b/messages/src/vonage_messages/models/whatsapp.py index b94dd75d..01c12fe5 100644 --- a/messages/src/vonage_messages/models/whatsapp.py +++ b/messages/src/vonage_messages/models/whatsapp.py @@ -3,8 +3,8 @@ from pydantic import BaseModel, ConfigDict, Field from vonage_utils.types.phone_number import PhoneNumber -from ..enums import ChannelType, MessageType from .base_message import BaseMessage +from .enums import ChannelType, MessageType class WhatsappContext(BaseModel): diff --git a/messages/src/vonage_messages/responses.py b/messages/src/vonage_messages/responses.py index 06a3eace..59a84b50 100644 --- a/messages/src/vonage_messages/responses.py +++ b/messages/src/vonage_messages/responses.py @@ -1,7 +1,11 @@ from pydantic import BaseModel -class MessageUuid(BaseModel): - """Response from Vonage's Messages API.""" +class SendMessageResponse(BaseModel): + """Response from Vonage's Messages API. + + Attributes: + message_uuid (str): The UUID of the sent message. + """ message_uuid: str diff --git a/messages/tests/_test_verify_v2.py b/messages/tests/_test_verify_v2.py deleted file mode 100644 index c6e72a98..00000000 --- a/messages/tests/_test_verify_v2.py +++ /dev/null @@ -1,66 +0,0 @@ -from os.path import abspath - -import responses -from pytest import raises -from vonage_http_client.errors import HttpRequestError -from vonage_http_client.http_client import HttpClient -from vonage_verify_v2.requests import * -from vonage_verify_v2.verify_v2 import VerifyV2 - -from testutils import build_response, get_mock_jwt_auth - -path = abspath(__file__) - - -verify = VerifyV2(HttpClient(get_mock_jwt_auth())) - - -@responses.activate -def test_make_verify_request(): - build_response( - path, 'POST', 'https://api.nexmo.com/v2/verify', 'verify_request.json', 202 - ) - silent_auth_channel = SilentAuthChannel( - channel=ChannelType.SILENT_AUTH, to='1234567890' - ) - sms_channel = SmsChannel(channel=ChannelType.SMS, to='1234567890', from_='Vonage') - params = { - 'brand': 'Vonage', - 'workflow': [silent_auth_channel, sms_channel], - } - request = VerifyRequest(**params) - - response = verify.start_verification(request) - assert response.request_id == '2c59e3f4-a047-499f-a14f-819cd1989d2e' - assert ( - response.check_url - == 'https://api-eu-3.vonage.com/v2/verify/cfbc9a3b-27a2-40d4-a4e0-0c59b3b41901/silent-auth/redirect' - ) - assert verify._http_client.last_response.status_code == 202 - - -@responses.activate -def test_verify_request_concurrent_verifications_error(): - build_response( - path, - 'POST', - 'https://api.nexmo.com/v2/verify', - 'verify_request_error.json', - 409, - ) - sms_channel = SmsChannel(channel=ChannelType.SMS, to='1234567890', from_='Vonage') - params = { - 'brand': 'Vonage', - 'workflow': [sms_channel], - } - request = VerifyRequest(**params) - - with raises(HttpRequestError) as e: - verify.start_verification(request) - - assert e.value.response.status_code == 409 - assert e.value.response.json()['title'] == 'Conflict' - assert ( - e.value.response.json()['detail'] - == 'Concurrent verifications to the same number are not allowed' - ) diff --git a/messages/tests/data/invalid_error.json b/messages/tests/data/invalid_error.json new file mode 100644 index 00000000..a59ca83e --- /dev/null +++ b/messages/tests/data/invalid_error.json @@ -0,0 +1,12 @@ +{ + "type": "https://developer.vonage.com/api-errors/messages#1150", + "title": "Invalid params", + "detail": "The value of one or more parameters is invalid.", + "instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf", + "invalid_parameters": [ + { + "name": "messenger.tag", + "reason": "invalid value" + } + ] +} \ No newline at end of file diff --git a/messages/tests/data/low_balance_error.json b/messages/tests/data/low_balance_error.json new file mode 100644 index 00000000..fb1fbde3 --- /dev/null +++ b/messages/tests/data/low_balance_error.json @@ -0,0 +1,6 @@ +{ + "type": "https://developer.nexmo.com/api-errors/#low-balance", + "title": "Low balance", + "detail": "This request could not be performed due to your account balance being low.", + "instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf" +} \ No newline at end of file diff --git a/messages/tests/data/send_message.json b/messages/tests/data/send_message.json new file mode 100644 index 00000000..b584d2db --- /dev/null +++ b/messages/tests/data/send_message.json @@ -0,0 +1,3 @@ +{ + "message_uuid": "d8f86df1-dec6-442f-870a-2241be27d721" +} \ No newline at end of file diff --git a/messages/tests/test_messages.py b/messages/tests/test_messages.py new file mode 100644 index 00000000..a57ca024 --- /dev/null +++ b/messages/tests/test_messages.py @@ -0,0 +1,77 @@ +from os.path import abspath + +import responses +from pytest import raises +from vonage_http_client.errors import HttpRequestError +from vonage_http_client.http_client import HttpClient +from vonage_messages.messages import Messages +from vonage_messages.models import Sms +from vonage_messages.models.messenger import ( + MessengerImage, + MessengerOptions, + MessengerResource, +) +from vonage_messages.responses import SendMessageResponse + +from testutils import build_response, get_mock_jwt_auth + +path = abspath(__file__) + + +messages = Messages(HttpClient(get_mock_jwt_auth())) + + +@responses.activate +def test_send_message(): + build_response( + path, 'POST', 'https://api.nexmo.com/v1/messages', 'send_message.json', 202 + ) + sms = Sms( + from_='Vonage APIs', + to='1234567890', + text='Hello, World!', + ) + response = messages.send(sms) + assert type(response) == SendMessageResponse + assert response.message_uuid == 'd8f86df1-dec6-442f-870a-2241be27d721' + + +@responses.activate +def test_send_message_low_balance_error(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/v1/messages', + 'low_balance_error.json', + 402, + ) + + with raises(HttpRequestError) as e: + messages.send(Sms(from_='Vonage APIs', to='1234567890', text='Hello, World!')) + + assert e.value.response.status_code == 402 + assert e.value.response.json()['title'] == 'Low balance' + + +@responses.activate +def test_send_message_invalid_error(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/v1/messages', + 'invalid_error.json', + 422, + ) + + messenger = MessengerImage( + to='1234567890', + from_='1234567890', + image=MessengerResource(url='https://example.com/image.jpg'), + messenger=MessengerOptions(category='message_tag', tag='invalid_tag'), + ) + + with raises(HttpRequestError) as e: + messages.send(messenger) + + assert e.value.response.status_code == 422 + assert e.value.response.json()['title'] == 'Invalid params' diff --git a/messages/tests/test_messenger_models.py b/messages/tests/test_messenger_models.py index e3dc141d..f34ef099 100644 --- a/messages/tests/test_messenger_models.py +++ b/messages/tests/test_messenger_models.py @@ -1,5 +1,4 @@ from pytest import raises -from vonage_messages.enums import WebhookVersion from vonage_messages.models import ( MessengerAudio, MessengerFile, @@ -9,6 +8,7 @@ MessengerText, MessengerVideo, ) +from vonage_messages.models.enums import WebhookVersion def test_messenger_options_validator(): diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index 4bf75979..c74d6994 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -1,5 +1,5 @@ -from vonage_messages.enums import WebhookVersion from vonage_messages.models import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo +from vonage_messages.models.enums import WebhookVersion def test_create_mms_image(): diff --git a/messages/tests/test_sms_models.py b/messages/tests/test_sms_models.py index ce5012fe..49b19771 100644 --- a/messages/tests/test_sms_models.py +++ b/messages/tests/test_sms_models.py @@ -1,5 +1,5 @@ -from vonage_messages.enums import EncodingType, WebhookVersion from vonage_messages.models import Sms, SmsOptions +from vonage_messages.models.enums import EncodingType, WebhookVersion def test_create_sms(): diff --git a/messages/tests/test_viber_models.py b/messages/tests/test_viber_models.py index f2fa7476..21373261 100644 --- a/messages/tests/test_viber_models.py +++ b/messages/tests/test_viber_models.py @@ -1,5 +1,4 @@ from pytest import raises -from vonage_messages.enums import WebhookVersion from vonage_messages.models import ( ViberAction, ViberFile, @@ -14,6 +13,7 @@ ViberVideoOptions, ViberVideoResource, ) +from vonage_messages.models.enums import WebhookVersion def test_viber_video_options_validator(): diff --git a/messages/tests/test_whatsapp_models.py b/messages/tests/test_whatsapp_models.py index 6a47c5c5..6967d9dc 100644 --- a/messages/tests/test_whatsapp_models.py +++ b/messages/tests/test_whatsapp_models.py @@ -1,6 +1,5 @@ from copy import deepcopy -from vonage_messages.enums import WebhookVersion from vonage_messages.models import ( WhatsappAudio, WhatsappAudioResource, @@ -20,6 +19,7 @@ WhatsappVideo, WhatsappVideoResource, ) +from vonage_messages.models.enums import WebhookVersion def test_whatsapp_text(): diff --git a/users/src/vonage_users/common.py b/users/src/vonage_users/common.py index 763a7b9b..a7b8dfd7 100644 --- a/users/src/vonage_users/common.py +++ b/users/src/vonage_users/common.py @@ -81,8 +81,7 @@ class User(BaseModel): id: Optional[str] = None @model_validator(mode='after') - @classmethod - def get_link(cls, data): - if data.links is not None: - data.link = data.links.self.href - return data + def get_link(self): + if self.links is not None: + self.link = self.links.self.href + return self diff --git a/vonage/CHANGES.md b/vonage/CHANGES.md index ddbf1d44..eef1c130 100644 --- a/vonage/CHANGES.md +++ b/vonage/CHANGES.md @@ -1,3 +1,6 @@ +# 3.99.0a6 +- Add support for the [Vonage Messages API](https://developer.vonage.com/en/messages/overview). + # 3.99.0a5 - Add support for the [Vonage Verify V2 API](https://developer.vonage.com/en/verify/overview). - Expose error classes at the top level of the `vonage-http-client` package. diff --git a/vonage/pyproject.toml b/vonage/pyproject.toml index f024e28a..06f1059b 100644 --- a/vonage/pyproject.toml +++ b/vonage/pyproject.toml @@ -8,12 +8,12 @@ requires-python = ">=3.8" dependencies = [ "vonage-utils>=1.0.1", "vonage-http-client>=1.2.1", + "vonage-messages>=1.0.0", "vonage-number-insight-v2>=0.1.0", "vonage-sms>=1.0.2", "vonage-users>=1.0.1", "vonage-verify>=1.0.1", "vonage-verify-v2>=1.0.0", - "vonage-messages>=1.0.0", ] classifiers = [ "Programming Language :: Python", diff --git a/vonage/src/vonage/_version.py b/vonage/src/vonage/_version.py index 29defcc4..e04178b8 100644 --- a/vonage/src/vonage/_version.py +++ b/vonage/src/vonage/_version.py @@ -1 +1 @@ -__version__ = '3.99.0a5' +__version__ = '3.99.0a6'