From c55a5f6c842cba0f87432e6696700f801ee9988f Mon Sep 17 00:00:00 2001 From: maxkahan Date: Tue, 6 Feb 2024 19:57:54 +0000 Subject: [PATCH] add more core logic, refactoring and tests --- http_client/src/http_client/auth.py | 32 ++++++++------ http_client/src/http_client/errors.py | 4 ++ http_client/src/http_client/http_client.py | 30 +++++++------ http_client/tests/data/example_get_text.txt | 1 - http_client/tests/test_auth.py | 41 ++++++++++++++---- http_client/tests/test_http_client.py | 19 -------- libs/tests/test_format_phone_number.py | 10 +---- ni2/src/ni2/ni2.py | 11 ----- {ni2 => number_insight_v2}/BUILD | 0 {ni2 => number_insight_v2}/README.md | 0 {ni2 => number_insight_v2}/pyproject.toml | 0 .../src/number_insight_v2}/BUILD | 0 .../src/number_insight_v2}/__init__.py | 0 .../src/number_insight_v2/errors.py | 5 +++ .../number_insight_v2/number_insight_v2.py | 43 +++++++++++++++++++ {ni2 => number_insight_v2}/tests/BUILD | 0 {ni2 => number_insight_v2}/tests/test_ni2.py | 0 vonage/src/vonage/vonage.py | 39 +++++------------ vonage/tests/test_vonage.py | 12 +++--- 19 files changed, 139 insertions(+), 108 deletions(-) delete mode 100644 http_client/tests/data/example_get_text.txt delete mode 100644 ni2/src/ni2/ni2.py rename {ni2 => number_insight_v2}/BUILD (100%) rename {ni2 => number_insight_v2}/README.md (100%) rename {ni2 => number_insight_v2}/pyproject.toml (100%) rename {ni2/src/ni2 => number_insight_v2/src/number_insight_v2}/BUILD (100%) rename {ni2/src/ni2 => number_insight_v2/src/number_insight_v2}/__init__.py (100%) create mode 100644 number_insight_v2/src/number_insight_v2/errors.py create mode 100644 number_insight_v2/src/number_insight_v2/number_insight_v2.py rename {ni2 => number_insight_v2}/tests/BUILD (100%) rename {ni2 => number_insight_v2}/tests/test_ni2.py (100%) diff --git a/http_client/src/http_client/auth.py b/http_client/src/http_client/auth.py index fd86da4f..99ac7cc5 100644 --- a/http_client/src/http_client/auth.py +++ b/http_client/src/http_client/auth.py @@ -4,7 +4,7 @@ from pydantic import validate_call from vonage_jwt.jwt import JwtClient -from .errors import JWTGenerationError +from .errors import InvalidAuthError, JWTGenerationError class Auth: @@ -15,7 +15,6 @@ class Auth: - api_secret (str): The API secret for authentication. - application_id (str): The application ID for JWT authentication. - private_key (str): The private key for JWT authentication. - - jwt_claims (dict): Additional JWT claims for authentication. Note: To use JWT authentication, provide values for both `application_id` and `private_key`. @@ -28,14 +27,16 @@ def __init__( api_secret: Optional[str] = None, application_id: Optional[str] = None, private_key: Optional[str] = None, - jwt_claims: Optional[dict] = {}, ) -> None: + self._validate_input_combinations( + api_key, api_secret, application_id, private_key + ) + self._api_key = api_key self._api_secret = api_secret if application_id is not None and private_key is not None: self._jwt_client = JwtClient(application_id, private_key) - self._jwt_claims = jwt_claims @property def api_key(self): @@ -45,20 +46,12 @@ def api_key(self): def api_secret(self): return self._api_secret - @property - def jwt_claims(self): - return self._jwt_claims - - @jwt_claims.setter - def jwt_claims(self, claims: dict): - self._jwt_claims = claims - def create_jwt_auth_string(self): return b'Bearer ' + self.generate_application_jwt() def generate_application_jwt(self): try: - return self._jwt_client.generate_application_jwt(self._jwt_claims) + return self._jwt_client.generate_application_jwt() except AttributeError as err: raise JWTGenerationError( 'JWT generation failed. Check that you passed in valid values for "application_id" and "private_key".' @@ -69,3 +62,16 @@ def create_basic_auth_string(self): 'ascii' ) return f'Basic {hash}' + + def _validate_input_combinations( + self, api_key, api_secret, application_id, private_key + ): + if (api_key and not api_secret) or (not api_key and api_secret): + raise InvalidAuthError( + 'Both api_key and api_secret must be set or both must be None.' + ) + + if (application_id and not private_key) or (not application_id and private_key): + raise InvalidAuthError( + 'Both application_id and private_key must be set or both must be None.' + ) diff --git a/http_client/src/http_client/errors.py b/http_client/src/http_client/errors.py index 14c36061..77921c09 100644 --- a/http_client/src/http_client/errors.py +++ b/http_client/src/http_client/errors.py @@ -8,6 +8,10 @@ class JWTGenerationError(VonageError): """Indicates an error with generating a JWT.""" +class InvalidAuthError(VonageError): + """Indicates an error with the authentication credentials provided.""" + + class InvalidHttpClientOptionsError(VonageError): """The options passed to the HTTP Client were invalid.""" diff --git a/http_client/src/http_client/http_client.py b/http_client/src/http_client/http_client.py index 743c8e87..3fe68cfd 100644 --- a/http_client/src/http_client/http_client.py +++ b/http_client/src/http_client/http_client.py @@ -19,6 +19,15 @@ logger = getLogger('vonage-http-client-v2') +class HttpClientOptions(BaseModel): + api_host: str = 'api.nexmo.com' + rest_host: Optional[str] = 'rest.nexmo.com' + timeout: Optional[Annotated[int, Field(ge=0)]] = None + pool_connections: Optional[Annotated[int, Field(ge=1)]] = 10 + pool_maxsize: Optional[Annotated[int, Field(ge=1)]] = 10 + max_retries: Optional[Annotated[int, Field(ge=0)]] = 3 + + class HttpClient: """A synchronous HTTP client used to send authenticated requests to Vonage APIs. @@ -35,7 +44,7 @@ class HttpClient: max_retries (int, optional): The maximum number of retries for HTTP requests. Must be >= 0. Default is 3. """ - def __init__(self, auth: Auth, http_client_options: dict = None): + def __init__(self, auth: Auth, http_client_options: HttpClientOptions = None): self._auth = auth try: if http_client_options is not None: @@ -64,6 +73,10 @@ def __init__(self, auth: Auth, http_client_options: dict = None): self._user_agent = f'vonage-python-sdk python/{python_version()}' self._headers = {'User-Agent': self._user_agent, 'Accept': 'application/json'} + @property + def auth(self): + return self._auth + @property def http_client_options(self): return self._http_client_options @@ -111,26 +124,15 @@ def _parse_response(self, response: Response): if 200 <= response.status_code < 300: if response.status_code == 204: return None - if content_type == 'application/json': - return response.json() - return response.text + return response.json() if response.status_code >= 400: logger.warning( f'Http Response Error! Status code: {response.status_code}; content: {repr(response.text)}; from url: {response.url}' ) - if response.status_code == 401: + if response.status_code == 401 or response.status_code == 403: raise AuthenticationError(response, content_type) elif response.status_code == 429: raise RateLimitedError(response, content_type) elif response.status_code == 500: raise ServerError(response, content_type) raise HttpRequestError(response, content_type) - - -class HttpClientOptions(BaseModel): - api_host: str = 'api.nexmo.com' - rest_host: Optional[str] = 'rest.nexmo.com' - timeout: Optional[Annotated[int, Field(ge=0)]] = None - pool_connections: Optional[Annotated[int, Field(ge=1)]] = 10 - pool_maxsize: Optional[Annotated[int, Field(ge=1)]] = 10 - max_retries: Optional[Annotated[int, Field(ge=0)]] = 3 diff --git a/http_client/tests/data/example_get_text.txt b/http_client/tests/data/example_get_text.txt deleted file mode 100644 index b45ef6fe..00000000 --- a/http_client/tests/data/example_get_text.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, World! \ No newline at end of file diff --git a/http_client/tests/test_auth.py b/http_client/tests/test_auth.py index 361c6bcb..24d4a26f 100644 --- a/http_client/tests/test_auth.py +++ b/http_client/tests/test_auth.py @@ -2,7 +2,7 @@ from unittest.mock import patch from http_client.auth import Auth -from http_client.errors import JWTGenerationError +from http_client.errors import InvalidAuthError, JWTGenerationError from pydantic import ValidationError from pytest import raises from vonage_jwt.jwt import JwtClient @@ -17,7 +17,6 @@ def read_file(path): api_secret = '1234qwerasdfzxcv' application_id = 'asdfzxcv' private_key = read_file('data/dummy_private_key.txt') -jwt_claims = {'iat': 1701729971} def test_create_auth_class_and_get_objects(): @@ -26,12 +25,10 @@ def test_create_auth_class_and_get_objects(): api_secret=api_secret, application_id=application_id, private_key=private_key, - jwt_claims=jwt_claims, ) assert auth.api_key == api_key assert auth.api_secret == api_secret - assert auth.jwt_claims == jwt_claims assert type(auth._jwt_client) == JwtClient @@ -40,11 +37,39 @@ def test_create_new_auth_invalid_type(): Auth(api_key=1234) -def test_set_new_jwt_claims(): - auth = Auth(application_id=application_id, private_key=private_key) - auth.jwt_claims = jwt_claims +def test_auth_init_missing_combinations(): + with raises(InvalidAuthError): + Auth(api_key=api_key) + with raises(InvalidAuthError): + Auth(api_secret=api_secret) + with raises(InvalidAuthError): + Auth(application_id=application_id) + with raises(InvalidAuthError): + Auth(private_key=private_key) + + +def test_auth_init_with_invalid_combinations(): + with raises(InvalidAuthError): + Auth(api_key=api_key, application_id=application_id) + with raises(InvalidAuthError): + Auth(api_key=api_key, private_key=private_key) + with raises(InvalidAuthError): + Auth(api_secret=api_secret, application_id=application_id) + with raises(InvalidAuthError): + Auth(api_secret=api_secret, private_key=private_key) + - assert auth.jwt_claims == jwt_claims +def test_auth_init_with_valid_api_key_and_api_secret(): + auth = Auth(api_key=api_key, api_secret=api_secret) + assert auth._api_key == api_key + assert auth._api_secret == api_secret + + +def test_auth_init_with_valid_application_id_and_private_key(): + auth = Auth(application_id=application_id, private_key=private_key) + assert auth._api_key is None + assert auth._api_secret is None + assert isinstance(auth._jwt_client, JwtClient) test_jwt = b'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBsaWNhdGlvbl9pZCI6ImFzZGYxMjM0IiwiaWF0IjoxNjg1NzMxMzkxLCJqdGkiOiIwYzE1MDJhZS05YmI5LTQ4YzQtYmQyZC0yOGFhNWUxYjZkMTkiLCJleHAiOjE2ODU3MzIyOTF9.mAkGeVgWOb7Mrzka7DSj32vSM8RaFpYse_2E7jCQ4DuH8i32wq9FxXGgfwdBQDHzgku3RYIjLM1xlVrGjNM3MsnZgR7ymQ6S4bdTTOmSK0dKbk91SrN7ZAC9k2a6JpCC2ZYgXpZ5BzpDTdy9BYu6msHKmkL79_aabFAhrH36Nk26pLvoI0-KiGImEex-aRR4iiaXhOebXBeqiQTRPKoKizREq4-8zBQv_j6yy4AiEYvBatQ8L_sjHsLj9jjITreX8WRvEW-G4TPpPLMaHACHTDMpJSOZAnegAkzTV2frVRmk6DyVXnemm4L0RQD1XZDaH7JPsKk24Hd2WZQyIgHOqQ' diff --git a/http_client/tests/test_http_client.py b/http_client/tests/test_http_client.py index 9d9a7338..1e2b4eb4 100644 --- a/http_client/tests/test_http_client.py +++ b/http_client/tests/test_http_client.py @@ -68,25 +68,6 @@ def test_make_get_request_no_content(): assert res == None -@responses.activate -def test_make_get_request_text_response(): - build_response( - path, - 'GET', - 'https://example.com/get_json', - 'example_get_text.txt', - content_type='text/plain', - ) - client = HttpClient( - Auth('asdfqwer', 'asdfqwer1234'), - http_client_options={'api_host': 'example.com'}, - ) - res = client.get(host='example.com', request_path='/get_json') - - assert res == 'Hello, World!' - assert responses.calls[0].response.headers['Content-Type'] == 'text/plain' - - @responses.activate def test_make_post_request(): build_response(path, 'POST', 'https://example.com/post_json', 'example_post.json') diff --git a/libs/tests/test_format_phone_number.py b/libs/tests/test_format_phone_number.py index e3d90ec1..be51f527 100644 --- a/libs/tests/test_format_phone_number.py +++ b/libs/tests/test_format_phone_number.py @@ -20,17 +20,11 @@ def test_format_phone_number_invalid_type(): number = ['1234567890'] with raises(InvalidPhoneNumberTypeError) as e: format_phone_number(number) - assert ( - str(e.value) - == 'The phone number provided has an invalid type. You provided: "". Must be a string or an integer.' - ) + assert '""' in str(e.value) def test_format_phone_number_invalid_format(): number = 'not a phone number' with raises(InvalidPhoneNumberError) as e: format_phone_number(number) - assert ( - str(e.value) - == 'Invalid phone number provided. You provided: "not a phone number".\nUse the E.164 format and start with the country code, e.g. "447700900000".' - ) + assert '"not a phone number"' in str(e.value) diff --git a/ni2/src/ni2/ni2.py b/ni2/src/ni2/ni2.py deleted file mode 100644 index a7ddcb85..00000000 --- a/ni2/src/ni2/ni2.py +++ /dev/null @@ -1,11 +0,0 @@ -from http_client.http_client import HttpClient - - -class NumberInsight2: - """Number Insight API V2.""" - - def __init__(self, http_client: HttpClient) -> None: - self._http_client = http_client - - def _(): - pass diff --git a/ni2/BUILD b/number_insight_v2/BUILD similarity index 100% rename from ni2/BUILD rename to number_insight_v2/BUILD diff --git a/ni2/README.md b/number_insight_v2/README.md similarity index 100% rename from ni2/README.md rename to number_insight_v2/README.md diff --git a/ni2/pyproject.toml b/number_insight_v2/pyproject.toml similarity index 100% rename from ni2/pyproject.toml rename to number_insight_v2/pyproject.toml diff --git a/ni2/src/ni2/BUILD b/number_insight_v2/src/number_insight_v2/BUILD similarity index 100% rename from ni2/src/ni2/BUILD rename to number_insight_v2/src/number_insight_v2/BUILD diff --git a/ni2/src/ni2/__init__.py b/number_insight_v2/src/number_insight_v2/__init__.py similarity index 100% rename from ni2/src/ni2/__init__.py rename to number_insight_v2/src/number_insight_v2/__init__.py diff --git a/number_insight_v2/src/number_insight_v2/errors.py b/number_insight_v2/src/number_insight_v2/errors.py new file mode 100644 index 00000000..ca8a4d7f --- /dev/null +++ b/number_insight_v2/src/number_insight_v2/errors.py @@ -0,0 +1,5 @@ +from errors import VonageError + + +class NumberInsightV2Error(VonageError): + """Indicates an error with the Number Insight v2 Package.""" diff --git a/number_insight_v2/src/number_insight_v2/number_insight_v2.py b/number_insight_v2/src/number_insight_v2/number_insight_v2.py new file mode 100644 index 00000000..c4aa358f --- /dev/null +++ b/number_insight_v2/src/number_insight_v2/number_insight_v2.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from pydantic import BaseModel +from http_client.http_client import HttpClient +from number_insight_v2.errors import NumberInsightV2Error +from copy import deepcopy + +from typing import List, Union + + +class FraudCheckRequest(BaseModel): + """""" + + number: str + insights: Union[str, List[str]] + + +@dataclass +class FraudCheckResponse: ... + + +# phone: Phone +# sim: SimSwap + + +class NumberInsightv2: + """Number Insight API V2.""" + + def __init__(self, http_client: HttpClient) -> None: + self._http_client = deepcopy(http_client) + self._http_client._parse_response = self.response_parser + self._auth_type = 'header' + + def fraud_check(self, number: str, insights: Union[str, List[str]]): + """""" + + def fraud_check(self, request: FraudCheckRequest) -> FraudCheckResponse: + """""" + response = self._http_client.post('/ni/fraud', request.model_dump()) + return FraudCheckResponse(response) + + def response_parser(self, response): + """""" + pass diff --git a/ni2/tests/BUILD b/number_insight_v2/tests/BUILD similarity index 100% rename from ni2/tests/BUILD rename to number_insight_v2/tests/BUILD diff --git a/ni2/tests/test_ni2.py b/number_insight_v2/tests/test_ni2.py similarity index 100% rename from ni2/tests/test_ni2.py rename to number_insight_v2/tests/test_ni2.py diff --git a/vonage/src/vonage/vonage.py b/vonage/src/vonage/vonage.py index 009a82a7..aae5076f 100644 --- a/vonage/src/vonage/vonage.py +++ b/vonage/src/vonage/vonage.py @@ -1,44 +1,25 @@ from http_client.auth import Auth -from http_client.http_client import HttpClient -from ni2.ni2 import NumberInsight2 +from http_client.http_client import HttpClient, HttpClientOptions +from number_insight_v2.number_insight_v2 import NumberInsightv2 + +from typing import Optional class Vonage: """Main Server SDK class for using Vonage APIs. Args: - api_key (str, optional): API key for authentication. - api_secret (str, optional): API secret for authentication. - application_id (str, optional): Application ID for JWT authentication. - private_key (str, optional): Private key for JWT authentication. - jwt_claims (dict, optional): Additional JWT claims for authentication. + auth (Auth): Class dealing with authentication objects and methods. + http_client_options (HttpClientOptions, optional): Options for the HTTP client. """ def __init__( - self, - api_key: str = None, - api_secret: str = None, - application_id: str = None, - private_key: str = None, - jwt_claims: dict = {}, - http_client_options: dict = {}, - ) -> None: - self._auth = Auth( - api_key=api_key, - api_secret=api_secret, - application_id=application_id, - private_key=private_key, - jwt_claims=jwt_claims, - ) - - self._http_client = HttpClient(self._auth, http_client_options) - - self.ni2 = NumberInsight2(self._http_client) + self, auth: Auth, http_client_options: Optional[HttpClientOptions] = None + ): + self._http_client = HttpClient(auth, http_client_options) - @property - def auth(self): - return self._auth + self.number_insight_v2 = NumberInsightv2(self._http_client) @property def http_client(self): diff --git a/vonage/tests/test_vonage.py b/vonage/tests/test_vonage.py index 17d81a18..410e8d27 100644 --- a/vonage/tests/test_vonage.py +++ b/vonage/tests/test_vonage.py @@ -1,11 +1,13 @@ from http_client.http_client import HttpClient -from vonage.vonage import Vonage +from vonage.vonage import Vonage, Auth def test_create_vonage_class_instance(): - vonage = Vonage(api_key='asdf', api_secret='qwerasdf') + vonage = Vonage(Auth(api_key='asdf', api_secret='qwerasdf')) - assert vonage.auth.api_key == 'asdf' - assert vonage.auth.api_secret == 'qwerasdf' - assert vonage.auth.create_basic_auth_string() == 'Basic YXNkZjpxd2VyYXNkZg==' + 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