diff --git a/.gitignore b/.gitignore index 43995bd4..9cf67dfe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ # C extensions *.so +# MacOS Files +.DS_Store + # Distribution / packaging .Python env/ diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 2808ce92..198adfc8 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -45,9 +45,9 @@ bandwidth/model/forbidden_request.py bandwidth/model/list_message_direction_enum.py bandwidth/model/list_message_item.py bandwidth/model/lookup_request.py +bandwidth/model/lookup_result.py bandwidth/model/lookup_status.py bandwidth/model/lookup_status_enum.py -bandwidth/model/lookup_status_result_inner.py bandwidth/model/machine_detection_configuration.py bandwidth/model/machine_detection_mode_enum.py bandwidth/model/media.py @@ -118,9 +118,9 @@ docs/ForbiddenRequest.md docs/ListMessageDirectionEnum.md docs/ListMessageItem.md docs/LookupRequest.md +docs/LookupResult.md docs/LookupStatus.md docs/LookupStatusEnum.md -docs/LookupStatusResultInner.md docs/MFAApi.md docs/MachineDetectionConfiguration.md docs/MachineDetectionModeEnum.md @@ -202,9 +202,9 @@ test/test_forbidden_request.py test/test_list_message_direction_enum.py test/test_list_message_item.py test/test_lookup_request.py +test/test_lookup_result.py test/test_lookup_status.py test/test_lookup_status_enum.py -test/test_lookup_status_result_inner.py test/test_machine_detection_configuration.py test/test_machine_detection_mode_enum.py test/test_media.py diff --git a/README.md b/README.md index 00c6d4d6..74295fe8 100644 --- a/README.md +++ b/README.md @@ -207,9 +207,9 @@ Class | Method | HTTP request | Description - [ListMessageDirectionEnum](docs/ListMessageDirectionEnum.md) - [ListMessageItem](docs/ListMessageItem.md) - [LookupRequest](docs/LookupRequest.md) + - [LookupResult](docs/LookupResult.md) - [LookupStatus](docs/LookupStatus.md) - [LookupStatusEnum](docs/LookupStatusEnum.md) - - [LookupStatusResultInner](docs/LookupStatusResultInner.md) - [MachineDetectionConfiguration](docs/MachineDetectionConfiguration.md) - [MachineDetectionModeEnum](docs/MachineDetectionModeEnum.md) - [Media](docs/Media.md) diff --git a/bandwidth.json b/bandwidth.json index b39916b8..8c213b4a 100644 --- a/bandwidth.json +++ b/bandwidth.json @@ -4156,7 +4156,58 @@ "type": "string" }, "status": { - "type": "string" + "$ref": "#/components/schemas/lookupStatusEnum" + } + } + }, + "lookupResult": { + "type": "object", + "description": "Carrier information results for the specified telephone number.", + "properties": { + "Response Code": { + "type": "integer", + "description": "Our vendor's response code.", + "example": 0 + }, + "Message": { + "type": "string", + "description": "Message associated with the response code.", + "example": "NOERROR" + }, + "E.164 Format": { + "type": "string", + "description": "The telephone number in E.164 format.", + "example": "+19195551234" + }, + "Formatted": { + "type": "string", + "description": "The formatted version of the telephone number.", + "example": "(919) 555-1234" + }, + "Country": { + "type": "string", + "description": "The country of the telephone number.", + "example": "US" + }, + "Line Type": { + "type": "string", + "description": "The line type of the telephone number.", + "example": "Mobile" + }, + "Line Provider": { + "type": "string", + "description": "The messaging service provider of the telephone number.", + "example": "Verizon Wireless" + }, + "Mobile Country Code": { + "type": "string", + "description": "The first half of the Home Network Identity (HNI).", + "example": "310" + }, + "Mobile Network Code": { + "type": "string", + "description": "The second half of the HNI.", + "example": "010" } } }, @@ -4176,54 +4227,7 @@ "type": "array", "description": "The carrier information results for the specified telephone number.", "items": { - "type": "object", - "properties": { - "Response Code": { - "type": "integer", - "description": "Our vendor's response code.", - "example": 0 - }, - "Message": { - "type": "string", - "description": "Message associated with the response code.", - "example": "NOERROR" - }, - "E.164 Format": { - "type": "string", - "description": "The telephone number in E.164 format.", - "example": "+19195551234" - }, - "Formatted": { - "type": "string", - "description": "The formatted version of the telephone number.", - "example": "(919) 555-1234" - }, - "Country": { - "type": "string", - "description": "The country of the telephone number.", - "example": "US" - }, - "Line Type": { - "type": "string", - "description": "The line type of the telephone number.", - "example": "Mobile" - }, - "Line Provider": { - "type": "string", - "description": "The messaging service provider of the telephone number.", - "example": "Verizon Wireless" - }, - "Mobile Country Code": { - "type": "string", - "description": "The first half of the Home Network Identity (HNI).", - "example": "310" - }, - "Mobile Network Code": { - "type": "string", - "description": "The second half of the HNI.", - "example": "010" - } - } + "$ref": "#/components/schemas/lookupResult" } }, "failedTelephoneNumbers": { diff --git a/bandwidth/model/create_lookup_response.py b/bandwidth/model/create_lookup_response.py index 280fd24e..24fa20b1 100644 --- a/bandwidth/model/create_lookup_response.py +++ b/bandwidth/model/create_lookup_response.py @@ -30,6 +30,10 @@ from bandwidth.exceptions import ApiAttributeError +def lazy_import(): + from bandwidth.model.lookup_status_enum import LookupStatusEnum + globals()['LookupStatusEnum'] = LookupStatusEnum + class CreateLookupResponse(ModelNormal): """NOTE: This class is auto generated by OpenAPI Generator. @@ -67,6 +71,7 @@ def additional_properties_type(): This must be a method because a model may have properties that are of type self, this must run after the class is loaded """ + lazy_import() return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501 _nullable = False @@ -81,9 +86,10 @@ def openapi_types(): openapi_types (dict): The key is attribute name and the value is attribute type. """ + lazy_import() return { 'request_id': (str,), # noqa: E501 - 'status': (str,), # noqa: E501 + 'status': (LookupStatusEnum,), # noqa: E501 } @cached_property @@ -138,7 +144,7 @@ def _from_openapi_data(cls, *args, **kwargs): # noqa: E501 through its discriminator because we passed in _visited_composed_classes = (Animal,) request_id (str): [optional] # noqa: E501 - status (str): [optional] # noqa: E501 + status (LookupStatusEnum): [optional] # noqa: E501 """ _check_type = kwargs.pop('_check_type', True) @@ -225,7 +231,7 @@ def __init__(self, *args, **kwargs): # noqa: E501 through its discriminator because we passed in _visited_composed_classes = (Animal,) request_id (str): [optional] # noqa: E501 - status (str): [optional] # noqa: E501 + status (LookupStatusEnum): [optional] # noqa: E501 """ _check_type = kwargs.pop('_check_type', True) diff --git a/bandwidth/model/lookup_result.py b/bandwidth/model/lookup_result.py new file mode 100644 index 00000000..9120d444 --- /dev/null +++ b/bandwidth/model/lookup_result.py @@ -0,0 +1,296 @@ +""" + Bandwidth + + Bandwidth's Communication APIs # noqa: E501 + + The version of the OpenAPI document: 1.0.0 + Contact: letstalk@bandwidth.com + Generated by: https://openapi-generator.tech +""" + + +import re # noqa: F401 +import sys # noqa: F401 + +from bandwidth.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel +) +from bandwidth.exceptions import ApiAttributeError + + + +class LookupResult(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = { + } + + validations = { + } + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + return { + 'response_code': (int,), # noqa: E501 + 'message': (str,), # noqa: E501 + 'e_164_format': (str,), # noqa: E501 + 'formatted': (str,), # noqa: E501 + 'country': (str,), # noqa: E501 + 'line_type': (str,), # noqa: E501 + 'line_provider': (str,), # noqa: E501 + 'mobile_country_code': (str,), # noqa: E501 + 'mobile_network_code': (str,), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + + attribute_map = { + 'response_code': 'Response Code', # noqa: E501 + 'message': 'Message', # noqa: E501 + 'e_164_format': 'E.164 Format', # noqa: E501 + 'formatted': 'Formatted', # noqa: E501 + 'country': 'Country', # noqa: E501 + 'line_type': 'Line Type', # noqa: E501 + 'line_provider': 'Line Provider', # noqa: E501 + 'mobile_country_code': 'Mobile Country Code', # noqa: E501 + 'mobile_network_code': 'Mobile Network Code', # noqa: E501 + } + + read_only_vars = { + } + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, *args, **kwargs): # noqa: E501 + """LookupResult - a model defined in OpenAPI + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + response_code (int): Our vendor's response code.. [optional] # noqa: E501 + message (str): Message associated with the response code.. [optional] # noqa: E501 + e_164_format (str): The telephone number in E.164 format.. [optional] # noqa: E501 + formatted (str): The formatted version of the telephone number.. [optional] # noqa: E501 + country (str): The country of the telephone number.. [optional] # noqa: E501 + line_type (str): The line type of the telephone number.. [optional] # noqa: E501 + line_provider (str): The messaging service provider of the telephone number.. [optional] # noqa: E501 + mobile_country_code (str): The first half of the Home Network Identity (HNI).. [optional] # noqa: E501 + mobile_network_code (str): The second half of the HNI.. [optional] # noqa: E501 + """ + + _check_type = kwargs.pop('_check_type', True) + _spec_property_naming = kwargs.pop('_spec_property_naming', True) + _path_to_item = kwargs.pop('_path_to_item', ()) + _configuration = kwargs.pop('_configuration', None) + _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + for arg in args: + if isinstance(arg, dict): + kwargs.update(arg) + else: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + for var_name, var_value in kwargs.items(): + if var_name not in self.attribute_map and \ + self._configuration is not None and \ + self._configuration.discard_unknown_keys and \ + self.additional_properties_type is None: + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = set([ + '_data_store', + '_check_type', + '_spec_property_naming', + '_path_to_item', + '_configuration', + '_visited_composed_classes', + ]) + + @convert_js_args_to_python_args + def __init__(self, *args, **kwargs): # noqa: E501 + """LookupResult - a model defined in OpenAPI + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + response_code (int): Our vendor's response code.. [optional] # noqa: E501 + message (str): Message associated with the response code.. [optional] # noqa: E501 + e_164_format (str): The telephone number in E.164 format.. [optional] # noqa: E501 + formatted (str): The formatted version of the telephone number.. [optional] # noqa: E501 + country (str): The country of the telephone number.. [optional] # noqa: E501 + line_type (str): The line type of the telephone number.. [optional] # noqa: E501 + line_provider (str): The messaging service provider of the telephone number.. [optional] # noqa: E501 + mobile_country_code (str): The first half of the Home Network Identity (HNI).. [optional] # noqa: E501 + mobile_network_code (str): The second half of the HNI.. [optional] # noqa: E501 + """ + + _check_type = kwargs.pop('_check_type', True) + _spec_property_naming = kwargs.pop('_spec_property_naming', False) + _path_to_item = kwargs.pop('_path_to_item', ()) + _configuration = kwargs.pop('_configuration', None) + _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) + + if args: + for arg in args: + if isinstance(arg, dict): + kwargs.update(arg) + else: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + for var_name, var_value in kwargs.items(): + if var_name not in self.attribute_map and \ + self._configuration is not None and \ + self._configuration.discard_unknown_keys and \ + self.additional_properties_type is None: + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + f"class with read only attributes.") diff --git a/bandwidth/model/lookup_status.py b/bandwidth/model/lookup_status.py index 6bdb93e0..f305bf0d 100644 --- a/bandwidth/model/lookup_status.py +++ b/bandwidth/model/lookup_status.py @@ -31,10 +31,10 @@ def lazy_import(): + from bandwidth.model.lookup_result import LookupResult from bandwidth.model.lookup_status_enum import LookupStatusEnum - from bandwidth.model.lookup_status_result_inner import LookupStatusResultInner + globals()['LookupResult'] = LookupResult globals()['LookupStatusEnum'] = LookupStatusEnum - globals()['LookupStatusResultInner'] = LookupStatusResultInner class LookupStatus(ModelNormal): @@ -92,7 +92,7 @@ def openapi_types(): return { 'request_id': (str,), # noqa: E501 'status': (LookupStatusEnum,), # noqa: E501 - 'result': ([LookupStatusResultInner],), # noqa: E501 + 'result': ([LookupResult],), # noqa: E501 'failed_telephone_numbers': ([str],), # noqa: E501 } @@ -151,7 +151,7 @@ def _from_openapi_data(cls, *args, **kwargs): # noqa: E501 _visited_composed_classes = (Animal,) request_id (str): The requestId.. [optional] # noqa: E501 status (LookupStatusEnum): [optional] # noqa: E501 - result ([LookupStatusResultInner]): The carrier information results for the specified telephone number.. [optional] # noqa: E501 + result ([LookupResult]): The carrier information results for the specified telephone number.. [optional] # noqa: E501 failed_telephone_numbers ([str]): The telephone numbers whose lookup failed.. [optional] # noqa: E501 """ @@ -240,7 +240,7 @@ def __init__(self, *args, **kwargs): # noqa: E501 _visited_composed_classes = (Animal,) request_id (str): The requestId.. [optional] # noqa: E501 status (LookupStatusEnum): [optional] # noqa: E501 - result ([LookupStatusResultInner]): The carrier information results for the specified telephone number.. [optional] # noqa: E501 + result ([LookupResult]): The carrier information results for the specified telephone number.. [optional] # noqa: E501 failed_telephone_numbers ([str]): The telephone numbers whose lookup failed.. [optional] # noqa: E501 """ diff --git a/bandwidth/models/__init__.py b/bandwidth/models/__init__.py index b715bcd9..871f08c7 100644 --- a/bandwidth/models/__init__.py +++ b/bandwidth/models/__init__.py @@ -35,9 +35,9 @@ from bandwidth.model.list_message_direction_enum import ListMessageDirectionEnum from bandwidth.model.list_message_item import ListMessageItem from bandwidth.model.lookup_request import LookupRequest +from bandwidth.model.lookup_result import LookupResult from bandwidth.model.lookup_status import LookupStatus from bandwidth.model.lookup_status_enum import LookupStatusEnum -from bandwidth.model.lookup_status_result_inner import LookupStatusResultInner from bandwidth.model.machine_detection_configuration import MachineDetectionConfiguration from bandwidth.model.machine_detection_mode_enum import MachineDetectionModeEnum from bandwidth.model.media import Media diff --git a/docs/CreateLookupResponse.md b/docs/CreateLookupResponse.md index 93388c4a..de12811f 100644 --- a/docs/CreateLookupResponse.md +++ b/docs/CreateLookupResponse.md @@ -6,7 +6,7 @@ The request has been accepted for processing but not yet finished and in a termi Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **request_id** | **str** | | [optional] -**status** | **str** | | [optional] +**status** | [**LookupStatusEnum**](LookupStatusEnum.md) | | [optional] **any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/docs/LookupResult.md b/docs/LookupResult.md new file mode 100644 index 00000000..61b2d0dc --- /dev/null +++ b/docs/LookupResult.md @@ -0,0 +1,21 @@ +# LookupResult + +Carrier information results for the specified telephone number. + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**response_code** | **int** | Our vendor's response code. | [optional] +**message** | **str** | Message associated with the response code. | [optional] +**e_164_format** | **str** | The telephone number in E.164 format. | [optional] +**formatted** | **str** | The formatted version of the telephone number. | [optional] +**country** | **str** | The country of the telephone number. | [optional] +**line_type** | **str** | The line type of the telephone number. | [optional] +**line_provider** | **str** | The messaging service provider of the telephone number. | [optional] +**mobile_country_code** | **str** | The first half of the Home Network Identity (HNI). | [optional] +**mobile_network_code** | **str** | The second half of the HNI. | [optional] +**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/LookupStatus.md b/docs/LookupStatus.md index 6f908ae1..466b9fd5 100644 --- a/docs/LookupStatus.md +++ b/docs/LookupStatus.md @@ -7,7 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **request_id** | **str** | The requestId. | [optional] **status** | [**LookupStatusEnum**](LookupStatusEnum.md) | | [optional] -**result** | [**[LookupStatusResultInner]**](LookupStatusResultInner.md) | The carrier information results for the specified telephone number. | [optional] +**result** | [**[LookupResult]**](LookupResult.md) | The carrier information results for the specified telephone number. | [optional] **failed_telephone_numbers** | **[str]** | The telephone numbers whose lookup failed. | [optional] **any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] diff --git a/test/integration/test_phone_number_lookup.py b/test/integration/test_phone_number_lookup.py new file mode 100644 index 00000000..83e0dda2 --- /dev/null +++ b/test/integration/test_phone_number_lookup.py @@ -0,0 +1,237 @@ +""" +Integration test for Bandwidth's Phone Number Lookup API +""" + +import os +import json +import time +import unittest + +import bandwidth +from bandwidth.api import phone_number_lookup_api +from bandwidth.model.lookup_request import LookupRequest +from bandwidth.model.create_lookup_response import CreateLookupResponse +from bandwidth.model.lookup_status import LookupStatus +from bandwidth.model.lookup_result import LookupResult +from bandwidth.model.lookup_status_enum import LookupStatusEnum +from bandwidth.model.tn_lookup_request_error import TnLookupRequestError +from bandwidth.exceptions import ApiException, UnauthorizedException, ForbiddenException + + +class TestPhoneNumberLookupIntegration(unittest.TestCase): + """Phone Number Lookup API integration test + """ + + def setUp(self) -> None: + configuration = bandwidth.Configuration( + username=os.environ['BW_USERNAME'], + password=os.environ['BW_PASSWORD'] + ) + api_client = bandwidth.ApiClient(configuration) + self.api_instance = phone_number_lookup_api.PhoneNumberLookupApi(api_client) + self.account_id = os.environ['BW_ACCOUNT_ID'] + + def validateResult(self, result: LookupResult, e_164_format: str, line_provider: str) -> None: + """Verify a successful phone number lookup LookupResult object + + Args: + result (LookupResult): Result derived from LookupStatus result list + e_164_format (str): Phone number in e164 format ex: +19195551234 + line_provider (str): Line service provider ex: Verizon + """ + self.assertEqual(result.response_code, 0) + self.assertIs(type(result.message), str) + self.assertEqual(result.e_164_format, e_164_format) + self.assertIs(type(result.formatted), str) + self.assertTrue(result.country == "US" or result.country == "Canada") + self.assertTrue(result.line_type == "Mobile" or result.line_type == "Fixed") + self.assertIn(line_provider, result.line_provider) + + # if result has 1 of these attributes it should have the other + if result.get('mobile_country_code') or result.get('mobile_network_code'): + self.assertIs(type(result.mobile_country_code), str) + self.assertIs(type(result.mobile_network_code), str) + + def pollLookupStatus(self, request_id: str) -> LookupStatus: + """Poll LookupRequest for 'COMPLETE' status + + Args: + request_id (str): LookupResult.request_id value to query + + Raises: + Exception: Tries 5 times and raises a general exception if the query takes more than 5 attempts to minimize run time. + + Returns: + LookupStatus: LookupStatus in 'COMPLETE' state + """ + get_lookup_status_response: LookupStatus = self.api_instance.get_lookup_status( + self.account_id, request_id) + get_lookup_status_response_attempts = 1 + while get_lookup_status_response.status != LookupStatusEnum('COMPLETE'): + # Raise an error if it takes more than 5 requests to get COMPLETE status + if get_lookup_status_response_attempts == 5: + raise Exception( + f'Took too long to get phone number lookup \'COMPLETE\' status. Aborting test after {get_lookup_status_response_attempts} attempts.') + time.sleep(2) + + get_lookup_status_response: LookupStatus = self.api_instance.get_lookup_status( + self.account_id, request_id) + get_lookup_status_response_attempts += 1 + + return get_lookup_status_response + + def assertAuthException(self, context: ApiException, expectedException: ApiException, expected_status_code: int) -> None: + """Validates that an auth exception (401 or 403) is properly formatted + + Args: + context (ApiException): Exception to validate + expectedException (ApiException): Expected exception type + expected_status_code (int): Expected status code + """ + self.assertIs(type(context.exception), expectedException) + self.assertIs(type(context.exception.status), int) + self.assertEqual(context.exception.status, expected_status_code) + self.assertIs(type(context.exception.body), str) + + def testSuccessfulPhoneNumberLookup(self) -> None: + """Test Phone Number Lookup API + """ + lookup_request = LookupRequest( + tns=[ + os.environ['BW_NUMBER'], + os.environ['VZW_NUMBER'], + os.environ['ATT_NUMBER'], + os.environ['T_MOBILE_NUMBER'], + # os.environ['BW_INVALID_TN_LOOKUP_NUMBER'] + ], + ) + + # Create the lookup request and validate the response + create_lookup_response: CreateLookupResponse = self.api_instance.create_lookup( + self.account_id, lookup_request, _return_http_data_only=False) + self.assertEqual(create_lookup_response[1], 202) + self.assertIs(type(create_lookup_response[0].status), LookupStatusEnum) + self.assertEqual(create_lookup_response[0].status, LookupStatusEnum("IN_PROGRESS")) + self.assertIs(type(create_lookup_response[0].request_id), str) + self.assertRegex(create_lookup_response[0].request_id, + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') + + # Check the status code for the GET LookupStatus response + get_lookup_status_response: LookupStatus = self.api_instance.get_lookup_status( + self.account_id, create_lookup_response[0].request_id, _return_http_data_only=False) + self.assertEqual(get_lookup_status_response[1], 200) + + # make the request again without _return_http_data_only=False to just get the LookupStatus model + get_lookup_status_response: LookupStatus = self.pollLookupStatus( + create_lookup_response[0].request_id) + + self.assertEqual(get_lookup_status_response.request_id, + create_lookup_response[0].request_id) + self.assertIs(type(get_lookup_status_response), LookupStatus) + + # Assert that each result is of type LookupResult + for i in range(len(get_lookup_status_response.result)): + self.assertIs(type(get_lookup_status_response.result[i]), LookupResult) + + # Check the information for a Bandwidth TN + bw_lookup_result: LookupResult = get_lookup_status_response.result[0] + self.validateResult(bw_lookup_result, os.environ['BW_NUMBER'], "Bandwidth") + + # Check the information for a Verizon TN + vzw_lookup_result = get_lookup_status_response.result[1] + self.validateResult(vzw_lookup_result, os.environ['VZW_NUMBER'], "Verizon") + + # Check the information for an AT&T TN + att_lookup_result = get_lookup_status_response.result[2] + self.validateResult(att_lookup_result, os.environ['ATT_NUMBER'], "AT&T") + + # Check the information for a T-Mobile TN + t_mobile_lookup_result = get_lookup_status_response.result[3] + self.validateResult(t_mobile_lookup_result, os.environ['T_MOBILE_NUMBER'], "T-Mobile") + + # The only way to get a failed number is if the api call to the downstream service fails - so there is no way to force this in our testing currently + # check the failed_telephone_number list + # self.assertIs(type(get_lookup_status_response.failed_telephone_numbers), list) + # self.assertIn(os.environ['BW_INVALID_TN_LOOKUP_NUMBER'], get_lookup_status_response.failed_telephone_numbers) + + def testFailedPhoneNumberLookup(self) -> None: + """Test Phone Number Lookup API with bad data to force an error + """ + with self.assertRaises(ApiException) as context: + lookup_request = LookupRequest( + tns=[ + 'not a number', + ], + ) + self.api_instance.create_lookup(self.account_id, lookup_request) + + self.assertIs(type(context.exception.status), int) + self.assertIs(type(context.exception.body), str) + + # initialize TnLookupRequestError model + error = TnLookupRequestError(message=(json.loads(context.exception.body))['message']) + self.assertIs(type(error), TnLookupRequestError) + + def testDuplicatePhoneNumberLookup(self) -> None: + """Test a request with a duplicate number. Should throw a 400 Bad Request error. + """ + with self.assertRaises(ApiException) as context: + lookup_request = LookupRequest( + tns=[ + os.environ['BW_NUMBER'], + os.environ['BW_NUMBER'] + ], + ) + self.api_instance.create_lookup(self.account_id, lookup_request) + + self.assertIs(type(context.exception.status), int) + self.assertEqual(context.exception.status, 400) + self.assertIs(type(context.exception.body), str) + + def testUnauthorizedRequest(self) -> None: + """Validate an unauthorized (401) request + """ + configuration = bandwidth.Configuration( + username='bad_username', + password='bad_password' + ) + unauthorized_api_client = bandwidth.ApiClient(configuration) + unauthorized_api_instance = phone_number_lookup_api.PhoneNumberLookupApi( + unauthorized_api_client) + lookup_request = LookupRequest( + tns=[ + os.environ['BW_NUMBER'] + ], + ) + + with self.assertRaises(UnauthorizedException) as context: + unauthorized_api_instance.create_lookup(self.account_id, lookup_request) + + self.assertAuthException(context, UnauthorizedException, 401) + + def testForbiddenRequest(self) -> None: + """Validate a forbidden (403) request + """ + configuration = bandwidth.Configuration( + username=os.environ['BW_USERNAME_FORBIDDEN'], + password=os.environ['BW_PASSWORD_FORBIDDEN'] + ) + forbidden_api_client = bandwidth.ApiClient(configuration) + forbidden_api_instance = phone_number_lookup_api.PhoneNumberLookupApi(forbidden_api_client) + lookup_request = LookupRequest( + tns=[ + os.environ['BW_NUMBER'] + ], + ) + + # This API throws a 401 when a user provides valid credentials with the `TN Lookup` role disabled + # with self.assertRaises(ForbiddenException) as context: + with self.assertRaises(UnauthorizedException) as context: + forbidden_api_instance.create_lookup(self.account_id, lookup_request) + + # self.validateAuthException(context, ForbiddenException, 403) + self.assertAuthException(context, UnauthorizedException, 401) + + +if __name__ == '__main__': + unittest.main()