Skip to content

Commit

Permalink
add sms
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkahan committed Mar 3, 2024
1 parent 715e9f6 commit 38843b4
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 0 deletions.
2 changes: 2 additions & 0 deletions http_client/src/vonage_http_client/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __init__(
api_secret: Optional[str] = None,
application_id: Optional[str] = None,
private_key: Optional[str] = None,
signature: Optional[str] = None,
signature_method: Optional[str] = None,
) -> None:
self._validate_input_combinations(
api_key, api_secret, application_id, private_key
Expand Down
16 changes: 16 additions & 0 deletions sms/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
resource(name='pyproject', source='pyproject.toml')
file(name='readme', source='README.md')

files(sources=['tests/data/*'])

python_distribution(
name='vonage-sms',
dependencies=[
':pyproject',
':readme',
'sms/src/vonage_sms',
],
provides=python_artifact(),
generate_setup=False,
repositories=['@testpypi'],
)
5 changes: 5 additions & 0 deletions sms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Vonage SMS Package

This package contains the code to use Vonage's SMS API with Python.

## Usage
29 changes: 29 additions & 0 deletions sms/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[project]
name = 'vonage-sms'
version = '0.1.0'
description = 'Vonage SMS package'
readme = "README.md"
authors = [{ name = "Vonage", email = "devrel@vonage.com" }]
requires-python = ">=3.8"
dependencies = [
"vonage-http-client>=1.0.0",
"vonage-utils>=1.0.0",
"pydantic>=2.6.1",
]
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"
1 change: 1 addition & 0 deletions sms/src/vonage_sms/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python_sources()
5 changes: 5 additions & 0 deletions sms/src/vonage_sms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .sms import Sms

__all__ = [
'Sms',
]
5 changes: 5 additions & 0 deletions sms/src/vonage_sms/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from vonage_utils.errors import VonageError


class SmsError(VonageError):
"""Indicates an error with the Vonage SMS Package."""
51 changes: 51 additions & 0 deletions sms/src/vonage_sms/sms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from copy import deepcopy
from dataclasses import dataclass
from typing import List, Literal, Optional, Union

from pydantic import BaseModel, Field, field_validator, validate_call
from vonage_http_client.http_client import HttpClient
from vonage_sms.errors import SmsError


class SmsMessage(BaseModel):
to: str
from_: str = Field(..., alias="from")
text: str
type: Optional[str] = None
sig: Optional[str] = Field(None, min_length=16, max_length=60)
status_report_req: Optional[int] = Field(
None,
alias="status-report-req",
description="Set to 1 to receive a Delivery Receipt",
)
client_ref: Optional[str] = Field(
None, alias="client-ref", description="Your own reference. Up to 40 characters."
)
network_code: Optional[str] = Field(
None,
alias="network-code",
description="A 4-5 digit number that represents the mobile carrier network code",
)


@dataclass
class SmsResponse:
id: str


class Sms:
"""Calls Vonage's SMS API."""

def __init__(self, http_client: HttpClient) -> None:
self._http_client = deepcopy(http_client)
self._auth_type = 'basic'

@validate_call
def send(self, message: SmsMessage) -> SmsResponse:
"""Send an SMS message."""
response = self._http_client.post(
self._http_client.api_host,
'/v2/ni',
message.model_dump(),
self._auth_type,
)
1 change: 1 addition & 0 deletions sms/tests/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python_tests(dependencies=['sms', 'testutils'])
19 changes: 19 additions & 0 deletions sms/tests/data/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"request_id": "2c2f5d3f-93ac-42b1-9083-4b14f0d583d3",
"type": "phone",
"phone": {
"phone": "1234567890",
"carrier": "Verizon Wireless",
"type": "MOBILE"
},
"fraud_score": {
"risk_score": "0",
"risk_recommendation": "allow",
"label": "low",
"status": "completed"
},
"sim_swap": {
"status": "completed",
"swapped": false
}
}
15 changes: 15 additions & 0 deletions sms/tests/data/fraud_score.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"request_id": "2c2f5d3f-93ac-42b1-9083-4b14f0d583d3",
"type": "phone",
"phone": {
"phone": "1234567890",
"carrier": "Verizon Wireless",
"type": "MOBILE"
},
"fraud_score": {
"risk_score": "0",
"risk_recommendation": "allow",
"label": "low",
"status": "completed"
}
}
11 changes: 11 additions & 0 deletions sms/tests/data/sim_swap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"request_id": "db5282b6-8046-4217-9c0e-d9c55d8696e9",
"type": "phone",
"phone": {
"phone": "1234567890"
},
"sim_swap": {
"status": "completed",
"swapped": false
}
}
93 changes: 93 additions & 0 deletions sms/tests/test_sms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from dataclasses import asdict
from os.path import abspath

import responses
from pydantic import ValidationError
from pytest import raises
from vonage_http_client.auth import Auth
from vonage_http_client.http_client import HttpClient

from vonage_utils.errors import InvalidPhoneNumberError
from vonage_utils.utils import remove_none_values

from vonage_sms.sms import Sms
from testutils import build_response

path = abspath(__file__)

sms = Sms(HttpClient(Auth('key', 'secret')))


def test_fraud_check_request_defaults():
request = FraudCheckRequest(phone='1234567890')
assert request.type == 'phone'
assert request.phone == '1234567890'
assert request.insights == ['fraud_score', 'sim_swap']


def test_fraud_check_request_custom_insights():
request = FraudCheckRequest(phone='1234567890', insights=['fraud_score'])
assert request.type == 'phone'
assert request.phone == '1234567890'
assert request.insights == ['fraud_score']


def test_fraud_check_request_invalid_phone():
with raises(InvalidPhoneNumberError):
FraudCheckRequest(phone='invalid_phone')
with raises(InvalidPhoneNumberError):
FraudCheckRequest(phone='123')
with raises(InvalidPhoneNumberError):
FraudCheckRequest(phone='12345678901234567890')


def test_fraud_check_request_invalid_insights():
with raises(ValidationError):
FraudCheckRequest(phone='1234567890', insights=['invalid_insight'])


@responses.activate
def test_ni2_defaults():
build_response(path, 'POST', 'https://api.nexmo.com/v2/ni', 'default.json')
request = FraudCheckRequest(phone='1234567890')
response = ni2.fraud_check(request)
assert type(response) == FraudCheckResponse
assert response.request_id == '2c2f5d3f-93ac-42b1-9083-4b14f0d583d3'
assert response.phone.carrier == 'Verizon Wireless'
assert response.fraud_score.risk_score == '0'
assert response.sim_swap.status == 'completed'


@responses.activate
def test_ni2_fraud_score_only():
build_response(path, 'POST', 'https://api.nexmo.com/v2/ni', 'fraud_score.json')
request = FraudCheckRequest(phone='1234567890', insights=['fraud_score'])
response = ni2.fraud_check(request)
assert type(response) == FraudCheckResponse
assert response.request_id == '2c2f5d3f-93ac-42b1-9083-4b14f0d583d3'
assert response.phone.carrier == 'Verizon Wireless'
assert response.fraud_score.risk_score == '0'
assert response.sim_swap is None

clear_response = asdict(response, dict_factory=remove_none_values)
print(clear_response)
assert 'fraud_score' in clear_response
assert 'sim_swap' not in clear_response


@responses.activate
def test_ni2_sim_swap_only():
build_response(path, 'POST', 'https://api.nexmo.com/v2/ni', 'sim_swap.json')
request = FraudCheckRequest(phone='1234567890', insights='sim_swap')
response = ni2.fraud_check(request)
assert type(response) == FraudCheckResponse
assert response.request_id == 'db5282b6-8046-4217-9c0e-d9c55d8696e9'
assert response.phone.phone == '1234567890'
assert response.fraud_score is None
assert response.sim_swap.status == 'completed'
assert response.sim_swap.swapped is False

clear_response = asdict(response, dict_factory=remove_none_values)
assert 'fraud_score' not in clear_response
assert 'sim_swap' in clear_response
assert 'reason' not in clear_response['sim_swap']
2 changes: 2 additions & 0 deletions vonage/src/vonage/vonage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from vonage_http_client.auth import Auth
from vonage_http_client.http_client import HttpClient, HttpClientOptions
from vonage_number_insight_v2.number_insight_v2 import NumberInsightV2
from vonage_sms import Sms

from ._version import __version__

Expand All @@ -21,6 +22,7 @@ def __init__(
self._http_client = HttpClient(auth, http_client_options, __version__)

self.number_insight_v2 = NumberInsightV2(self._http_client)
self.sms = Sms(self._http_client)

@property
def http_client(self):
Expand Down

0 comments on commit 38843b4

Please sign in to comment.