Skip to content

Commit

Permalink
add more core logic, refactoring and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkahan committed Feb 6, 2024
1 parent b14a228 commit c55a5f6
Show file tree
Hide file tree
Showing 19 changed files with 139 additions and 108 deletions.
32 changes: 19 additions & 13 deletions http_client/src/http_client/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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`.
Expand All @@ -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):
Expand All @@ -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".'
Expand All @@ -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.'
)
4 changes: 4 additions & 0 deletions http_client/src/http_client/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
30 changes: 16 additions & 14 deletions http_client/src/http_client/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 0 additions & 1 deletion http_client/tests/data/example_get_text.txt

This file was deleted.

41 changes: 33 additions & 8 deletions http_client/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand All @@ -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


Expand All @@ -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'
Expand Down
19 changes: 0 additions & 19 deletions http_client/tests/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
10 changes: 2 additions & 8 deletions libs/tests/test_format_phone_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<class \'list\'>". Must be a string or an integer.'
)
assert '"<class \'list\'>"' 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)
11 changes: 0 additions & 11 deletions ni2/src/ni2/ni2.py

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions number_insight_v2/src/number_insight_v2/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from errors import VonageError


class NumberInsightV2Error(VonageError):
"""Indicates an error with the Number Insight v2 Package."""
43 changes: 43 additions & 0 deletions number_insight_v2/src/number_insight_v2/number_insight_v2.py
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
File renamed without changes.
39 changes: 10 additions & 29 deletions vonage/src/vonage/vonage.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Loading

0 comments on commit c55a5f6

Please sign in to comment.