From 61718d653d7d58ba3eebf3e085d493bba7f5035c Mon Sep 17 00:00:00 2001 From: maxkahan Date: Fri, 26 Apr 2024 00:34:21 +0100 Subject: [PATCH] finish NI module --- number_insight/README.md | 4 +- .../src/vonage_number_insight/__init__.py | 34 +++++- .../vonage_number_insight/number_insight.py | 14 +++ .../src/vonage_number_insight/responses.py | 60 +++++++++- .../tests/data/advanced_async_insight.json | 7 ++ .../data/advanced_async_insight_error.json | 4 + .../advanced_async_insight_partial_error.json | 8 ++ .../tests/data/advanced_sync_insight.json | 44 +++++++ .../tests/data/standard_insight.json | 36 ++++++ number_insight/tests/test_number_insight.py | 112 +++++++++++++++++- vonage/CHANGES.md | 3 + vonage/pyproject.toml | 1 + vonage/src/vonage/_version.py | 2 +- 13 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 number_insight/tests/data/advanced_async_insight.json create mode 100644 number_insight/tests/data/advanced_async_insight_error.json create mode 100644 number_insight/tests/data/advanced_async_insight_partial_error.json create mode 100644 number_insight/tests/data/advanced_sync_insight.json create mode 100644 number_insight/tests/data/standard_insight.json diff --git a/number_insight/README.md b/number_insight/README.md index 16d5325a..91479b01 100644 --- a/number_insight/README.md +++ b/number_insight/README.md @@ -1,6 +1,8 @@ # Vonage Number Insight Package -This package contains the code to use [Vonage's Number Insight API](https://developer.vonage.com/en/number-insight/overview) in Python. This package includes methods to get information about phone numbers. It has 3 levels of insight, basic, standard, and advanced. +This package contains the code to use [Vonage's Number Insight API](https://developer.vonage.com/en/number-insight/overview) in Python. This package includes methods to get information about phone numbers. It has 3 levels of insight: basic, standard, and advanced. + +The advanced insight can be obtained synchronously or asynchronously. An async approach is recommended to avoid timeouts. Optionally, you can get caller name information (additional charge) by passing the `cnam` parameter to a standard or advanced insight request. ## Usage diff --git a/number_insight/src/vonage_number_insight/__init__.py b/number_insight/src/vonage_number_insight/__init__.py index b791210a..83dc11c3 100644 --- a/number_insight/src/vonage_number_insight/__init__.py +++ b/number_insight/src/vonage_number_insight/__init__.py @@ -1,3 +1,35 @@ +from . import errors from .number_insight import NumberInsight +from .requests import ( + AdvancedAsyncInsightRequest, + AdvancedSyncInsightRequest, + BasicInsightRequest, + StandardInsightRequest, +) +from .responses import ( + AdvancedAsyncInsightResponse, + AdvancedSyncInsightResponse, + BasicInsightResponse, + CallerIdentity, + Carrier, + RealTimeData, + RoamingStatus, + StandardInsightResponse, +) -__all__ = ['NumberInsight'] +__all__ = [ + 'NumberInsight', + 'BasicInsightRequest', + 'StandardInsightRequest', + 'AdvancedAsyncInsightRequest', + 'AdvancedSyncInsightRequest', + 'BasicInsightResponse', + 'CallerIdentity', + 'Carrier', + 'RealTimeData', + 'RoamingStatus', + 'StandardInsightResponse', + 'AdvancedSyncInsightResponse', + 'AdvancedAsyncInsightResponse', + 'errors', +] diff --git a/number_insight/src/vonage_number_insight/number_insight.py b/number_insight/src/vonage_number_insight/number_insight.py index 4176b147..bd00d11e 100644 --- a/number_insight/src/vonage_number_insight/number_insight.py +++ b/number_insight/src/vonage_number_insight/number_insight.py @@ -1,3 +1,5 @@ +from logging import getLogger + from pydantic import validate_call from vonage_http_client.http_client import HttpClient @@ -15,6 +17,8 @@ StandardInsightResponse, ) +logger = getLogger('vonage_number_insight') + class NumberInsight: """Calls Vonage's Number Insight API.""" @@ -99,6 +103,7 @@ def advanced_async_number_insight( params=options.model_dump(exclude_none=True), auth_type=self._auth_type, ) + self._check_for_error(response) return AdvancedAsyncInsightResponse(**response) @@ -122,6 +127,7 @@ def advanced_sync_number_insight( params=options.model_dump(exclude_none=True), auth_type=self._auth_type, ) + self._check_for_error(response) return AdvancedSyncInsightResponse(**response) @@ -135,5 +141,13 @@ def _check_for_error(self, response: dict) -> None: NumberInsightError: If the response contains an error. """ if response['status'] != 0: + if response['status'] in {43, 44, 45}: + logger.warning( + 'Live mobile lookup not returned. Not all parameters are available.' + ) + return + logger.warning( + f'Error using the Number Insight API. Response received: {response}' + ) error_message = f'Error with the following details: {response}' raise NumberInsightError(error_message) diff --git a/number_insight/src/vonage_number_insight/responses.py b/number_insight/src/vonage_number_insight/responses.py index 367ea097..6fdea775 100644 --- a/number_insight/src/vonage_number_insight/responses.py +++ b/number_insight/src/vonage_number_insight/responses.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Literal, Optional, Union from pydantic import BaseModel @@ -15,13 +15,61 @@ class BasicInsightResponse(BaseModel): country_prefix: Optional[str] = None +class Carrier(BaseModel): + network_code: Optional[str] = None + name: Optional[str] = None + country: Optional[str] = None + network_type: Optional[str] = None + + +class CallerIdentity(BaseModel): + caller_type: Optional[str] = None + caller_name: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + subscription_type: Optional[str] = None + + class StandardInsightResponse(BasicInsightResponse): - ... + request_price: Optional[str] = None + refund_price: Optional[str] = None + remaining_balance: Optional[str] = None + current_carrier: Optional[Carrier] = None + original_carrier: Optional[Carrier] = None + ported: Optional[str] = None + caller_identity: Optional[CallerIdentity] = None + caller_type: Optional[str] = None + caller_name: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None -class AdvancedAsyncInsightResponse(BaseModel): - ... +class RoamingStatus(BaseModel): + status: Optional[str] = None + roaming_country_code: Optional[str] = None + roaming_network_code: Optional[str] = None + roaming_network_name: Optional[str] = None + +class RealTimeData(BaseModel): + active_status: Optional[str] = None + handset_status: Optional[str] = None -class AdvancedSyncInsightResponse(BaseModel): - ... + +class AdvancedSyncInsightResponse(StandardInsightResponse): + roaming: Optional[Union[RoamingStatus, Literal['unknown']]] = None + lookup_outcome: Optional[int] = None + lookup_outcome_message: Optional[str] = None + valid_number: Optional[str] = None + reachable: Optional[str] = None + real_time_data: Optional[RealTimeData] = None + ip_warnings: Optional[str] = None + + +class AdvancedAsyncInsightResponse(BaseModel): + request_id: Optional[str] = None + number: Optional[str] = None + remaining_balance: Optional[str] = None + request_price: Optional[str] = None + status: Optional[int] = None + error_text: Optional[str] = None diff --git a/number_insight/tests/data/advanced_async_insight.json b/number_insight/tests/data/advanced_async_insight.json new file mode 100644 index 00000000..5033a11a --- /dev/null +++ b/number_insight/tests/data/advanced_async_insight.json @@ -0,0 +1,7 @@ +{ + "number": "447700900000", + "remaining_balance": "32.92665294", + "request_id": "434205b5-90ec-4ee2-a337-7b40d9683420", + "request_price": "0.04000000", + "status": 0 +} \ No newline at end of file diff --git a/number_insight/tests/data/advanced_async_insight_error.json b/number_insight/tests/data/advanced_async_insight_error.json new file mode 100644 index 00000000..d95a1f76 --- /dev/null +++ b/number_insight/tests/data/advanced_async_insight_error.json @@ -0,0 +1,4 @@ +{ + "error_text": "Invalid credentials", + "status": 4 +} \ No newline at end of file diff --git a/number_insight/tests/data/advanced_async_insight_partial_error.json b/number_insight/tests/data/advanced_async_insight_partial_error.json new file mode 100644 index 00000000..180b3154 --- /dev/null +++ b/number_insight/tests/data/advanced_async_insight_partial_error.json @@ -0,0 +1,8 @@ +{ + "error_text": "Live mobile lookup not returned", + "status": 43, + "number": "447700900000", + "remaining_balance": "32.92665294", + "request_id": "434205b5-90ec-4ee2-a337-7b40d9683420", + "request_price": "0.04000000" +} \ No newline at end of file diff --git a/number_insight/tests/data/advanced_sync_insight.json b/number_insight/tests/data/advanced_sync_insight.json new file mode 100644 index 00000000..63e266e5 --- /dev/null +++ b/number_insight/tests/data/advanced_sync_insight.json @@ -0,0 +1,44 @@ +{ + "caller_identity": { + "caller_name": "John Smith", + "caller_type": "consumer", + "first_name": "John", + "last_name": "Smith", + "subscription_type": "postpaid" + }, + "caller_name": "John Smith", + "caller_type": "consumer", + "country_code": "US", + "country_code_iso3": "USA", + "country_name": "United States of America", + "country_prefix": "1", + "current_carrier": { + "country": "US", + "name": "AT&T Mobility", + "network_code": "310090", + "network_type": "mobile" + }, + "first_name": "John", + "international_format_number": "12345678900", + "ip_warnings": "unknown", + "last_name": "Smith", + "lookup_outcome": 1, + "lookup_outcome_message": "Partial success - some fields populated", + "national_format_number": "(234) 567-8900", + "original_carrier": { + "country": "US", + "name": "AT&T Mobility", + "network_code": "310090", + "network_type": "mobile" + }, + "ported": "not_ported", + "reachable": "unknown", + "refund_price": "0.01025000", + "remaining_balance": "32.68590294", + "request_id": "97e973e7-2e27-4fd3-9e1a-972ea14dd992", + "request_price": "0.05025000", + "roaming": "unknown", + "status": 44, + "status_message": "Lookup Handler unable to handle request", + "valid_number": "valid" +} \ No newline at end of file diff --git a/number_insight/tests/data/standard_insight.json b/number_insight/tests/data/standard_insight.json new file mode 100644 index 00000000..7017b1fa --- /dev/null +++ b/number_insight/tests/data/standard_insight.json @@ -0,0 +1,36 @@ +{ + "status": 0, + "status_message": "Success", + "request_id": "1d56406b-9d52-497a-a023-b3f40b62f9b3", + "international_format_number": "447700900000", + "national_format_number": "07700 900000", + "country_code": "GB", + "country_code_iso3": "GBR", + "country_name": "United Kingdom", + "country_prefix": "44", + "request_price": "0.00500000", + "remaining_balance": "32.98665294", + "current_carrier": { + "network_code": "23415", + "name": "Vodafone Limited", + "country": "GB", + "network_type": "mobile" + }, + "original_carrier": { + "network_code": "23420", + "name": "Hutchison 3G Ltd", + "country": "GB", + "network_type": "mobile" + }, + "ported": "ported", + "caller_identity": { + "caller_type": "consumer", + "caller_name": "John Smith", + "first_name": "John", + "last_name": "Smith" + }, + "caller_name": "John Smith", + "last_name": "Smith", + "first_name": "John", + "caller_type": "consumer" +} \ No newline at end of file diff --git a/number_insight/tests/test_number_insight.py b/number_insight/tests/test_number_insight.py index b7f7fc57..b376430e 100644 --- a/number_insight/tests/test_number_insight.py +++ b/number_insight/tests/test_number_insight.py @@ -5,7 +5,12 @@ from vonage_http_client.http_client import HttpClient from vonage_number_insight.errors import NumberInsightError from vonage_number_insight.number_insight import NumberInsight -from vonage_number_insight.requests import BasicInsightRequest +from vonage_number_insight.requests import ( + AdvancedAsyncInsightRequest, + AdvancedSyncInsightRequest, + BasicInsightRequest, + StandardInsightRequest, +) from testutils import build_response, get_mock_api_key_auth @@ -47,4 +52,107 @@ def test_basic_insight_error(): options = BasicInsightRequest(number='1234567890', country_code='US') number_insight.basic_number_insight(options) assert e.match('Invalid request :: Not valid number format detected') - assert 0 + + +@responses.activate +def test_standard_insight(): + build_response( + path, + 'GET', + 'https://api.nexmo.com/ni/standard/json', + 'standard_insight.json', + ) + options = StandardInsightRequest(number='12345678900', country_code='US', cnam=True) + response = number_insight.standard_number_insight(options) + assert response.status == 0 + assert response.status_message == 'Success' + assert response.current_carrier.network_code == '23415' + assert response.original_carrier.network_type == 'mobile' + assert response.caller_identity.caller_name == 'John Smith' + + +@responses.activate +def test_advanced_async_insight(): + build_response( + path, + 'GET', + 'https://api.nexmo.com/ni/advanced/async/json', + 'advanced_async_insight.json', + ) + options = AdvancedAsyncInsightRequest( + callback='https://example.com/callback', + number='447700900000', + country_code='GB', + cnam=True, + ) + response = number_insight.advanced_async_number_insight(options) + assert response.status == 0 + assert response.request_id == '434205b5-90ec-4ee2-a337-7b40d9683420' + assert response.number == '447700900000' + assert response.remaining_balance == '32.92665294' + + +@responses.activate +def test_advanced_async_insight_error(): + build_response( + path, + 'GET', + 'https://api.nexmo.com/ni/advanced/async/json', + 'advanced_async_insight_error.json', + ) + + options = AdvancedAsyncInsightRequest( + callback='https://example.com/callback', + number='447700900000', + country_code='GB', + cnam=True, + ) + with raises(NumberInsightError) as e: + number_insight.advanced_async_number_insight(options) + assert e.match('Invalid credentials') + + +@responses.activate +def test_advanced_async_insight_partial_error(caplog): + build_response( + path, + 'GET', + 'https://api.nexmo.com/ni/advanced/async/json', + 'advanced_async_insight_partial_error.json', + ) + + options = AdvancedAsyncInsightRequest( + callback='https://example.com/callback', + number='447700900000', + country_code='GB', + cnam=True, + ) + response = number_insight.advanced_async_number_insight(options) + assert 'Not all parameters are available' in caplog.text + assert response.status == 43 + + +@responses.activate +def test_advanced_sync_insight(caplog): + build_response( + path, + 'GET', + 'https://api.nexmo.com/ni/advanced/json', + 'advanced_sync_insight.json', + ) + options = AdvancedSyncInsightRequest( + number='12345678900', country_code='US', cnam=True, real_time_data=True + ) + response = number_insight.advanced_sync_number_insight(options) + + assert 'Not all parameters are available' in caplog.text + assert response.status == 44 + assert response.request_id == '97e973e7-2e27-4fd3-9e1a-972ea14dd992' + assert response.current_carrier.network_code == '310090' + assert response.first_name == 'John' + assert response.last_name == 'Smith' + assert response.lookup_outcome == 1 + assert response.lookup_outcome_message == 'Partial success - some fields populated' + assert response.roaming == 'unknown' + assert response.status_message == 'Lookup Handler unable to handle request' + assert response.valid_number == 'valid' diff --git a/vonage/CHANGES.md b/vonage/CHANGES.md index 95acc56d..12cf5b55 100644 --- a/vonage/CHANGES.md +++ b/vonage/CHANGES.md @@ -1,3 +1,6 @@ +# 3.99.0a8 +- Add support for the [Vonage Number Insight API](https://developer.vonage.com/en/number-insight/overview). + # 3.99.0a7 - Add support for the [Vonage Voice API](https://developer.vonage.com/en/voice/voice-api/overview). - Add `http_client` property to each module that has an HTTP Client, e.g. Voice, Sms, Verify. diff --git a/vonage/pyproject.toml b/vonage/pyproject.toml index 9b1680c5..581992b6 100644 --- a/vonage/pyproject.toml +++ b/vonage/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "vonage-utils>=1.1.0", "vonage-http-client>=1.3.0", "vonage-messages>=1.1.0", + "vonage-number-insight>=1.0.0", "vonage-number-insight-v2>=0.1.0b0", "vonage-sms>=1.1.0", "vonage-users>=1.1.0", diff --git a/vonage/src/vonage/_version.py b/vonage/src/vonage/_version.py index 5038f475..94c2cd85 100644 --- a/vonage/src/vonage/_version.py +++ b/vonage/src/vonage/_version.py @@ -1 +1 @@ -__version__ = '3.99.0a7' +__version__ = '3.99.0a8'