diff --git a/http_client/CHANGES.md b/http_client/CHANGES.md index 0e35f1be..2e3462b8 100644 --- a/http_client/CHANGES.md +++ b/http_client/CHANGES.md @@ -1,5 +1,6 @@ # 1.2.0 - Add `last_request` and `last_response` properties +- Add new `Forbidden` error # 1.1.1 - Add new Patch method diff --git a/http_client/README.md b/http_client/README.md index d5d39673..a422bf2e 100644 --- a/http_client/README.md +++ b/http_client/README.md @@ -42,9 +42,21 @@ response = client.get(host='api.nexmo.com', request_path='/v1/messages') response = client.post(host='api.nexmo.com', request_path='/v1/messages', params={'key': 'value'}) ``` +## Get the Last Request and Last Response from the HTTP Client + +The `HttpClient` class exposes two properties, `last_request` and `last_response` that cache the last sent request and response. + +```python +# Get last request, has type requests.PreparedRequest +request = client.last_request + +# Get last response, has type requests.Response +response = client.last_response +``` + ### Appending to the User-Agent Header -The HttpClient class also supports appending additional information to the User-Agent header via the append_to_user_agent method: +The `HttpClient` class also supports appending additional information to the User-Agent header via the append_to_user_agent method: ```python client.append_to_user_agent('additional_info') diff --git a/http_client/pyproject.toml b/http_client/pyproject.toml index ffd4545f..d8f02731 100644 --- a/http_client/pyproject.toml +++ b/http_client/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "vonage-http-client" -version = "1.1.1" +version = "1.2.0" description = "An HTTP client for making requests to Vonage APIs." readme = "README.md" authors = [{ name = "Vonage", email = "devrel@vonage.com" }] requires-python = ">=3.8" dependencies = [ - "vonage-utils>=1.0.0", + "vonage-utils>=1.0.1", "vonage-jwt>=1.1.0", "requests>=2.27.0", "pydantic>=2.6.1", diff --git a/http_client/src/vonage_http_client/errors.py b/http_client/src/vonage_http_client/errors.py index a44b00b4..3980a9a5 100644 --- a/http_client/src/vonage_http_client/errors.py +++ b/http_client/src/vonage_http_client/errors.py @@ -67,6 +67,24 @@ def __init__(self, response: Response, content_type: str): super().__init__(response, content_type) +class ForbiddenError(HttpRequestError): + """Exception indicating a forbidden request in a Vonage SDK request. + + This error is raised when the HTTP response status code is 403 (Forbidden). + + Args: + response (requests.Response): The HTTP response object. + content_type (str): The response content type. + + Attributes (inherited from HttpRequestError parent exception): + response (requests.Response): The HTTP response object. + message (str): The returned error message. + """ + + def __init__(self, response: Response, content_type: str): + super().__init__(response, content_type) + + class NotFoundError(HttpRequestError): """Exception indicating a resource was not found in a Vonage SDK request. diff --git a/http_client/src/vonage_http_client/http_client.py b/http_client/src/vonage_http_client/http_client.py index 6d8dd55c..db1cccc0 100644 --- a/http_client/src/vonage_http_client/http_client.py +++ b/http_client/src/vonage_http_client/http_client.py @@ -11,6 +11,7 @@ from vonage_http_client.auth import Auth from vonage_http_client.errors import ( AuthenticationError, + ForbiddenError, HttpRequestError, InvalidHttpClientOptionsError, NotFoundError, @@ -109,7 +110,8 @@ def last_request(self) -> Optional[PreparedRequest]: """The last request sent to the server. Returns: - Optional[PreparedRequest]: The exact bytes of the request sent to the server. + Optional[PreparedRequest]: The exact bytes of the request sent to the server, + or None if no request has been sent. """ return self._last_response.request @@ -118,7 +120,8 @@ def last_response(self) -> Optional[Response]: """The last response received from the server. Returns: - Optional[Response]: The response object received from the server. + Optional[Response]: The response object received from the server, + or None if no response has been received. """ return self._last_response @@ -221,7 +224,6 @@ def _parse_response(self, response: Response) -> Union[dict, None]: f'Response received from {response.url} with status code: {response.status_code}; headers: {response.headers}' ) self._last_response = response - print(response.content) content_type = response.headers['Content-Type'].split(';', 1)[0] if 200 <= response.status_code < 300: if response.status_code == 204: @@ -234,8 +236,10 @@ def _parse_response(self, response: Response) -> Union[dict, None]: logger.warning( f'Http Response Error! Status code: {response.status_code}; content: {repr(response.text)}; from url: {response.url}' ) - if response.status_code == 401 or response.status_code == 403: + if response.status_code == 401: raise AuthenticationError(response, content_type) + if response.status_code == 403: + raise ForbiddenError(response, content_type) elif response.status_code == 404: raise NotFoundError(response, content_type) elif response.status_code == 429: diff --git a/http_client/tests/data/403.json b/http_client/tests/data/403.json new file mode 100644 index 00000000..c08ab5ef --- /dev/null +++ b/http_client/tests/data/403.json @@ -0,0 +1,6 @@ +{ + "type": "https://developer.vonage.com/api-errors#forbidden", + "title": "Forbidden", + "detail": "Your account does not have permission to perform this action.", + "instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf" +} \ No newline at end of file diff --git a/http_client/tests/test_http_client.py b/http_client/tests/test_http_client.py index c13487b1..3bd4d7fa 100644 --- a/http_client/tests/test_http_client.py +++ b/http_client/tests/test_http_client.py @@ -8,6 +8,7 @@ from vonage_http_client.auth import Auth from vonage_http_client.errors import ( AuthenticationError, + ForbiddenError, HttpRequestError, InvalidHttpClientOptionsError, RateLimitedError, @@ -196,6 +197,21 @@ def test_authentication_error_no_content(): assert type(err.response) == Response +@responses.activate +def test_forbidden_error(): + build_response(path, 'GET', 'https://example.com/get_json', '403.json', 403) + + client = HttpClient(Auth()) + try: + client.get(host='example.com', request_path='/get_json', auth_type='basic') + except ForbiddenError as err: + assert err.response.json()['title'] == 'Forbidden' + assert ( + err.response.json()['detail'] + == 'Your account does not have permission to perform this action.' + ) + + @responses.activate def test_not_found_error(): build_response(path, 'GET', 'https://example.com/get_json', '404.json', 404) diff --git a/users/pyproject.toml b/users/pyproject.toml index c42c2ba4..38337222 100644 --- a/users/pyproject.toml +++ b/users/pyproject.toml @@ -1,13 +1,13 @@ [project] name = 'vonage-users' -version = '1.0.0' -description = 'Vonage SMS package' +version = '1.0.1' +description = 'Vonage Users package' readme = "README.md" authors = [{ name = "Vonage", email = "devrel@vonage.com" }] requires-python = ">=3.8" dependencies = [ - "vonage-http-client>=1.1.1", - "vonage-utils>=1.0.0", + "vonage-http-client>=1.2.0", + "vonage-utils>=1.0.1", "pydantic>=2.6.1", ] classifiers = [ diff --git a/verify/README.md b/verify/README.md index 6dccb42d..bd033887 100644 --- a/verify/README.md +++ b/verify/README.md @@ -1,13 +1,8 @@ # Vonage Verify Package -This package contains the code to use Vonage's Verify API in Python. There is a more current package to user Vonage's Verify v2 API which is recommended to use for most use cases. The v2 API lets you send messages via multiple channels, including Email, SMS, MMS, WhatsApp, Messenger and others. You can also make Silent Authentication requests with Verify v2 to give an end user a more seamless experience. - -This package includes methods for sending 2-factor authentication (2FA) messages and returns... - - -asdf -asdf +This package contains the code to use Vonage's Verify API in Python. This package includes methods for working with 2-factor authentication (2FA) messages sent via SMS or TTS. +Note: There is a more current package available: [Vonage's Verify v2 API](https://developer.vonage.com/en/verify/overview) which is recommended for most use cases. The v2 API lets you send messages via multiple channels, including Email, SMS, MMS, WhatsApp, Messenger and others. You can also make Silent Authentication requests with Verify v2 to give an end user a more seamless experience. ## Usage @@ -15,13 +10,54 @@ It is recommended to use this as part of the main `vonage` package. The examples ### Make a Verify Request - +```python +response = vonage_client.verify.trigger_next_event('my_request_id') +``` + +### Request a Network Unblock + +Note: Network Unblock is switched off by default. Contact Sales to enable the Network Unblock API for your account. + +```python +response = vonage_client.verify.request_network_unblock('23410') +``` diff --git a/verify/pyproject.toml b/verify/pyproject.toml index 43d2ab4d..7994d8b8 100644 --- a/verify/pyproject.toml +++ b/verify/pyproject.toml @@ -6,8 +6,8 @@ readme = "README.md" authors = [{ name = "Vonage", email = "devrel@vonage.com" }] requires-python = ">=3.8" dependencies = [ - "vonage-http-client>=1.1.1", - "vonage-utils>=1.0.0", + "vonage-http-client>=1.2.0", + "vonage-utils>=1.0.1", "pydantic>=2.6.1", ] classifiers = [ diff --git a/verify/src/vonage_verify/__init__.py b/verify/src/vonage_verify/__init__.py index 442b7842..65a043bb 100644 --- a/verify/src/vonage_verify/__init__.py +++ b/verify/src/vonage_verify/__init__.py @@ -1,11 +1,25 @@ -# from .errors import PartialFailureError, SmsError +from .errors import VerifyError +from .language_codes import LanguageCode, Psd2LanguageCode from .requests import Psd2Request, VerifyRequest - -# from .responses import MessageResponse, SmsResponse +from .responses import ( + CheckCodeResponse, + NetworkUnblockStatus, + StartVerificationResponse, + VerifyControlStatus, + VerifyStatus, +) from .verify import Verify __all__ = [ 'Verify', - 'VerifyRequest', + 'VerifyError', + 'LanguageCode', + 'Psd2LanguageCode', 'Psd2Request', + 'VerifyRequest', + 'CheckCodeResponse', + 'NetworkUnblockStatus', + 'StartVerificationResponse', + 'VerifyControlStatus', + 'VerifyStatus', ] diff --git a/verify/src/vonage_verify/verify.py b/verify/src/vonage_verify/verify.py index ad825bf6..b26b4c66 100644 --- a/verify/src/vonage_verify/verify.py +++ b/verify/src/vonage_verify/verify.py @@ -1,12 +1,13 @@ -from typing import List, Union +from typing import List, Optional, Union -from pydantic import validate_call +from pydantic import Field, validate_call from vonage_http_client.http_client import HttpClient from .errors import VerifyError from .requests import BaseVerifyRequest, Psd2Request, VerifyRequest from .responses import ( CheckCodeResponse, + NetworkUnblockStatus, StartVerificationResponse, VerifyControlStatus, VerifyStatus, @@ -153,6 +154,28 @@ def trigger_next_event(self, request_id: str) -> VerifyControlStatus: return VerifyControlStatus(**response) + @validate_call + def request_network_unblock( + self, network: str, unblock_duration: Optional[int] = Field(None, ge=0, le=86400) + ) -> NetworkUnblockStatus: + """Request to unblock a network that has been blocked due to potential fraud detection. + + Note: The network unblock feature is switched off by default. + Please contact Sales to enable the Network Unblock API for your account. + + Args: + network (str): The network code of the network to unblock. + unblock_duration (int, optional): How long (in seconds) to unblock the network for. + """ + response = self._http_client.post( + self._http_client.api_host, + '/verify/network-unblock', + {'network': network, 'duration': unblock_duration}, + self._auth_type, + ) + + return NetworkUnblockStatus(**response) + def _make_verify_request( self, verify_request: BaseVerifyRequest ) -> StartVerificationResponse: diff --git a/verify/tests/data/network_unblock.json b/verify/tests/data/network_unblock.json new file mode 100644 index 00000000..6620d3f4 --- /dev/null +++ b/verify/tests/data/network_unblock.json @@ -0,0 +1,4 @@ +{ + "network": "23410", + "unblocked_until": "2024-04-22T08:34:58Z" +} \ No newline at end of file diff --git a/verify/tests/data/network_unblock_error.json b/verify/tests/data/network_unblock_error.json new file mode 100644 index 00000000..bf7cadba --- /dev/null +++ b/verify/tests/data/network_unblock_error.json @@ -0,0 +1,6 @@ +{ + "type": "https://developer.vonage.com/api-errors#bad-request", + "title": "Not Found", + "detail": "The network you provided does not have an active block.", + "instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf" +} \ No newline at end of file diff --git a/verify/tests/test_verify.py b/verify/tests/test_verify.py index 451a8feb..d8a5af19 100644 --- a/verify/tests/test_verify.py +++ b/verify/tests/test_verify.py @@ -2,11 +2,12 @@ import responses from pytest import raises +from vonage_http_client.errors import NotFoundError from vonage_http_client.http_client import HttpClient from vonage_verify.errors import VerifyError from vonage_verify.language_codes import LanguageCode, Psd2LanguageCode from vonage_verify.requests import Psd2Request, VerifyRequest -from vonage_verify.responses import VerifyControlStatus +from vonage_verify.responses import NetworkUnblockStatus, VerifyControlStatus from vonage_verify.verify import Verify from testutils import build_response, get_mock_api_key_auth @@ -227,3 +228,72 @@ def test_cancel_verification_error(): assert e.match( "The requestId 'cc121958d8fb4368aa3bb762bb9a0f75' does not exist or its no longer active." ) + + +@responses.activate +def test_trigger_next_event(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/verify/control/json', + 'trigger_next_event.json', + ) + response = verify.trigger_next_event('c5037cb8b47449158ed6611afde58990') + + assert type(response) == VerifyControlStatus + assert response.status == '0' + assert response.command == 'trigger_next_event' + + +@responses.activate +def test_trigger_next_event_error(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/verify/control/json', + 'trigger_next_event_error.json', + ) + + with raises(VerifyError) as e: + verify.trigger_next_event('2c021d25cf2e47a9b277a996f4325b81') + + assert e.match("'status': '19") + assert e.match('No more events are left to execute for the request') + + +@responses.activate +def test_request_network_unblock(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/verify/network-unblock', + 'network_unblock.json', + 202, + ) + + response = verify.request_network_unblock('23410') + + assert verify._http_client.last_response.status_code == 202 + assert type(response) == NetworkUnblockStatus + assert response.network == '23410' + assert response.unblocked_until == '2024-04-22T08:34:58Z' + + +@responses.activate +def test_request_network_unblock_error(): + build_response( + path, + 'POST', + 'https://api.nexmo.com/verify/network-unblock', + 'network_unblock_error.json', + 404, + ) + + try: + verify.request_network_unblock('23410') + except NotFoundError as e: + assert ( + e.response.json()['detail'] + == 'The network you provided does not have an active block.' + ) + assert e.response.json()['title'] == 'Not Found' diff --git a/vonage/CHANGES.md b/vonage/CHANGES.md index 6bf57af6..ebeaef18 100644 --- a/vonage/CHANGES.md +++ b/vonage/CHANGES.md @@ -1,5 +1,6 @@ # 3.99.0a4 - Add support for the [Vonage Verify API](https://developer.vonage.com/en/api/verify). +- Add `last_request` and `last_response` properties to the HTTP Client. # 3.99.0a3 - Add support for the [Vonage Users API](https://developer.vonage.com/en/api/application.v2#User). diff --git a/vonage/pyproject.toml b/vonage/pyproject.toml index f45f3e9c..053f8839 100644 --- a/vonage/pyproject.toml +++ b/vonage/pyproject.toml @@ -6,11 +6,12 @@ readme = "README.md" authors = [{ name = "Vonage", email = "devrel@vonage.com" }] requires-python = ">=3.8" dependencies = [ - "vonage-utils>=1.0.0", - "vonage-http-client>=1.1.1", + "vonage-utils>=1.0.1", + "vonage-http-client>=1.2.0", "vonage-number-insight-v2>=0.1.0", "vonage-sms>=1.0.2", - "vonage-users>=1.0.0", + "vonage-users>=1.0.1", + "vonage-verify>=1.0.0", ] classifiers = [ "Programming Language :: Python", diff --git a/vonage/src/vonage/_version.py b/vonage/src/vonage/_version.py index cbad4bb3..9233543a 100644 --- a/vonage/src/vonage/_version.py +++ b/vonage/src/vonage/_version.py @@ -1 +1 @@ -__version__ = '3.99.0a3' +__version__ = '3.99.0a4' diff --git a/vonage_utils/CHANGES.md b/vonage_utils/CHANGES.md index a376cb52..bc23e4c1 100644 --- a/vonage_utils/CHANGES.md +++ b/vonage_utils/CHANGES.md @@ -1,2 +1,5 @@ +# 1.0.1 +- Add `PhoneNumber` type + # 1.0.0 - Initial upload \ No newline at end of file diff --git a/vonage_utils/pyproject.toml b/vonage_utils/pyproject.toml index 9303d98e..75fd4474 100644 --- a/vonage_utils/pyproject.toml +++ b/vonage_utils/pyproject.toml @@ -1,6 +1,6 @@ [project] name = 'vonage-utils' -version = '1.0.0' +version = '1.0.1' description = 'Utils package containing objects for use with Vonage APIs' readme = "README.md" authors = [{ name = "Vonage", email = "devrel@vonage.com" }] diff --git a/vonage_utils/src/vonage_utils/__init__.py b/vonage_utils/src/vonage_utils/__init__.py index 7e04e7f1..db619eb7 100644 --- a/vonage_utils/src/vonage_utils/__init__.py +++ b/vonage_utils/src/vonage_utils/__init__.py @@ -1,4 +1,5 @@ from .errors import VonageError +from .types.phone_number import PhoneNumber from .utils import format_phone_number, remove_none_values -__all__ = ['VonageError', 'format_phone_number', 'remove_none_values'] +__all__ = ['VonageError', 'format_phone_number', 'remove_none_values', PhoneNumber] diff --git a/vonage_utils/src/vonage_utils/types/__init__.py b/vonage_utils/src/vonage_utils/types/__init__.py new file mode 100644 index 00000000..e69de29b