Skip to content

Commit a4c6e5a

Browse files
committed
add new models, change structure, start implementing pagination
1 parent 14237bb commit a4c6e5a

File tree

7 files changed

+148
-33
lines changed

7 files changed

+148
-33
lines changed

testutils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .testutils import build_response
21
from .mock_auth import get_mock_api_key_auth, get_mock_jwt_auth
2+
from .testutils import build_response
33

44
__all__ = ['build_response', 'get_mock_api_key_auth', 'get_mock_jwt_auth']

testutils/mock_auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from os.path import join, dirname
1+
from os.path import dirname, join
22

33
from vonage_http_client.auth import Auth
44

users/src/vonage_users/common.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from typing import Dict, List, Optional
2+
3+
from pydantic import BaseModel, Field, HttpUrl
4+
from typing_extensions import Annotated
5+
6+
PhoneNumber = Annotated[str, Field(pattern='^[1-9]\d{6,14}$')]
7+
8+
9+
class PstnChannel(BaseModel):
10+
number: int
11+
12+
13+
class SipChannel(BaseModel):
14+
uri: str = Field(..., pattern='^(sip|sips):\+?([\w|:.\-@;,=%&]+)')
15+
username: str = None
16+
password: str = None
17+
18+
19+
class VbcChannel(BaseModel):
20+
extension: str
21+
22+
23+
class WebsocketChannel(BaseModel):
24+
uri: str = Field(pattern='^(ws|wss)://[a-zA-Z0-9~#%@&-_?\/.,:;)(][]*$')
25+
content_type: str = Field(pattern="^audio/l16;rate=(8000|16000)$")
26+
headers: Optional[Dict[str, str]] = None
27+
28+
29+
class SmsChannel(BaseModel):
30+
number: PhoneNumber
31+
32+
33+
class MmsChannel(BaseModel):
34+
number: PhoneNumber
35+
36+
37+
class WhatsappChannel(BaseModel):
38+
number: PhoneNumber
39+
40+
41+
class ViberChannel(BaseModel):
42+
number: PhoneNumber
43+
44+
45+
class MessengerChannel(BaseModel):
46+
id: str
47+
48+
49+
class Channels(BaseModel):
50+
pstn: Optional[List[PstnChannel]] = None
51+
sip: Optional[List[SipChannel]] = None
52+
vbc: Optional[List[VbcChannel]] = None
53+
websocket: Optional[List[WebsocketChannel]] = None
54+
sms: Optional[List[SmsChannel]] = None
55+
mms: Optional[List[MmsChannel]] = None
56+
whatsapp: Optional[List[WhatsappChannel]] = None
57+
viber: Optional[List[ViberChannel]] = None
58+
messenger: Optional[List[MessengerChannel]] = None
59+
60+
61+
class Properties(BaseModel):
62+
custom_data: Optional[Dict[str, str]]
63+
64+
65+
class User(BaseModel):
66+
name: Optional[str] = Field(None, example="my_user_name")
67+
display_name: Optional[str] = Field(None, example="My User Name")
68+
image_url: Optional[HttpUrl] = Field(None, example="https://example.com/image.png")
69+
properties: Optional[Properties]
70+
channels: Optional[Channels]

users/src/vonage_users/requests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from typing import Literal, Optional
22

3-
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
3+
from pydantic import BaseModel, Field
44

55

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

9-
page_size: Optional[int] = Field(None, ge=1, le=100)
9+
page_size: Optional[int] = Field(10, ge=1, le=100)
1010
order: Optional[Literal['asc', 'desc', 'ASC', 'DESC']] = None
1111
cursor: Optional[str] = Field(
1212
None,

users/src/vonage_users/responses.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ class ListUsersResponse(BaseModel):
3535
links: Links = Field(..., validation_alias='_links')
3636

3737

38+
class CreateUserResponse(BaseModel):
39+
id: str
40+
name: str
41+
display_name: str
42+
links: UserLinks = Field(..., validation_alias='_links')
43+
44+
3845
# class MessageResponse(BaseModel):
3946
# to: str
4047
# message_id: str = Field(..., validation_alias='message-id')

users/src/vonage_users/users.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,69 @@
1-
from typing import Optional
2-
from pydantic import validate_call
1+
from typing import Generator, Literal, Optional
2+
3+
from pydantic import BaseModel, validate_call
34
from vonage_http_client.http_client import HttpClient
45

5-
from .errors import UsersError
6+
from .common import User
67
from .requests import ListUsersRequest
7-
from .responses import ListUsersResponse
8+
from .responses import CreateUserResponse, ListUsersResponse
9+
10+
11+
class Filters(BaseModel):
12+
order: Optional[Literal['asc', 'desc', 'ASC', 'DESC']] = None
13+
14+
15+
import urllib.parse
16+
17+
18+
def parse_cursor_from_url(url: str) -> Optional[str]:
19+
"""Extract the cursor from the "next" URL."""
20+
query_string = urllib.parse.urlparse(url).query
21+
params = urllib.parse.parse_qs(query_string)
22+
return params.get('cursor', [None])[0]
823

924

1025
class Users:
1126
"""Class containing methods for user management.
1227
13-
When using APIs that require a Vonage Application to be created,
14-
you can create users to associate with that application.
28+
When using APIs that require a Vonage Application to be created, you can create users to
29+
associate with that application.
1530
"""
1631

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

2136
@validate_call
22-
def list_users(self, params: Optional[ListUsersRequest] = None) -> ListUsersResponse:
23-
"""List all users."""
24-
response = self._http_client.get(
37+
def list_users(
38+
self,
39+
order: Literal['asc', 'desc', 'ASC', 'DESC'] = None,
40+
name: str = None,
41+
) -> Generator:
42+
"""List all users with pagination handled by a generator."""
43+
cursor = None
44+
while True:
45+
params = ListUsersRequest(order=order, cursor=cursor, name=name)
46+
response = self._http_client.get(
47+
self._http_client.api_host,
48+
'/v1/users',
49+
# need to send the right stuff to the api
50+
params.model_dump() if params is not None else None,
51+
self._auth_type,
52+
)
53+
users = ListUsersResponse(**response)
54+
for user in users.embedded.users:
55+
yield user
56+
if not users.links.next:
57+
break
58+
cursor = parse_cursor_from_url(users.links.next.href)
59+
60+
@validate_call
61+
def create_user(self, params: Optional[User]):
62+
"""Create a user."""
63+
response = self._http_client.post(
2564
self._http_client.api_host,
2665
'/v1/users',
2766
params.model_dump() if params is not None else None,
2867
self._auth_type,
2968
)
30-
return ListUsersResponse(**response)
69+
return CreateUserResponse(**response)

users/tests/test_users.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
from os.path import abspath
22

33
import responses
4-
from pydantic import ValidationError
5-
from pytest import raises
6-
from vonage_http_client.auth import Auth
7-
from vonage_http_client.errors import HttpRequestError
84
from vonage_http_client.http_client import HttpClient
95
from vonage_users import Users
10-
from vonage_users.errors import UsersError
116
from vonage_users.requests import ListUsersRequest
12-
from vonage_users.responses import ListUsersResponse
137

148
from testutils import build_response, get_mock_jwt_auth
159

@@ -33,18 +27,23 @@ def test_create_list_users_request():
3327
@responses.activate
3428
def test_list_users():
3529
build_response(path, 'GET', 'https://api.nexmo.com/v1/users', 'list_users.json')
36-
response: ListUsersResponse = users.list_users()
37-
assert response.page_size == 10
38-
assert response.embedded.users[0].id == 'USR-82e028d9-5201-4f1e-8188-604b2d3471fd'
39-
assert response.embedded.users[0].name == 'my_user_name'
40-
assert response.embedded.users[0].display_name == 'My User Name'
41-
assert (
42-
response.embedded.users[0].links.self.href
43-
== 'https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471fd'
44-
)
45-
assert (
46-
response.links.self.href
47-
== 'https://api.nexmo.com/v1/users?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg='
48-
)
30+
users_generator = users.list_users()
31+
print(next(users_generator))
32+
# for user in users_generator:
33+
# print(user)
34+
assert 0
35+
36+
# assert response.page_size == 10
37+
# assert response.embedded.users[0].id == 'USR-82e028d9-5201-4f1e-8188-604b2d3471fd'
38+
# assert response.embedded.users[0].name == 'my_user_name'
39+
# assert response.embedded.users[0].display_name == 'My User Name'
40+
# assert (
41+
# response.embedded.users[0].links.self.href
42+
# == 'https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471fd'
43+
# )
44+
# assert (
45+
# response.links.self.href
46+
# == 'https://api.nexmo.com/v1/users?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg='
47+
# )
4948

5049
# print(response.model_dump_json(indent=2))

0 commit comments

Comments
 (0)