diff --git a/http_client/CHANGES.md b/http_client/CHANGES.md index c487096..68238c1 100644 --- a/http_client/CHANGES.md +++ b/http_client/CHANGES.md @@ -1,3 +1,6 @@ +# 1.4.0 +- Add new `oauth2` logic for calling APIs that require Oauth + # 1.3.1 - Update minimum dependency version diff --git a/http_client/pyproject.toml b/http_client/pyproject.toml index e98c904..20b5cd1 100644 --- a/http_client/pyproject.toml +++ b/http_client/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "vonage-http-client" -version = "1.3.1" +version = "1.4.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.1.1", + "vonage-utils>=1.1.2", "vonage-jwt>=1.1.1", "requests>=2.27.0", "typing-extensions>=4.9.0", diff --git a/http_client/src/vonage_http_client/http_client.py b/http_client/src/vonage_http_client/http_client.py index 89bfd7d..8a516f1 100644 --- a/http_client/src/vonage_http_client/http_client.py +++ b/http_client/src/vonage_http_client/http_client.py @@ -130,11 +130,12 @@ def post( host: str, request_path: str = '', params: dict = None, - auth_type: Literal['jwt', 'basic', 'body', 'signature'] = 'jwt', - sent_data_type: Literal['json', 'data'] = 'json', + auth_type: Literal['jwt', 'basic', 'body', 'signature', 'oauth2'] = 'jwt', + sent_data_type: Literal['json', 'form', 'query-params'] = 'json', + token: Optional[str] = None, ) -> Union[dict, None]: return self.make_request( - 'POST', host, request_path, params, auth_type, sent_data_type + 'POST', host, request_path, params, auth_type, sent_data_type, token ) def get( @@ -192,8 +193,9 @@ def make_request( host: str, request_path: str = '', params: Optional[dict] = None, - auth_type: Literal['jwt', 'basic', 'body', 'signature'] = 'jwt', + auth_type: Literal['jwt', 'basic', 'body', 'signature', 'oauth2'] = 'jwt', sent_data_type: Literal['json', 'form', 'query_params'] = 'json', + token: Optional[str] = None, ): url = f'https://{host}{request_path}' logger.debug( @@ -206,6 +208,8 @@ def make_request( elif auth_type == 'body': params['api_key'] = self._auth.api_key params['api_secret'] = self._auth.api_secret + elif auth_type == 'oauth2': + self._headers['Authorization'] = f'Bearer {token}' elif auth_type == 'signature': params['api_key'] = self._auth.api_key params['sig'] = self._auth.sign_params(params) diff --git a/network_auth/BUILD b/network_auth/BUILD new file mode 100644 index 0000000..8487b43 --- /dev/null +++ b/network_auth/BUILD @@ -0,0 +1,16 @@ +resource(name='pyproject', source='pyproject.toml') +file(name='readme', source='README.md') + +files(sources=['tests/data/*']) + +python_distribution( + name='vonage-network-auth', + dependencies=[ + ':pyproject', + ':readme', + 'network_auth/src/vonage_network_auth', + ], + provides=python_artifact(), + generate_setup=False, + repositories=['@pypi'], +) diff --git a/network_auth/CHANGES.md b/network_auth/CHANGES.md new file mode 100644 index 0000000..4df1290 --- /dev/null +++ b/network_auth/CHANGES.md @@ -0,0 +1,2 @@ +# 0.1.0b0 +- Initial upload \ No newline at end of file diff --git a/network_auth/README.md b/network_auth/README.md new file mode 100644 index 0000000..ba06fc9 --- /dev/null +++ b/network_auth/README.md @@ -0,0 +1,36 @@ +# Vonage Network API Authentication Client + +This package (`vonage-network-auth`) provides a client for authenticating Network APIs that require Oauth2 authentcation. Using it, it is possible to generate authenticated JWTs for use with GNP APIs, e.g. Sim Swap, Number Verification. + +This package is intended to be used as part of an SDK, accessing required methods through the SDK instead of directly. Thus, it doesn't require manual installation or configuration unless you're using this package independently of an SDK. + +For full API documentation, refer to the [Vonage developer documentation](https://developer.vonage.com). + +Please note this package is in beta. + +## Installation + +Install from the Python Package Index with pip: + +```bash +pip install vonage-network-auth +``` + +## Usage + +### Create a `NetworkAuth` Object + +```python +from vonage_network_auth import NetworkAuth +from vonage_http_client import HttpClient, Auth + +network_auth = NetworkAuth(HttpClient(Auth(application_id='application-id', private_key='private-key'))) +``` + +### Generate an Authenticated Access Token + +```python +token = network_auth.get_oauth2_user_token( + number='447700900000', scope='dpv:FraudPreventionAndDetection#check-sim-swap' +) +``` diff --git a/network_auth/pyproject.toml b/network_auth/pyproject.toml new file mode 100644 index 0000000..c14a52d --- /dev/null +++ b/network_auth/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "vonage-network-auth" +version = "0.1.0b0" +description = "Package for working with Network APIs that require Oauth2 in Python." +readme = "README.md" +authors = [{ name = "Vonage", email = "devrel@vonage.com" }] +requires-python = ">=3.8" +dependencies = ["vonage-http-client>=1.4.0", "vonage-utils>=1.1.2"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: Apache Software License", +] + +[project.urls] +Homepage = "https://github.com/Vonage/vonage-python-sdk" + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/network_auth/src/vonage_network_auth/BUILD b/network_auth/src/vonage_network_auth/BUILD new file mode 100644 index 0000000..db46e8d --- /dev/null +++ b/network_auth/src/vonage_network_auth/BUILD @@ -0,0 +1 @@ +python_sources() diff --git a/network_auth/src/vonage_network_auth/__init__.py b/network_auth/src/vonage_network_auth/__init__.py new file mode 100644 index 0000000..e321e40 --- /dev/null +++ b/network_auth/src/vonage_network_auth/__init__.py @@ -0,0 +1,4 @@ +from .network_auth import NetworkAuth +from .responses import OidcResponse, TokenResponse + +__all__ = ['NetworkAuth', 'OidcResponse', 'TokenResponse'] diff --git a/network_auth/src/vonage_network_auth/network_auth.py b/network_auth/src/vonage_network_auth/network_auth.py new file mode 100644 index 0000000..1813d59 --- /dev/null +++ b/network_auth/src/vonage_network_auth/network_auth.py @@ -0,0 +1,91 @@ +from pydantic import validate_call +from vonage_http_client.http_client import HttpClient + +from .responses import OidcResponse, TokenResponse + + +class NetworkAuth: + """Class containing methods for authenticating Network APIs following Camara standards.""" + + def __init__(self, http_client: HttpClient): + self._http_client = http_client + self._host = 'api-eu.vonage.com' + self._auth_type = 'jwt' + self._sent_data_type = 'form' + + @property + def http_client(self) -> HttpClient: + """The HTTP client used to make requests to the Network Auth API. + + Returns: + HttpClient: The HTTP client used to make requests to the Network Auth API. + """ + return self._http_client + + @validate_call + def get_oauth2_user_token(self, number: str, scope: str) -> str: + """Get an OAuth2 user token for a given number and scope. + + Args: + number (str): The phone number to authenticate. + scope (str): The scope of the token. + + Returns: + str: The OAuth2 user token. + """ + oidc_response = self.make_oidc_request(number, scope) + token_response = self.request_access_token(oidc_response.auth_req_id) + return token_response.access_token + + @validate_call + def make_oidc_request(self, number: str, scope: str) -> OidcResponse: + """Make an OIDC request to authenticate a user. + + Args: + number (str): The phone number to authenticate. + scope (str): The scope of the token. + + Returns: + OidcResponse: A response containing the authentication request ID. + """ + number = self._ensure_plus_prefix(number) + params = {'login_hint': number, 'scope': scope} + + response = self._http_client.post( + self._host, + '/oauth2/bc-authorize', + params, + self._auth_type, + self._sent_data_type, + ) + return OidcResponse(**response) + + @validate_call + def request_access_token( + self, auth_req_id: str, grant_type: str = 'urn:openid:params:grant-type:ciba' + ) -> TokenResponse: + """Request a Camara access token using an authentication request ID given as a response to + an OIDC request.""" + params = {'auth_req_id': auth_req_id, 'grant_type': grant_type} + + response = self._http_client.post( + self._host, + '/oauth2/token', + params, + self._auth_type, + self._sent_data_type, + ) + return TokenResponse(**response) + + def _ensure_plus_prefix(self, number: str) -> str: + """Ensure that the number has a plus prefix. + + Args: + number (str): The phone number to check. + + Returns: + str: The phone number with a plus prefix. + """ + if number.startswith('+'): + return number + return f'+{number}' diff --git a/network_auth/src/vonage_network_auth/responses.py b/network_auth/src/vonage_network_auth/responses.py new file mode 100644 index 0000000..396fdf4 --- /dev/null +++ b/network_auth/src/vonage_network_auth/responses.py @@ -0,0 +1,16 @@ +from typing import Optional + +from pydantic import BaseModel + + +class OidcResponse(BaseModel): + auth_req_id: str + expires_in: int + interval: Optional[int] = None + + +class TokenResponse(BaseModel): + access_token: str + token_type: Optional[str] = None + refresh_token: Optional[str] = None + expires_in: Optional[int] = None diff --git a/network_auth/tests/BUILD b/network_auth/tests/BUILD new file mode 100644 index 0000000..0f91737 --- /dev/null +++ b/network_auth/tests/BUILD @@ -0,0 +1 @@ +python_tests(dependencies=['network_auth', 'testutils']) diff --git a/network_auth/tests/data/oidc_request.json b/network_auth/tests/data/oidc_request.json new file mode 100644 index 0000000..1ad7177 --- /dev/null +++ b/network_auth/tests/data/oidc_request.json @@ -0,0 +1,5 @@ +{ + "auth_req_id": "arid/8b0d35f3-4627-487c-a776-aegtdsf4rsd2", + "expires_in": 300, + "interval": 0 +} \ No newline at end of file diff --git a/network_auth/tests/data/oidc_request_permissions_error.json b/network_auth/tests/data/oidc_request_permissions_error.json new file mode 100644 index 0000000..873e6c0 --- /dev/null +++ b/network_auth/tests/data/oidc_request_permissions_error.json @@ -0,0 +1,6 @@ +{ + "type": "https://developer.vonage.com/api-errors#invalid-param", + "title": "Bad Request", + "detail": "No Network Application associated with Vonage Application: 29f760f8-7ce1-46c9-ade3-f2dedee4ed5f", + "instance": "b45ae630-7621-42b0-8ff0-6c1ad98e6e32" +} \ No newline at end of file diff --git a/network_auth/tests/data/token_request.json b/network_auth/tests/data/token_request.json new file mode 100644 index 0000000..cd10a01 --- /dev/null +++ b/network_auth/tests/data/token_request.json @@ -0,0 +1,5 @@ +{ + "access_token": "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYW51YmlzLWNlcnRzLWMxLWV1dzEucHJvZC52MS52b25hZ2VuZXR3b3Jrcy5uZXQvandrcyIsImtpZCI6IkNOPVZvbmFnZSAxdmFwaWd3IEludGVybmFsIENBOjoxOTUxODQ2ODA3NDg1NTYwNjYzODY3MTM0NjE2MjU2MTU5MjU2NDkiLCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL2FudWJpcy1jZXJ0cy1jMS1ldXcxLnByb2QudjEudm9uYWdlbmV0d29ya3MubmV0L3YxL2NlcnRzLzA4NjliNDMyZTEzZmIyMzcwZTk2ZGI4YmUxMDc4MjJkIn0.eyJwcmluY2lwYWwiOnsiYXBpS2V5IjoiNGI1MmMwMGUiLCJhcHBsaWNhdGlvbklkIjoiMmJlZTViZWQtNmZlZS00ZjM2LTkxNmQtNWUzYjRjZDI1MjQzIiwibWFzdGVyQWNjb3VudElkIjoiNGI1MmMwMGUiLCJjYXBhYmlsaXRpZXMiOlsibmV0d29yay1hcGktZmVhdHVyZXMiXSwiZXh0cmFDb25maWciOnsiY2FtYXJhU3RhdGUiOiJmb0ZyQndnOFNmeGMydnd2S1o5Y3UrMlgrT0s1K2FvOWhJTTVGUGZMQ1dOeUlMTHR3WmY1dFRKbDdUc1p4QnY4QWx3aHM2bFNWcGVvVkhoWngvM3hUenFRWVkwcHpIZE5XL085ZEdRN1RKOE9sU1lDdTFYYXFEcnNFbEF4WEJVcUpGdnZTTkp5a1A5ZDBYWVN4ajZFd0F6UUFsNGluQjE1c3VMRFNsKy82U1FDa29Udnpld0tvcFRZb0F5MVg2dDJVWXdEVWFDNjZuOS9kVWxIemN3V0NGK3QwOGNReGxZVUxKZyt3T0hwV2xvWGx1MGc3REx0SCtHd0pvRGJoYnMyT2hVY3BobGZqajBpeHQ1OTRsSG5sQ1NYNkZrMmhvWEhKUW01S3JtOVBKSmttK0xTRjVsRTd3NUxtWTRvYTFXSGpkY0dwV1VsQlNQY000YnprOGU0bVE9PSJ9fSwiZmVkZXJhdGVkQXNzZXJ0aW9ucyI6e30sImF1ZCI6ImFwaS1ldS52b25hZ2UuY29tIiwiZXhwIjoxNzE3MDkyODY4LCJqdGkiOiJmNDZhYTViOC1hODA2LTRjMzctODQyMS02OGYwMzJjNDlhMWYiLCJpYXQiOjE3MTcwOTE5NzAsImlzcyI6IlZJQU0tSUFQIiwibmJmIjoxNzE3MDkxOTU1fQ.iLUbyDPR1HGLKh29fy6fqK65Q1O7mjWOletAEPJD4eu7gb0E85EL4M9R7ckJq5lIvgedQt3vBheTaON9_u-VYjMqo8ulPoEoGUDHbOzNbs4MmCW0_CRdDPGyxnUhvcbuJhPgnEHxmfHjJBljncUnk-Z7XCgyNajBNXeQQnHkRF_6NMngxJ-qjjhqbYL0VsF_JS7-TXxixNL0KAFl0SeN2DjkfwRBCclP-69CTExDjyOvouAcchqi-6ZYj_tXPCrTADuzUrQrW8C5nHp2-XjWJSFKzyvi48n8V1U6KseV-eYzBzvy7bJf0tRMX7G6gctTYq3DxdC_eXvXlnp1zx16mg", + "token_type": "bearer", + "expires_in": 29 +} \ No newline at end of file diff --git a/network_auth/tests/test_network_auth.py b/network_auth/tests/test_network_auth.py new file mode 100644 index 0000000..4bdbf90 --- /dev/null +++ b/network_auth/tests/test_network_auth.py @@ -0,0 +1,111 @@ +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_network_auth import NetworkAuth +from vonage_network_auth.responses import OidcResponse + +from testutils import build_response, get_mock_jwt_auth + +path = abspath(__file__) + + +network_auth = NetworkAuth(HttpClient(get_mock_jwt_auth())) + + +def test_http_client_property(): + http_client = network_auth.http_client + assert isinstance(http_client, HttpClient) + + +@responses.activate +def test_oidc_request(): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/oauth2/bc-authorize', + 'oidc_request.json', + ) + + response = network_auth.make_oidc_request( + number='447700900000', + scope='dpv:FraudPreventionAndDetection#check-sim-swap', + ) + + assert response.auth_req_id == 'arid/8b0d35f3-4627-487c-a776-aegtdsf4rsd2' + assert response.expires_in == 300 + assert response.interval == 0 + + +@responses.activate +def test_request_access_token(): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/oauth2/token', + 'token_request.json', + ) + + oidc_response_dict = { + 'auth_req_id': '0dadaeb4-7c79-4d39-b4b0-5a6cc08bf537', + 'expires_in': '120', + 'interval': '2', + } + oidc_response = OidcResponse(**oidc_response_dict) + response = network_auth.request_access_token(oidc_response.auth_req_id) + + assert ( + response.access_token + == 'eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYW51YmlzLWNlcnRzLWMxLWV1dzEucHJvZC52MS52b25hZ2VuZXR3b3Jrcy5uZXQvandrcyIsImtpZCI6IkNOPVZvbmFnZSAxdmFwaWd3IEludGVybmFsIENBOjoxOTUxODQ2ODA3NDg1NTYwNjYzODY3MTM0NjE2MjU2MTU5MjU2NDkiLCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL2FudWJpcy1jZXJ0cy1jMS1ldXcxLnByb2QudjEudm9uYWdlbmV0d29ya3MubmV0L3YxL2NlcnRzLzA4NjliNDMyZTEzZmIyMzcwZTk2ZGI4YmUxMDc4MjJkIn0.eyJwcmluY2lwYWwiOnsiYXBpS2V5IjoiNGI1MmMwMGUiLCJhcHBsaWNhdGlvbklkIjoiMmJlZTViZWQtNmZlZS00ZjM2LTkxNmQtNWUzYjRjZDI1MjQzIiwibWFzdGVyQWNjb3VudElkIjoiNGI1MmMwMGUiLCJjYXBhYmlsaXRpZXMiOlsibmV0d29yay1hcGktZmVhdHVyZXMiXSwiZXh0cmFDb25maWciOnsiY2FtYXJhU3RhdGUiOiJmb0ZyQndnOFNmeGMydnd2S1o5Y3UrMlgrT0s1K2FvOWhJTTVGUGZMQ1dOeUlMTHR3WmY1dFRKbDdUc1p4QnY4QWx3aHM2bFNWcGVvVkhoWngvM3hUenFRWVkwcHpIZE5XL085ZEdRN1RKOE9sU1lDdTFYYXFEcnNFbEF4WEJVcUpGdnZTTkp5a1A5ZDBYWVN4ajZFd0F6UUFsNGluQjE1c3VMRFNsKy82U1FDa29Udnpld0tvcFRZb0F5MVg2dDJVWXdEVWFDNjZuOS9kVWxIemN3V0NGK3QwOGNReGxZVUxKZyt3T0hwV2xvWGx1MGc3REx0SCtHd0pvRGJoYnMyT2hVY3BobGZqajBpeHQ1OTRsSG5sQ1NYNkZrMmhvWEhKUW01S3JtOVBKSmttK0xTRjVsRTd3NUxtWTRvYTFXSGpkY0dwV1VsQlNQY000YnprOGU0bVE9PSJ9fSwiZmVkZXJhdGVkQXNzZXJ0aW9ucyI6e30sImF1ZCI6ImFwaS1ldS52b25hZ2UuY29tIiwiZXhwIjoxNzE3MDkyODY4LCJqdGkiOiJmNDZhYTViOC1hODA2LTRjMzctODQyMS02OGYwMzJjNDlhMWYiLCJpYXQiOjE3MTcwOTE5NzAsImlzcyI6IlZJQU0tSUFQIiwibmJmIjoxNzE3MDkxOTU1fQ.iLUbyDPR1HGLKh29fy6fqK65Q1O7mjWOletAEPJD4eu7gb0E85EL4M9R7ckJq5lIvgedQt3vBheTaON9_u-VYjMqo8ulPoEoGUDHbOzNbs4MmCW0_CRdDPGyxnUhvcbuJhPgnEHxmfHjJBljncUnk-Z7XCgyNajBNXeQQnHkRF_6NMngxJ-qjjhqbYL0VsF_JS7-TXxixNL0KAFl0SeN2DjkfwRBCclP-69CTExDjyOvouAcchqi-6ZYj_tXPCrTADuzUrQrW8C5nHp2-XjWJSFKzyvi48n8V1U6KseV-eYzBzvy7bJf0tRMX7G6gctTYq3DxdC_eXvXlnp1zx16mg' + ) + assert response.token_type == 'bearer' + assert response.expires_in == 29 + + +@responses.activate +def test_whole_oauth2_flow(): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/oauth2/bc-authorize', + 'oidc_request.json', + ) + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/oauth2/token', + 'token_request.json', + ) + + access_token = network_auth.get_oauth2_user_token( + number='447700900000', scope='dpv:FraudPreventionAndDetection#check-sim-swap' + ) + assert ( + access_token + == 'eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYW51YmlzLWNlcnRzLWMxLWV1dzEucHJvZC52MS52b25hZ2VuZXR3b3Jrcy5uZXQvandrcyIsImtpZCI6IkNOPVZvbmFnZSAxdmFwaWd3IEludGVybmFsIENBOjoxOTUxODQ2ODA3NDg1NTYwNjYzODY3MTM0NjE2MjU2MTU5MjU2NDkiLCJ0eXAiOiJKV1QiLCJ4NXUiOiJodHRwczovL2FudWJpcy1jZXJ0cy1jMS1ldXcxLnByb2QudjEudm9uYWdlbmV0d29ya3MubmV0L3YxL2NlcnRzLzA4NjliNDMyZTEzZmIyMzcwZTk2ZGI4YmUxMDc4MjJkIn0.eyJwcmluY2lwYWwiOnsiYXBpS2V5IjoiNGI1MmMwMGUiLCJhcHBsaWNhdGlvbklkIjoiMmJlZTViZWQtNmZlZS00ZjM2LTkxNmQtNWUzYjRjZDI1MjQzIiwibWFzdGVyQWNjb3VudElkIjoiNGI1MmMwMGUiLCJjYXBhYmlsaXRpZXMiOlsibmV0d29yay1hcGktZmVhdHVyZXMiXSwiZXh0cmFDb25maWciOnsiY2FtYXJhU3RhdGUiOiJmb0ZyQndnOFNmeGMydnd2S1o5Y3UrMlgrT0s1K2FvOWhJTTVGUGZMQ1dOeUlMTHR3WmY1dFRKbDdUc1p4QnY4QWx3aHM2bFNWcGVvVkhoWngvM3hUenFRWVkwcHpIZE5XL085ZEdRN1RKOE9sU1lDdTFYYXFEcnNFbEF4WEJVcUpGdnZTTkp5a1A5ZDBYWVN4ajZFd0F6UUFsNGluQjE1c3VMRFNsKy82U1FDa29Udnpld0tvcFRZb0F5MVg2dDJVWXdEVWFDNjZuOS9kVWxIemN3V0NGK3QwOGNReGxZVUxKZyt3T0hwV2xvWGx1MGc3REx0SCtHd0pvRGJoYnMyT2hVY3BobGZqajBpeHQ1OTRsSG5sQ1NYNkZrMmhvWEhKUW01S3JtOVBKSmttK0xTRjVsRTd3NUxtWTRvYTFXSGpkY0dwV1VsQlNQY000YnprOGU0bVE9PSJ9fSwiZmVkZXJhdGVkQXNzZXJ0aW9ucyI6e30sImF1ZCI6ImFwaS1ldS52b25hZ2UuY29tIiwiZXhwIjoxNzE3MDkyODY4LCJqdGkiOiJmNDZhYTViOC1hODA2LTRjMzctODQyMS02OGYwMzJjNDlhMWYiLCJpYXQiOjE3MTcwOTE5NzAsImlzcyI6IlZJQU0tSUFQIiwibmJmIjoxNzE3MDkxOTU1fQ.iLUbyDPR1HGLKh29fy6fqK65Q1O7mjWOletAEPJD4eu7gb0E85EL4M9R7ckJq5lIvgedQt3vBheTaON9_u-VYjMqo8ulPoEoGUDHbOzNbs4MmCW0_CRdDPGyxnUhvcbuJhPgnEHxmfHjJBljncUnk-Z7XCgyNajBNXeQQnHkRF_6NMngxJ-qjjhqbYL0VsF_JS7-TXxixNL0KAFl0SeN2DjkfwRBCclP-69CTExDjyOvouAcchqi-6ZYj_tXPCrTADuzUrQrW8C5nHp2-XjWJSFKzyvi48n8V1U6KseV-eYzBzvy7bJf0tRMX7G6gctTYq3DxdC_eXvXlnp1zx16mg' + ) + + +def test_number_plus_prefixes(): + assert network_auth._ensure_plus_prefix('447700900000') == '+447700900000' + assert network_auth._ensure_plus_prefix('+447700900000') == '+447700900000' + + +@responses.activate +def test_oidc_request_permissions_error(): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/oauth2/bc-authorize', + 'oidc_request_permissions_error.json', + status_code=400, + ) + + with raises(HttpRequestError) as err: + response = network_auth.make_oidc_request( + number='447700900000', + scope='dpv:FraudPreventionAndDetection#check-sim-swap', + ) + assert err.match('"title": "Bad Request"') diff --git a/network_sim_swap/BUILD b/network_sim_swap/BUILD new file mode 100644 index 0000000..11bc974 --- /dev/null +++ b/network_sim_swap/BUILD @@ -0,0 +1,16 @@ +resource(name='pyproject', source='pyproject.toml') +file(name='readme', source='README.md') + +files(sources=['tests/data/*']) + +python_distribution( + name='vonage-network-sim-swap', + dependencies=[ + ':pyproject', + ':readme', + 'network_sim_swap/src/vonage_network_sim_swap', + ], + provides=python_artifact(), + generate_setup=False, + repositories=['@pypi'], +) diff --git a/network_sim_swap/CHANGES.md b/network_sim_swap/CHANGES.md new file mode 100644 index 0000000..4df1290 --- /dev/null +++ b/network_sim_swap/CHANGES.md @@ -0,0 +1,2 @@ +# 0.1.0b0 +- Initial upload \ No newline at end of file diff --git a/network_sim_swap/README.md b/network_sim_swap/README.md new file mode 100644 index 0000000..7a2d70b --- /dev/null +++ b/network_sim_swap/README.md @@ -0,0 +1,41 @@ +# Vonage Sim Swap Network API Client + +This package (`vonage-network-sim-swap`) allows you to check whether a SIM card has been swapped, and the last swap date. + +This package is not intended to be used directly, instead being accessed from an enclosing SDK package. Thus, it doesn't require manual installation or configuration unless you're using this package independently of an SDK. + +For full API documentation, refer to the [Vonage developer documentation](https://developer.vonage.com). + +Please note this package is in beta. + +## Registering to Use the Sim Swap API + +To use this API, you must first create and register your business profile with the Vonage Network Registry. [This documentation page](https://developer.vonage.com/en/getting-started-network/registration) explains how this can be done. You need to obtain approval for each network and region you want to use the APIs in. + +## Installation + +Install from the Python Package Index with pip: + +```bash +pip install vonage-network-sim-swap +``` + +## Usage + +It is recommended to use this as part of the `vonage-network` package. The examples below assume you've created an instance of the `vonage_network.VonageNetwork` class called `network_client`. + +### Check if a SIM Has Been Swapped + +```python +from vonage_network_sim_swap import SwapStatus +swap_status: SwapStatus = vonage_network.sim_swap.check(phone_number='MY_NUMBER') +print(swap_status.swapped) +``` + +### Get the Date of the Last SIM Swap + +```python +from vonage_network_sim_swap import LastSwapDate +swap_date: LastSwapDate = vonage_network.sim_swap.get_last_swap_date +print(swap_date.last_swap_date) +``` \ No newline at end of file diff --git a/network_sim_swap/pyproject.toml b/network_sim_swap/pyproject.toml new file mode 100644 index 0000000..2d8b4d2 --- /dev/null +++ b/network_sim_swap/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "vonage-network-sim-swap" +version = "0.1.0b0" +description = "Package for working with the Vonage Sim Swap Network API." +readme = "README.md" +authors = [{ name = "Vonage", email = "devrel@vonage.com" }] +requires-python = ">=3.8" +dependencies = [ + "vonage-http-client>=1.4.0", + "vonage-network-auth>=0.1.0b0", + "vonage-utils>=1.1.2", +] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: Apache Software License", +] + +[project.urls] +Homepage = "https://github.com/Vonage/vonage-python-sdk" + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/network_sim_swap/src/vonage_network_sim_swap/BUILD b/network_sim_swap/src/vonage_network_sim_swap/BUILD new file mode 100644 index 0000000..db46e8d --- /dev/null +++ b/network_sim_swap/src/vonage_network_sim_swap/BUILD @@ -0,0 +1 @@ +python_sources() diff --git a/network_sim_swap/src/vonage_network_sim_swap/__init__.py b/network_sim_swap/src/vonage_network_sim_swap/__init__.py new file mode 100644 index 0000000..41b7241 --- /dev/null +++ b/network_sim_swap/src/vonage_network_sim_swap/__init__.py @@ -0,0 +1,4 @@ +from .responses import LastSwapDate, SwapStatus +from .sim_swap import NetworkSimSwap + +__all__ = ['NetworkSimSwap', 'LastSwapDate', 'SwapStatus'] diff --git a/network_sim_swap/src/vonage_network_sim_swap/responses.py b/network_sim_swap/src/vonage_network_sim_swap/responses.py new file mode 100644 index 0000000..80c7634 --- /dev/null +++ b/network_sim_swap/src/vonage_network_sim_swap/responses.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel, Field + + +class SwapStatus(BaseModel): + swapped: str + + +class LastSwapDate(BaseModel): + last_swap_date: str = Field(..., validation_alias='latestSimChange') diff --git a/network_sim_swap/src/vonage_network_sim_swap/sim_swap.py b/network_sim_swap/src/vonage_network_sim_swap/sim_swap.py new file mode 100644 index 0000000..576a9a8 --- /dev/null +++ b/network_sim_swap/src/vonage_network_sim_swap/sim_swap.py @@ -0,0 +1,75 @@ +from pydantic import validate_call +from vonage_http_client import HttpClient +from vonage_network_auth import NetworkAuth + +from .responses import LastSwapDate, SwapStatus + + +class NetworkSimSwap: + """Class containing methods for working with the Vonage SIM Swap Network API.""" + + def __init__(self, http_client: HttpClient): + self._http_client = http_client + self._host = 'api-eu.vonage.com' + + self._auth_type = 'oauth2' + self._network_auth = NetworkAuth(self._http_client) + + @property + def http_client(self) -> HttpClient: + """The HTTP client used to make requests to the Network Sim Swap API. + + Returns: + HttpClient: The HTTP client used to make requests to the Network Sim Swap API. + """ + return self._http_client + + @validate_call + def check(self, phone_number: str, max_age: int = None) -> SwapStatus: + """Check if a SIM swap has been performed in a given time frame. + + Args: + phone_number (str): The phone number to check. Use the E.164 format with + or without a leading +. + max_age (int, optional): Period in hours to be checked for SIM swap. + + Returns: + SwapStatus: Class containing the Swap Status response. + """ + token = self._network_auth.get_oauth2_user_token( + number=phone_number, scope='dpv:FraudPreventionAndDetection#check-sim-swap' + ) + + params = {'phoneNumber': phone_number} + if max_age: + params['maxAge'] = max_age + + return self._http_client.post( + self._host, + '/camara/sim-swap/v040/check', + params, + auth_type=self._auth_type, + token=token, + ) + + @validate_call + def get_last_swap_date(self, phone_number: str) -> LastSwapDate: + """Get the last SIM swap date for a phone number. + + Args: + phone_number (str): The phone number to check. Use the E.164 format with + or without a leading +. + + Returns: + """ + token = self._network_auth.get_oauth2_user_token( + number=phone_number, + scope='dpv:FraudPreventionAndDetection#retrieve-sim-swap-date', + ) + return self._http_client.post( + self._host, + '/camara/sim-swap/v040/retrieve-date', + params={'phoneNumber': phone_number}, + auth_type=self._auth_type, + token=token, + ) diff --git a/network_sim_swap/tests/BUILD b/network_sim_swap/tests/BUILD new file mode 100644 index 0000000..b77e3c3 --- /dev/null +++ b/network_sim_swap/tests/BUILD @@ -0,0 +1 @@ +python_tests(dependencies=['network_sim_swap', 'testutils']) diff --git a/network_sim_swap/tests/data/check_sim_swap.json b/network_sim_swap/tests/data/check_sim_swap.json new file mode 100644 index 0000000..8d90e1b --- /dev/null +++ b/network_sim_swap/tests/data/check_sim_swap.json @@ -0,0 +1,3 @@ +{ + "swapped": true +} \ No newline at end of file diff --git a/network_sim_swap/tests/data/get_swap_date.json b/network_sim_swap/tests/data/get_swap_date.json new file mode 100644 index 0000000..13d4832 --- /dev/null +++ b/network_sim_swap/tests/data/get_swap_date.json @@ -0,0 +1,3 @@ +{ + "latestSimChange": "2023-12-22T04:00:44.000Z" +} \ No newline at end of file diff --git a/network_sim_swap/tests/test_sim_swap.py b/network_sim_swap/tests/test_sim_swap.py new file mode 100644 index 0000000..09927a7 --- /dev/null +++ b/network_sim_swap/tests/test_sim_swap.py @@ -0,0 +1,49 @@ +from os.path import abspath +from unittest.mock import MagicMock, patch + +import responses +from vonage_http_client.http_client import HttpClient +from vonage_network_sim_swap import NetworkSimSwap + +from testutils import build_response, get_mock_jwt_auth + +path = abspath(__file__) + +sim_swap = NetworkSimSwap(HttpClient(get_mock_jwt_auth())) + + +def test_http_client_property(): + http_client = sim_swap.http_client + assert isinstance(http_client, HttpClient) + + +@patch('vonage_network_auth.NetworkAuth.get_oauth2_user_token') +@responses.activate +def test_check_sim_swap(mock_get_oauth2_user_token: MagicMock): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/camara/sim-swap/v040/check', + 'check_sim_swap.json', + ) + mock_get_oauth2_user_token.return_value = 'token' + + response = sim_swap.check('447700900000', max_age=24) + + assert response['swapped'] == True + + +@patch('vonage_network_auth.NetworkAuth.get_oauth2_user_token') +@responses.activate +def test_get_last_swap_date(mock_get_oauth2_user_token: MagicMock): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/camara/sim-swap/v040/retrieve-date', + 'get_swap_date.json', + ) + mock_get_oauth2_user_token.return_value = 'token' + + response = sim_swap.get_last_swap_date('447700900000') + + assert response['latestSimChange'] == '2023-12-22T04:00:44.000Z' diff --git a/pants.toml b/pants.toml index bcbc4d6..4790034 100644 --- a/pants.toml +++ b/pants.toml @@ -1,5 +1,5 @@ [GLOBAL] -pants_version = '2.19.1' +pants_version = '2.21.0' backend_packages = [ 'pants.backend.python', @@ -31,10 +31,13 @@ interpreter_constraints = ['>=3.8'] report = ['html', 'console'] filter = [ 'vonage/src', + 'vonage_network/src', 'http_client/src', 'application/src', 'jwt/src', 'messages/src', + 'network_auth/src', + 'network_sim_swap/src', 'number_insight/src', 'number_insight_v2/src', 'sms/src', diff --git a/requirements.txt b/requirements.txt index ecac15d..e4a7ea2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,8 @@ pyjwt[crypto]>=1.6.4 -e http_client -e jwt -e messages +-e network_auth +-e network_sim_swap -e number_insight -e number_insight_v2 -e sms @@ -16,4 +18,5 @@ pyjwt[crypto]>=1.6.4 -e verify_v2 -e voice -e vonage +-e vonage_network -e vonage_utils diff --git a/verify_v2/CHANGES.md b/verify_v2/CHANGES.md index 71b83f4..85cc501 100644 --- a/verify_v2/CHANGES.md +++ b/verify_v2/CHANGES.md @@ -1,3 +1,6 @@ +# 1.1.2 +- Allow minimum `channel_timeout` value to be 15 seconds + # 1.1.1 - Update minimum dependency version diff --git a/verify_v2/pyproject.toml b/verify_v2/pyproject.toml index 4bca731..3d14664 100644 --- a/verify_v2/pyproject.toml +++ b/verify_v2/pyproject.toml @@ -1,13 +1,13 @@ [project] name = 'vonage-verify-v2' -version = '1.1.1' +version = '1.1.2' description = 'Vonage verify v2 package' readme = "README.md" authors = [{ name = "Vonage", email = "devrel@vonage.com" }] requires-python = ">=3.8" dependencies = [ - "vonage-http-client>=1.3.1", - "vonage-utils>=1.1.1", + "vonage-http-client>=1.4.0", + "vonage-utils>=1.1.2", "pydantic>=2.7.1", ] classifiers = [ diff --git a/verify_v2/src/vonage_verify_v2/requests.py b/verify_v2/src/vonage_verify_v2/requests.py index e4869d6..aa06760 100644 --- a/verify_v2/src/vonage_verify_v2/requests.py +++ b/verify_v2/src/vonage_verify_v2/requests.py @@ -79,7 +79,7 @@ class VerifyRequest(BaseModel): ] ] locale: Optional[Locale] = None - channel_timeout: Optional[int] = Field(None, ge=60, le=900) + channel_timeout: Optional[int] = Field(None, ge=15, le=900) client_ref: Optional[str] = Field(None, min_length=1, max_length=16) code_length: Optional[int] = Field(None, ge=4, le=10) code: Optional[str] = Field(None, pattern=r'^[a-zA-Z0-9]{4,10}$') diff --git a/vonage/CHANGES.md b/vonage/CHANGES.md index 9dcf11d..b9b1649 100644 --- a/vonage/CHANGES.md +++ b/vonage/CHANGES.md @@ -1,3 +1,7 @@ +# 3.99.0a11 +- Remove the Number Insight v2 beta which was not in use and is going to be deprecated +- Lower the VerifyV2 minimum channel timeout to 15s + # 3.99.1a10 - Migrate the Vonage JWT package - Internal refactoring diff --git a/vonage/README.md b/vonage/README.md index 8552f88..a86bc71 100644 --- a/vonage/README.md +++ b/vonage/README.md @@ -30,12 +30,14 @@ options = HttpClientOptions(api_host='api.nexmo.com', timeout=30) vonage = Vonage(auth=auth, http_client_options=options) ``` -The Vonage class provides access to various Vonage APIs through its properties. For example, to use methods to call the Number Insight API v2: +The Vonage class provides access to various Vonage APIs through its properties. For example, to use methods to call the SMS API: ```python -from vonage_number_insight_v2 import FraudCheckRequest +from vonage_sms import SmsMessage -vonage.number_insight_v2.fraud_check(FraudCheckRequest(phone='1234567890')) +message = SmsMessage(to='1234567890', from_='Vonage', text='Hello World') +response = client.sms.send(message) +print(response.model_dump_json(exclude_unset=True)) ``` You can also access the underlying `HttpClient` instance through the `http_client` property: diff --git a/vonage/pyproject.toml b/vonage/pyproject.toml index b3ee559..b52e94d 100644 --- a/vonage/pyproject.toml +++ b/vonage/pyproject.toml @@ -6,16 +6,15 @@ readme = "README.md" authors = [{ name = "Vonage", email = "devrel@vonage.com" }] requires-python = ">=3.8" dependencies = [ - "vonage-utils>=1.1.1", - "vonage-http-client>=1.3.1", + "vonage-utils>=1.1.2", + "vonage-http-client>=1.4.0", "vonage-application>=1.0.0", "vonage-messages>=1.1.1", "vonage-number-insight>=1.0.0", - "vonage-number-insight-v2>=0.1.1b0", "vonage-sms>=1.1.1", "vonage-users>=1.1.2", "vonage-verify>=1.1.1", - "vonage-verify-v2>=1.1.1", + "vonage-verify-v2>=1.1.2", "vonage-voice>=1.0.3", ] classifiers = [ diff --git a/vonage/src/vonage/__init__.py b/vonage/src/vonage/__init__.py index ee39bfe..d3ead5e 100644 --- a/vonage/src/vonage/__init__.py +++ b/vonage/src/vonage/__init__.py @@ -6,7 +6,6 @@ HttpClientOptions, Messages, NumberInsight, - NumberInsightV2, Sms, Users, Verify, @@ -22,7 +21,6 @@ 'Application', 'Messages', 'NumberInsight', - 'NumberInsightV2', 'Sms', 'Users', 'Verify', diff --git a/vonage/src/vonage/_version.py b/vonage/src/vonage/_version.py index 2b24737..6806e89 100644 --- a/vonage/src/vonage/_version.py +++ b/vonage/src/vonage/_version.py @@ -1 +1 @@ -__version__ = '3.99.1a10' +__version__ = '3.99.0a11' diff --git a/vonage/src/vonage/vonage.py b/vonage/src/vonage/vonage.py index f329646..5896df5 100644 --- a/vonage/src/vonage/vonage.py +++ b/vonage/src/vonage/vonage.py @@ -4,7 +4,6 @@ from vonage_http_client import Auth, HttpClient, HttpClientOptions from vonage_messages import Messages from vonage_number_insight import NumberInsight -from vonage_number_insight_v2 import NumberInsightV2 from vonage_sms import Sms from vonage_users import Users from vonage_verify import Verify @@ -35,7 +34,6 @@ def __init__( self.application = Application(self._http_client) self.messages = Messages(self._http_client) self.number_insight = NumberInsight(self._http_client) - self.number_insight_v2 = NumberInsightV2(self._http_client) self.sms = Sms(self._http_client) self.users = Users(self._http_client) self.verify = Verify(self._http_client) diff --git a/vonage_network/BUILD b/vonage_network/BUILD new file mode 100644 index 0000000..6f2c006 --- /dev/null +++ b/vonage_network/BUILD @@ -0,0 +1,11 @@ +resource(name='pyproject', source='pyproject.toml') + +file(name='readme', source='README.md') + +python_distribution( + name='vonage_network', + dependencies=[':pyproject', ':readme', 'vonage_network/src/vonage_network'], + provides=python_artifact(), + generate_setup=False, + repositories=['@pypi'], +) diff --git a/vonage_network/CHANGES.md b/vonage_network/CHANGES.md new file mode 100644 index 0000000..26cbc56 --- /dev/null +++ b/vonage_network/CHANGES.md @@ -0,0 +1,2 @@ +# 0.1.0b0 +- Initial beta release \ No newline at end of file diff --git a/vonage_network/README.md b/vonage_network/README.md new file mode 100644 index 0000000..9265cf4 --- /dev/null +++ b/vonage_network/README.md @@ -0,0 +1,47 @@ +# Vonage Network API Python SDK + +The Vonage Network API Python SDK Package `vonage-network` provides a streamlined interface for using Vonage APIs in Python projects. This package includes the `VonageNetwork` class, which simplifies API interactions. + +The `VonageNetwork` class in this package serves as an entry point for using [Vonage Network APIs](https://developer.vonage.com/en/getting-started-network/what-are-network-apis). It abstracts away complexities with authentication, HTTP requests and more. + +For full API documentation refer to the [Vonage Developer documentation](https://developer.vonage.com). + +Please note this package is in beta and could be subject to change or removal. + +## Installation + +Install the package using pip: + +```bash +pip install vonage-network +``` + +## Usage + +```python +from vonage_network import VonageNetwork, Auth, HttpClientOptions + +# Create an Auth instance +auth = Auth(api_key='your_api_key', api_secret='your_api_secret') + +# Create HttpClientOptions instance +# (not required unless you want to change options from the defaults) +options = HttpClientOptions(api_host='api.nexmo.com', timeout=30) + +# Create a Vonage network client instance +vonage_network = VonageNetwork(auth=auth, http_client_options=options) +``` + +The `VonageNetwork` class provides access to various Vonage Network APIs through its properties. For example, to call the Network Sim Swap API: + +```python +from vonage_network_sim_swap import SwapStatus +swap_status: SwapStatus = vonage_network.sim_swap.check(phone_number='MY_NUMBER') +print(swap_status.swapped) +``` + +You can also access the underlying `HttpClient` instance through the `http_client` property: + +```python +user_agent = vonage_network.http_client.user_agent +``` \ No newline at end of file diff --git a/vonage_network/pyproject.toml b/vonage_network/pyproject.toml new file mode 100644 index 0000000..7ff7b6d --- /dev/null +++ b/vonage_network/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "vonage-network" +dynamic = ["version"] +description = "Python Server SDK for using Vonage Network APIs" +readme = "README.md" +authors = [{ name = "Vonage", email = "devrel@vonage.com" }] +requires-python = ">=3.8" +dependencies = [ + "vonage-utils>=1.1.2", + "vonage-http-client>=1.4.0", + "vonage-network-auth>=0.1.0b0", + "vonage-network-sim-swap>=0.1.0b0", +] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: Apache Software License", +] + +[project.urls] +homepage = "https://github.com/Vonage/vonage-python-sdk" + +[tool.setuptools.dynamic] +version = { attr = "vonage._version.__version__" } + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/vonage_network/src/vonage_network/BUILD b/vonage_network/src/vonage_network/BUILD new file mode 100644 index 0000000..79353bf --- /dev/null +++ b/vonage_network/src/vonage_network/BUILD @@ -0,0 +1 @@ +python_sources(name='vonage_network') diff --git a/vonage_network/src/vonage_network/__init__.py b/vonage_network/src/vonage_network/__init__.py new file mode 100644 index 0000000..2ae6c58 --- /dev/null +++ b/vonage_network/src/vonage_network/__init__.py @@ -0,0 +1,18 @@ +from vonage_utils import VonageError + +from .vonage_network import ( + Auth, + HttpClient, + HttpClientOptions, + NetworkSimSwap, + VonageNetwork, +) + +__all__ = [ + 'VonageError', + 'VonageNetwork', + 'NetworkSimSwap', + 'Auth', + 'HttpClient', + 'HttpClientOptions', +] diff --git a/vonage_network/src/vonage_network/_version.py b/vonage_network/src/vonage_network/_version.py new file mode 100644 index 0000000..db4e81a --- /dev/null +++ b/vonage_network/src/vonage_network/_version.py @@ -0,0 +1 @@ +__version__ = '0.1.0b0' diff --git a/vonage_network/src/vonage_network/vonage_network.py b/vonage_network/src/vonage_network/vonage_network.py new file mode 100644 index 0000000..a056ee0 --- /dev/null +++ b/vonage_network/src/vonage_network/vonage_network.py @@ -0,0 +1,36 @@ +from platform import python_version +from typing import Optional + +from vonage_http_client import Auth, HttpClient, HttpClientOptions +from vonage_network_sim_swap import NetworkSimSwap + +from ._version import __version__ + + +class VonageNetwork: + """Main Server SDK class for using Vonage Network APIs. + + When creating an instance, it will create the authentication objects and + an HTTP Client needed for using Vonage Network APIs. + + Use an instance of this class to access the Vonage Network APIs, e.g. to access + methods associated with the Vonage Sim Swap API, call `vonage_network.sim_swap.method_name()`. + + Args: + auth (Auth): Class dealing with authentication objects and methods. + http_client_options (HttpClientOptions, optional): Options for the HTTP client. + """ + + def __init__( + self, auth: Auth, http_client_options: Optional[HttpClientOptions] = None + ): + self._http_client = HttpClient(auth, http_client_options, __version__) + self._http_client._user_agent = ( + f'vonage-network-python-sdk/{__version__} python/{python_version()}' + ) + + self.sim_swap = NetworkSimSwap(self._http_client) + + @property + def http_client(self): + return self._http_client diff --git a/vonage_network/tests/BUILD b/vonage_network/tests/BUILD new file mode 100644 index 0000000..dabf212 --- /dev/null +++ b/vonage_network/tests/BUILD @@ -0,0 +1 @@ +python_tests() diff --git a/vonage_network/tests/test_vonage_network.py b/vonage_network/tests/test_vonage_network.py new file mode 100644 index 0000000..7bcf7ce --- /dev/null +++ b/vonage_network/tests/test_vonage_network.py @@ -0,0 +1,16 @@ +from vonage_http_client.http_client import HttpClient +from vonage_network._version import __version__ + +from vonage_network import Auth, VonageNetwork + + +def test_create_vonage_class_instance(): + vonage = VonageNetwork(Auth(api_key='asdf', api_secret='qwerasdf')) + + assert vonage.http_client.auth.api_key == 'asdf' + assert vonage.http_client.auth.api_secret == 'qwerasdf' + assert ( + vonage.http_client.auth.create_basic_auth_string() == 'Basic YXNkZjpxd2VyYXNkZg==' + ) + assert type(vonage.http_client) == HttpClient + assert f'vonage-network-python-sdk/{__version__}' in vonage.http_client.user_agent