Skip to content

Commit

Permalink
adding users api structure and list endpoint, refactoring and testing
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkahan committed Mar 26, 2024
1 parent b4657c0 commit 14237bb
Show file tree
Hide file tree
Showing 21 changed files with 412 additions and 13 deletions.
3 changes: 2 additions & 1 deletion pants.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[GLOBAL]
pants_version = '2.19.0'
pants_version = '2.19.1'

backend_packages = [
'pants.backend.python',
Expand Down Expand Up @@ -34,6 +34,7 @@ filter = [
'http_client/src',
'number_insight_v2/src',
'sms/src',
'users/src',
'utils/src',
'testutils',
]
Expand Down
12 changes: 2 additions & 10 deletions sms/src/vonage_sms/responses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List, Optional

from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field


class MessageResponse(BaseModel):
Expand All @@ -16,12 +16,4 @@ class MessageResponse(BaseModel):

class SmsResponse(BaseModel):
message_count: str = Field(..., validation_alias='message-count')
messages: List[dict]

@field_validator('messages')
@classmethod
def create_message_response(cls, value):
messages = []
for message in value:
messages.append(MessageResponse(**message))
return messages
messages: List[MessageResponse]
23 changes: 23 additions & 0 deletions sms/tests/data/send_long_sms.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"message-count": "2",
"messages": [
{
"to": "1234567890",
"message-id": "62dfdf68-6c7c-479a-a190-5c52f798a787",
"status": "0",
"remaining-balance": "37.43563628",
"message-price": "0.04120000",
"network": "23420",
"client-ref": "ref123"
},
{
"to": "1234567890",
"message-id": "72ff9536-62d6-455a-9f0b-65f3c265b423",
"status": "0",
"remaining-balance": "37.43563628",
"message-price": "0.04120000",
"network": "23420",
"client-ref": "ref123"
}
]
}
10 changes: 10 additions & 0 deletions sms/tests/test_sms.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ def test_send_message():
assert response.messages[0].network == '23420'


@responses.activate
def test_send_long_message():
build_response(path, 'POST', 'https://rest.nexmo.com/sms/json', 'send_long_sms.json')
message = SmsMessage(to='1234567890', from_='Acme Inc.', text='Hello, World!')
response = sms.send(message)
assert response.message_count == '2'
assert response.messages[0].message_id == '62dfdf68-6c7c-479a-a190-5c52f798a787'
assert response.messages[1].message_id == '72ff9536-62d6-455a-9f0b-65f3c265b423'


@responses.activate
def test_send_message_with_signature():
sms = Sms(
Expand Down
4 changes: 3 additions & 1 deletion testutils/BUILD
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
python_sources()
file(name='fake_private_key', source='data/fake_private_key.txt')

python_sources(dependencies=[':fake_private_key'])
3 changes: 2 additions & 1 deletion testutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .testutils import build_response
from .mock_auth import get_mock_api_key_auth, get_mock_jwt_auth

__all__ = ['build_response']
__all__ = ['build_response', 'get_mock_api_key_auth', 'get_mock_jwt_auth']
28 changes: 28 additions & 0 deletions testutils/data/fake_private_key.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQdAHqJHs/a+Ra
2ubvSd1vz/aWlJ9BqnMUtB7guTlyggdENAbleIkzep6mUHepDJdQh8Qv6zS3lpUe
K0UkDfr1/FvsvxurGw/YYPagUEhP/HxMbs2rnQTiAdWOT+Ux9vPABoyNYvZB90xN
IVhBDRWgkz1HPQBRNjFcm3NOol83h5Uwp5YroGTWx+rpmIiRhQj3mv6luk102d95
4ulpPpzcYWKIpJNdclJrEkBZaghDZTOpbv79qd+ds9AVp1j8i9cG/owBJpsJWxfw
StMDpNeEZqopeQWmA121sSEsxpAbKJ5DA7F/lmckx74sulKHX1fDWT76cRhloaEQ
VmETdj0VAgMBAAECggEAZ+SBtchz8vKbsBqtAbM/XcR5Iqi1TR2eWMHDJ/65HpSm
+XuyujjerN0e6EZvtT4Uxmq8QaPJNP0kmhI31hXvsB0UVcUUDa4hshb1pIYO3Gq7
Kr8I29EZB2mhndm9Ii9yYhEBiVA66zrNeR225kkWr97iqjhBibhoVr8Vc6oiqcIP
nFy5zSFtQSkhucaPge6rW00JSOD3wg2GM+rgS6r22t8YmqTzAwvwfil5pQfUngal
oywqLOf6CUYXPBleJc1KgaIIP/cSvqh6b/t25o2VXnI4rpRhtleORvYBbH6K6xLa
OWgg6B58T+0/QEqtZIAn4miYtVCkYLB78Ormc7Q9ewKBgQDuSytuYqxdZh/L/RDU
CErFcNO5I1e9fkLAs5dQEBvvdQC74+oA1MsDEVv0xehFa1JwPKSepmvB2UznZg9L
CtR7QKMDZWvS5xx4j0E/b+PiNQ/tlcFZB2UZ0JwviSxdd7omOTscq9c3RIhFHar1
Y38Fixkfm44Ij/K3JqIi2v2QMwKBgQDf8TYOOmAr9UuipUDxMsRSqTGVIY8B+aEJ
W+2aLrqJVkLGTRfrbjzXWYo3+n7kNJjFgNkltDq6HYtufHMYRs/0PPtNR0w0cDPS
Xr7m2LNHTDcBalC/AS4yKZJLNLm+kXA84vkw4qiTjc0LSFxJkouTQzkea0l8EWHt
zRMv/qYVlwKBgBaJOWRJJK/4lo0+M7c5yYh+sSdTNlsPc9Sxp1/FBj9RO26JkXne
pgx2OdIeXWcjTTqcIZ13c71zhZhkyJF6RroZVNFfaCEcBk9IjQ0o0c504jq/7Pc0
gdU9K2g7etykFBDFXNfLUKFDc/fFZIOskzi8/PVGStp4cqXrm23cdBqNAoGBAKtf
A2bP9ViuVjsZCyGJIAPBxlfBXpa8WSe4WZNrvwPqJx9pT6yyp4yE0OkVoJUyStaZ
S5M24NocUd8zDUC+r9TP9d+leAOI+Z87MgumOUuOX2mN2kzQsnFgrrsulhXnZmSx
rNBkI20HTqobrcP/iSAgiU1l/M4c3zwDe3N3A9HxAoGBAM2hYu0Ij6htSNgo/WWr
IEYYXuwf8hPkiuwzlaiWhD3eocgd4S8SsBu/bTCY19hQ2QbBPaYyFlNem+ynQyXx
IOacrgIHCrYnRCxjPfFF/MxgUHJb8ZoiexprP/FME5p0PoRQIEFYa+jVht3hT5wC
9aedWufq4JJb+akO6MVUjTvs
-----END PRIVATE KEY-----
19 changes: 19 additions & 0 deletions testutils/mock_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from os.path import join, dirname

from vonage_http_client.auth import Auth


def read_file(path):
with open(join(dirname(__file__), path)) as input_file:
return input_file.read()


def get_mock_api_key_auth():
return Auth(api_key='test_api_key', api_secret='test_api_secret')


def get_mock_jwt_auth():
return Auth(
application_id='test_application_id',
private_key=read_file('data/fake_private_key.txt'),
)
16 changes: 16 additions & 0 deletions users/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-users',
dependencies=[
':pyproject',
':readme',
'users/src/vonage_users',
],
provides=python_artifact(),
generate_setup=False,
repositories=['@pypi'],
)
2 changes: 2 additions & 0 deletions users/CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# 1.0.0
- Initial upload
31 changes: 31 additions & 0 deletions users/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Vonage Users Package

This package contains the code to use Vonage's Users API in Python.

It includes methods for managing users.

## Usage

It is recommended to use this as part of the main `vonage` package. The examples below assume you've created an instance of the `vonage.Vonage` class called `vonage_client`.

### List Users

### Create a New User

### Get a User

### Update a User

### Delete a User

<!-- ### Manage a User
Create a `User` object, then pass into the `Users.create` or `Users.update` method.
```python
from vonage_users import User, UserResponse
user = User(name='John Doe', email='john.doe@example.com')
response: UserResponse = vonage_client.users.create(user)
print(response.model_dump(exclude_unset=True)) -->
29 changes: 29 additions & 0 deletions users/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[project]
name = 'vonage-users'
version = '1.0.0'
description = 'Vonage SMS package'
readme = "README.md"
authors = [{ name = "Vonage", email = "devrel@vonage.com" }]
requires-python = ">=3.8"
dependencies = [
"vonage-http-client>=1.1.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 users/src/vonage_users/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python_sources()
8 changes: 8 additions & 0 deletions users/src/vonage_users/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# from .errors import PartialFailureError, SmsError
# from .requests import SmsMessage
# from .responses import MessageResponse, SmsResponse
from .users import Users

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


class UsersError(VonageError):
"""Indicates an error with the Vonage Users Package."""
60 changes: 60 additions & 0 deletions users/src/vonage_users/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import Literal, Optional

from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator


class ListUsersRequest(BaseModel):
"""Request object for listing users."""

page_size: Optional[int] = Field(None, ge=1, le=100)
order: Optional[Literal['asc', 'desc', 'ASC', 'DESC']] = None
cursor: Optional[str] = Field(
None,
description="The cursor to start returning results from. You are not expected to provide this manually, but to follow the url provided in _links.next.href or _links.prev.href in the response which contains a cursor value.",
)
name: Optional[str] = None


# class SmsMessage(BaseModel):
# """Message object containing the data and options for an SMS message."""

# to: str
# from_: str = Field(..., serialization_alias='from')
# text: str
# sig: Optional[str] = Field(None, min_length=16, max_length=60)
# client_ref: Optional[str] = Field(
# None, serialization_alias='client-ref', max_length=100
# )
# type: Optional[Literal['text', 'binary', 'unicode']] = None
# ttl: Optional[int] = Field(None, ge=20000, le=604800000)
# status_report_req: Optional[bool] = Field(
# None, serialization_alias='status-report-req'
# )
# callback: Optional[str] = Field(None, max_length=100)
# message_class: Optional[int] = Field(
# None, serialization_alias='message-class', ge=0, le=3
# )
# body: Optional[str] = None
# udh: Optional[str] = None
# protocol_id: Optional[int] = Field(
# None, serialization_alias='protocol-id', ge=0, le=255
# )
# account_ref: Optional[str] = Field(None, serialization_alias='account-ref')
# entity_id: Optional[str] = Field(None, serialization_alias='entity-id')
# content_id: Optional[str] = Field(None, serialization_alias='content-id')

# @field_validator('body', 'udh')
# @classmethod
# def validate_body(cls, value, info: ValidationInfo):
# data = info.data
# if 'type' not in data or not data['type'] == 'binary':
# raise ValueError(
# 'This parameter can only be set when the "type" parameter is set to "binary".'
# )
# return value

# @model_validator(mode='after')
# def validate_type(self) -> 'SmsMessage':
# if self.type == 'binary' and self.body is None and self.udh is None:
# raise ValueError('This parameter is required for binary messages.')
# return self
59 changes: 59 additions & 0 deletions users/src/vonage_users/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import List, Optional

from pydantic import BaseModel, Field


class Link(BaseModel):
href: str


class UserLinks(BaseModel):
self: Link


class Links(BaseModel):
self: Link
first: Link
next: Optional[Link] = None
prev: Optional[Link] = None


class User(BaseModel):
id: Optional[str]
name: Optional[str]
display_name: Optional[str]
links: Optional[UserLinks] = Field(..., validation_alias='_links')


class Embedded(BaseModel):
users: List[User] = []


class ListUsersResponse(BaseModel):
page_size: int
embedded: Embedded = Field(..., validation_alias='_embedded')
links: Links = Field(..., validation_alias='_links')


# class MessageResponse(BaseModel):
# to: str
# message_id: str = Field(..., validation_alias='message-id')
# status: str
# remaining_balance: str = Field(..., validation_alias='remaining-balance')
# message_price: str = Field(..., validation_alias='message-price')
# network: str
# client_ref: Optional[str] = Field(None, validation_alias='client-ref')
# account_ref: Optional[str] = Field(None, validation_alias='account-ref')


# class SmsResponse(BaseModel):
# message_count: str = Field(..., validation_alias='message-count')
# messages: List[dict]

# @field_validator('messages')
# @classmethod
# def create_message_response(cls, value):
# messages = []
# for message in value:
# messages.append(MessageResponse(**message))
# return messages
30 changes: 30 additions & 0 deletions users/src/vonage_users/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Optional
from pydantic import validate_call
from vonage_http_client.http_client import HttpClient

from .errors import UsersError
from .requests import ListUsersRequest
from .responses import ListUsersResponse


class Users:
"""Class containing methods for user management.
When using APIs that require a Vonage Application to be created,
you can create users to associate with that application.
"""

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

@validate_call
def list_users(self, params: Optional[ListUsersRequest] = None) -> ListUsersResponse:
"""List all users."""
response = self._http_client.get(
self._http_client.api_host,
'/v1/users',
params.model_dump() if params is not None else None,
self._auth_type,
)
return ListUsersResponse(**response)
1 change: 1 addition & 0 deletions users/tests/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python_tests(dependencies=['users', 'testutils'])
Loading

0 comments on commit 14237bb

Please sign in to comment.