diff --git a/telnyx/api_resources/__init__.py b/telnyx/api_resources/__init__.py index 262512f..78481a6 100644 --- a/telnyx/api_resources/__init__.py +++ b/telnyx/api_resources/__init__.py @@ -18,6 +18,11 @@ from telnyx.api_resources.number_order import NumberOrder from telnyx.api_resources.number_order_phone_number import NumberOrderPhoneNumber from telnyx.api_resources.number_reservation import NumberReservation +from telnyx.api_resources.phone_number import ( + PhoneNumber, + MessagingSettings, + VoiceSettings, +) from telnyx.api_resources.phone_number_regulatory_requirement import ( PhoneNumberRegulatoryRequirement, ) @@ -44,11 +49,14 @@ "Message", "MessagingPhoneNumber", "MessagingProfile", + "MessagingSettings", "NumberOrder", "NumberOrderPhoneNumber", "NumberReservation", + "PhoneNumber", "PhoneNumberRegulatoryRequirement", "PublicKey", "RegulatoryRequirement", "ShortCode", + "VoiceSettings", ] diff --git a/telnyx/api_resources/abstract/nested_resource_class_methods.py b/telnyx/api_resources/abstract/nested_resource_class_methods.py index acf2c41..527459e 100644 --- a/telnyx/api_resources/abstract/nested_resource_class_methods.py +++ b/telnyx/api_resources/abstract/nested_resource_class_methods.py @@ -4,9 +4,17 @@ from telnyx.six.moves.urllib.parse import quote_plus -def nested_resource_class_methods(resource, path=None, operations=None): +def nested_resource_class_methods( + resource, path=None, operations=None, pluralize_path=True +): + def make_path(v): + if pluralize_path: + return "%ss" % v + else: + return v + if path is None: - path = "%ss" % resource + path = make_path(resource) if operations is None: raise ValueError("operations list required") @@ -20,7 +28,7 @@ def nested_resource_url(cls, id, nested_id=None): parts.append(quote_plus(nested_id, safe=util.telnyx_valid_id_parts)) return "/".join(parts) - resource_url_method = "%ss_url" % resource + resource_url_method = "%s_url" % make_path(resource) setattr(cls, resource_url_method, classmethod(nested_resource_url)) def nested_resource_request(cls, method, url, api_key=None, **params): @@ -28,7 +36,7 @@ def nested_resource_request(cls, method, url, api_key=None, **params): response, api_key = requestor.request(method, url, params) return util.convert_to_telnyx_object(response, api_key) - resource_request_method = "%ss_request" % resource + resource_request_method = "%s_request" % make_path(resource) setattr(cls, resource_request_method, classmethod(nested_resource_request)) for operation in operations: @@ -76,7 +84,7 @@ def list_nested_resources(cls, id, **params): url = getattr(cls, resource_url_method)(id) return getattr(cls, resource_request_method)("get", url, **params) - list_method = "list_%ss" % resource + list_method = "list_%s" % make_path(resource) setattr(cls, list_method, classmethod(list_nested_resources)) else: diff --git a/telnyx/api_resources/phone_number.py b/telnyx/api_resources/phone_number.py new file mode 100644 index 0000000..7922a4c --- /dev/null +++ b/telnyx/api_resources/phone_number.py @@ -0,0 +1,128 @@ +from __future__ import absolute_import, division, print_function + +from telnyx import six, util +from telnyx.six.moves.urllib.parse import quote_plus + +from telnyx.api_resources.abstract import ( + CreateableAPIResource, + DeletableAPIResource, + ListableAPIResource, + UpdateableAPIResource, + nested_resource_class_methods, +) + + +@nested_resource_class_methods("voice", operations=["list"], pluralize_path=False) +@nested_resource_class_methods("voice", operations=["update"], pluralize_path=False) +@nested_resource_class_methods( + "enable_emergency", path="actions/enable_emergency", operations=["create"] +) +@nested_resource_class_methods( + "messaging", path="messaging", operations=["list"], pluralize_path=False +) +@nested_resource_class_methods( + "messaging", path="messaging", operations=["update"], pluralize_path=False +) +class PhoneNumber(DeletableAPIResource, ListableAPIResource, UpdateableAPIResource): + OBJECT_NAME = "phone_number" + + @classmethod + def all_voice(cls, **params): + """ Returns the voice settings for /all/ numbers owned by the user. + + This method breaks the naming convention of helper methods by adding + an `all_` prefix, which might be confusing at first. The reason for + this prefix is the `voice()` method name is already taken. The + Telnyx API supports these two endpoints: + + - /v2/phone_numbers/{id}/voice + - /v2/phone_numbers/voice + + The `/{id}/voice` endpoint is taken by the instance method `voice()`. + As we can't nicely re-use this method for two different endpoints, + we have this `all_voice()` endpoint to support the `/voice` endpoint. + """ + + return PhoneNumber.list_voice(None, **params) + + def voice(self, **params): + """ Returns the voice settings for the instantiated phone number. """ + + return PhoneNumber.list_voice(self.id, **params) + + def enable_emergency(self, **params): + return PhoneNumber.create_enable_emergency(self.id, **params) + + @classmethod + def all_messaging(cls, **params): + """ Returns the messaging settings for /all/ numbers owned by the + user. + + See the documentation for `all_voice()` for an explanation on the + difference between `all_messaging()` and `messaging()`. + """ + + return PhoneNumber.list_messaging(None, **params) + + def messaging(self, **params): + """ Returns the messaging settings for the instantiated phone + number. + """ + + return PhoneNumber.list_messaging(self.id, **params) + + +class VoiceSettings(ListableAPIResource, UpdateableAPIResource): + OBJECT_NAME = "voice_settings" + + @classmethod + def class_url(cls): + return "/v2/phone_numbers/voice" + + def instance_url(self): + id = self.get("id") + + if not isinstance(id, six.string_types): + raise error.InvalidRequestError( + "Could not determine which URL to request: %s instance " + "has invalid ID: %r, %s. ID should be of type `str` (or" + " `unicode`)" % (type(self).__name__, id, type(id)), + "id", + ) + + id = util.utf8(id) + extn = quote_plus(id) + return "/v2/phone_numbers/%s/voice" % extn + + @classmethod + def modify(cls, sid, **params): + url = "/v2/phone_numbers/%s/voice" % quote_plus(util.utf8(sid)) + return cls._modify(url, **params) + + +class MessagingSettings(ListableAPIResource, UpdateableAPIResource): + OBJECT_NAME = "messaging_settings" + + @classmethod + def class_url(cls): + return "/v2/phone_numbers/messaging" + + def instance_url(self): + id = self.get("id") + + if not isinstance(id, six.string_types): + raise error.InvalidRequestError( + "Could not determine which URL to request: %s instance " + "has invalid ID: %r, %s. ID should be of type `str` (or" + " `unicode`)" % (type(self).__name__, id, type(id)), + "id", + ) + + id = util.utf8(id) + extn = quote_plus(id) + return "/v2/phone_numbers/%s/messaging" % extn + + @classmethod + def modify(cls, sid, **params): + url = "/v2/phone_numbers/%s/messaging" % quote_plus(util.utf8(sid)) + return cls._modify(url, **params) diff --git a/telnyx/util.py b/telnyx/util.py index ab3014c..e167f70 100644 --- a/telnyx/util.py +++ b/telnyx/util.py @@ -112,13 +112,16 @@ def load_object_classes(): api_resources.Message.OBJECT_NAME: api_resources.Message, api_resources.MessagingPhoneNumber.OBJECT_NAME: api_resources.MessagingPhoneNumber, api_resources.MessagingProfile.OBJECT_NAME: api_resources.MessagingProfile, + api_resources.MessagingSettings.OBJECT_NAME: api_resources.MessagingSettings, api_resources.NumberOrder.OBJECT_NAME: api_resources.NumberOrder, api_resources.NumberOrderPhoneNumber.OBJECT_NAME: api_resources.NumberOrderPhoneNumber, api_resources.NumberReservation.OBJECT_NAME: api_resources.NumberReservation, + api_resources.PhoneNumber.OBJECT_NAME: api_resources.PhoneNumber, api_resources.PhoneNumberRegulatoryRequirement.OBJECT_NAME: api_resources.PhoneNumberRegulatoryRequirement, api_resources.PublicKey.OBJECT_NAME: api_resources.PublicKey, api_resources.RegulatoryRequirement.OBJECT_NAME: api_resources.RegulatoryRequirement, api_resources.ShortCode.OBJECT_NAME: api_resources.ShortCode, + api_resources.VoiceSettings.OBJECT_NAME: api_resources.VoiceSettings, } diff --git a/tests/api_resources/test_phone_number.py b/tests/api_resources/test_phone_number.py new file mode 100644 index 0000000..fa9e5f4 --- /dev/null +++ b/tests/api_resources/test_phone_number.py @@ -0,0 +1,193 @@ +from __future__ import absolute_import, division, print_function + +import telnyx + +TEST_RESOURCE_ID = "1293384261075731499" + + +class TestPhoneNumber(object): + def test_is_listable(self, request_mock): + resources = telnyx.PhoneNumber.list() + request_mock.assert_requested("get", "/v2/phone_numbers") + assert isinstance(resources.data, list) + assert isinstance(resources.data[0], telnyx.PhoneNumber) + + def test_is_retrievable(self, request_mock): + resource = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + request_mock.assert_requested("get", "/v2/phone_numbers/%s" % TEST_RESOURCE_ID) + assert isinstance(resource, telnyx.PhoneNumber) + + def test_is_saveable(self, request_mock): + phone_number = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + phone_number.tags = ["foo", "bar"] + resource = phone_number.save() + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.PhoneNumber) + assert resource is phone_number + + def test_is_modifiable(self, request_mock): + resource = telnyx.PhoneNumber.modify(TEST_RESOURCE_ID, tags=["foo", "bar"]) + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.PhoneNumber) + + def test_is_deletable(self, request_mock): + resource = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + resource.delete() + request_mock.assert_requested( + "delete", "/v2/phone_numbers/%s" % TEST_RESOURCE_ID + ) + + def test_voice_instance_is_retrievable(self, request_mock): + phone_number = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + resource = phone_number.voice() + + request_mock.assert_requested( + "get", "/v2/phone_numbers/%s/voice" % TEST_RESOURCE_ID + ) + + assert isinstance(resource, telnyx.VoiceSettings) + + def test_voice_is_listable(self, request_mock): + resources = telnyx.PhoneNumber.all_voice() + + request_mock.assert_requested("get", "/v2/phone_numbers/voice") + + assert isinstance(resources.data, list) + assert isinstance(resources.data[0], telnyx.VoiceSettings) + + def test_voice_instance_is_saveable(self, request_mock): + phone_number = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + resource = phone_number.voice() + resource.call_forwarding = {} + resource.save() + + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/voice" % TEST_RESOURCE_ID + ) + + def test_voice_is_saveable(self, request_mock): + resource = telnyx.PhoneNumber.all_voice().data[0] + resource.call_forwarding = {} + resource.save() + + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/voice" % resource.id + ) + + def test_enable_emergency(self, request_mock): + resource = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + resource = resource.enable_emergency( + emergency_enabled=True, emergency_address_id="123" + ) + request_mock.assert_requested( + "post", "/v2/phone_numbers/%s/actions/enable_emergency" % TEST_RESOURCE_ID + ) + + def test_messaging_instance_is_retrievable(self, request_mock): + phone_number = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + resource = phone_number.messaging() + + request_mock.assert_requested( + "get", "/v2/phone_numbers/%s/messaging" % TEST_RESOURCE_ID + ) + + assert isinstance(resource, telnyx.MessagingSettings) + + def test_messaging_is_listable(self, request_mock): + resources = telnyx.PhoneNumber.all_messaging() + + request_mock.assert_requested("get", "/v2/phone_numbers/messaging") + + assert isinstance(resources.data, list) + assert isinstance(resources.data[0], telnyx.MessagingSettings) + + def test_messaging_instance_is_saveable(self, request_mock): + phone_number = telnyx.PhoneNumber.retrieve(TEST_RESOURCE_ID) + resource = phone_number.messaging() + resource.call_forwarding = {} + resource.save() + + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/messaging" % TEST_RESOURCE_ID + ) + + def test_messaging_is_saveable(self, request_mock): + resource = telnyx.PhoneNumber.all_messaging().data[0] + resource.call_forwarding = {} + resource.save() + + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/messaging" % resource.id + ) + + +class TestVoiceSettings(object): + def test_is_listable(self, request_mock): + resources = telnyx.VoiceSettings.list() + request_mock.assert_requested("get", "/v2/phone_numbers/voice") + assert isinstance(resources.data, list) + assert isinstance(resources.data[0], telnyx.VoiceSettings) + + def test_is_retrievable(self, request_mock): + resource = telnyx.VoiceSettings.retrieve(TEST_RESOURCE_ID) + request_mock.assert_requested( + "get", "/v2/phone_numbers/%s/voice" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.VoiceSettings) + + def test_is_saveable(self, request_mock): + voice_settings = telnyx.VoiceSettings.retrieve(TEST_RESOURCE_ID) + voice_settings.tech_prefix_enabled = True + resource = voice_settings.save() + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/voice" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.VoiceSettings) + assert resource is voice_settings + + def test_is_modifiable(self, request_mock): + resource = telnyx.VoiceSettings.modify( + TEST_RESOURCE_ID, tech_prefix_enabled=True + ) + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/voice" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.VoiceSettings) + + +class TestMessagingSettings(object): + def test_is_listable(self, request_mock): + resources = telnyx.MessagingSettings.list() + request_mock.assert_requested("get", "/v2/phone_numbers/messaging") + assert isinstance(resources.data, list) + assert isinstance(resources.data[0], telnyx.MessagingSettings) + + def test_is_retrievable(self, request_mock): + resource = telnyx.MessagingSettings.retrieve(TEST_RESOURCE_ID) + request_mock.assert_requested( + "get", "/v2/phone_numbers/%s/messaging" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.MessagingSettings) + + def test_is_saveable(self, request_mock): + messaging_settings = telnyx.MessagingSettings.retrieve(TEST_RESOURCE_ID) + messaging_settings.messaging_profile_id = "123" + resource = messaging_settings.save() + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/messaging" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.MessagingSettings) + assert resource is messaging_settings + + def test_is_modifiable(self, request_mock): + resource = telnyx.MessagingSettings.modify( + TEST_RESOURCE_ID, messaging_profile_id="123" + ) + request_mock.assert_requested( + "patch", "/v2/phone_numbers/%s/messaging" % TEST_RESOURCE_ID + ) + assert isinstance(resource, telnyx.MessagingSettings)