Skip to content

Commit f3ce002

Browse files
committed
edits to voice api models
1 parent 85a7fc0 commit f3ce002

File tree

10 files changed

+80
-8
lines changed

10 files changed

+80
-8
lines changed

voice/CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 1.3.0
2+
- Add new `headers` and `standard_headers` options to the `Sip` data model
3+
- Add new `standardHeaders` option to the `SipEndpoint` NCCO model
4+
- Add check for invalid hostnames when downloading a recording with `Voice.download_recording`
5+
- Allow the `CreateCallRequest` model to accept a SIP URI as well as a phone number in the `from_` field
6+
17
# 1.2.0
28
- Make all models originally accessed by `vonage_voice.models.***` available at the top level of the package, i.e. `vonage_voice.***`
39

voice/src/vonage_voice/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.2.0'
1+
__version__ = '1.3.0'

voice/src/vonage_voice/models/common.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pydantic import BaseModel, Field
44
from vonage_utils.types import PhoneNumber, SipUri
55
from vonage_voice.models.enums import Channel
6+
from vonage_voice.models.input_types import Dtmf
67

78

89
class Phone(BaseModel):
@@ -21,9 +22,14 @@ class Sip(BaseModel):
2122
2223
Args:
2324
uri (SipUri): The SIP URI.
25+
headers (Optional[dict]): Metadata to include in the request. The headers are transmitted as part of the SIP INVITE as `X-key: value` headers.
26+
standard_headers (Optional[dict]): Standard SIP headers to include in the request. Unlike `headers`, these are not prepended with `X-`.
27+
This should be of the form `{'User-to-User': '342342ef34;encoding=hex'}
2428
"""
2529

2630
uri: SipUri
31+
headers: Optional[dict] = None
32+
standard_headers: Optional[dict] = None
2733
type: Channel = Channel.SIP
2834

2935

voice/src/vonage_voice/models/connect_endpoints.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,14 @@ class SipEndpoint(BaseModel):
7373
headers (Optional[dict]): The headers to include with the SIP connection. To use
7474
TLS and/or SRTP, include respectively `transport=tls` or `media=srtp` to the URL with
7575
the semicolon `;` as a delimiter.
76+
standardHeaders (Optional[dict]): Standard SIP headers to include in the request. Unlike
77+
`headers`, these are not prepended with `X-`. This should be of the form
78+
`{'User-to-User': '342342ef34;encoding=hex'}`.
7679
"""
7780

7881
uri: SipUri
7982
headers: Optional[dict] = None
83+
standardHeaders: Optional[dict] = None
8084
type: ConnectEndpointType = ConnectEndpointType.SIP
8185

8286

voice/src/vonage_voice/models/requests.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ class CreateCallRequest(BaseModel):
3030
answer_method (Optional[Literal['POST', 'GET']]): The HTTP method used to send
3131
event information to `answer_url`.
3232
to (list[Union[ToPhone, Sip, Websocket, Vbc]]): The type of connection to call.
33-
from_ (Optional[Phone]): The phone number to use when calling. Mutually exclusive
34-
with the `random_from_number` property.
33+
from_ (Optional[Union[Phone, str]]): The phone number or SIP URI to use when calling. Mutually exclusive with the `random_from_number` property.
3534
random_from_number (Optional[bool]): Whether to use a random number as the caller's
3635
phone number. The number will be selected from the list of the numbers assigned
3736
to the current application. Mutually exclusive with the `from_` property.
@@ -60,8 +59,7 @@ class CreateCallRequest(BaseModel):
6059
answer_url: list[str] = None
6160
answer_method: Optional[Literal['POST', 'GET']] = None
6261
to: list[Union[ToPhone, Sip, Websocket, Vbc]]
63-
64-
from_: Optional[Phone] = Field(None, serialization_alias='from')
62+
from_: Optional[Union[Phone, str]] = Field(None, serialization_alias='from')
6563
random_from_number: Optional[bool] = None
6664
event_url: Optional[list[str]] = None
6765
event_method: Optional[Literal['POST', 'GET']] = None

voice/src/vonage_voice/voice.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from vonage_http_client.http_client import HttpClient
55
from vonage_jwt.verify_jwt import verify_signature
66
from vonage_utils.types import Dtmf
7+
from vonage_voice.errors import VoiceError
78
from vonage_voice.models.ncco import NccoAction
89

910
from .models.requests import (
@@ -272,6 +273,9 @@ def download_recording(self, url: str, file_path: str) -> None:
272273
url (str): The URL of the recording to get.
273274
file_path (str): The path to save the recording to.
274275
"""
276+
if not 'vonage.com' in url and not 'nexmo.com' in url:
277+
raise VoiceError('The recording URL must be from a Vonage or Nexmo hostname.')
278+
275279
self._http_client.download_file_stream(url=url, file_path=file_path)
276280

277281
@validate_call

voice/tests/test_ncco_actions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,13 @@ def test_create_connect_endpoints():
113113
}
114114

115115
assert connect_endpoints.SipEndpoint(
116-
uri='sip:example@sip.example.com', headers={'qwer': 'asdf'}
116+
uri='sip:example@sip.example.com',
117+
headers={'qwer': 'asdf'},
118+
standardHeaders={'User-to-User': '342342ef34;encoding=hex'},
117119
).model_dump() == {
118120
'uri': 'sip:example@sip.example.com',
119121
'headers': {'qwer': 'asdf'},
122+
'standardHeaders': {'User-to-User': '342342ef34;encoding=hex'},
120123
'type': 'sip',
121124
}
122125

voice/tests/test_voice.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from os.path import abspath
23

34
import responses
@@ -8,6 +9,7 @@
89
AudioStreamOptions,
910
CreateCallRequest,
1011
ListCallsFilter,
12+
Sip,
1113
TtsStreamOptions,
1214
)
1315
from vonage_voice.errors import VoiceError
@@ -35,9 +37,42 @@ def test_create_call_basic_ncco():
3537
ncco = [Talk(text='Hello world')]
3638
call = CreateCallRequest(
3739
ncco=ncco,
38-
to=[{'type': 'sip', 'uri': 'sip:test@example.com'}],
40+
to=[
41+
Sip(
42+
uri='sip:test@example.com',
43+
headers={'location': 'New York City'},
44+
standard_headers={'User-to-User': '342342ef34;encoding=hex'},
45+
)
46+
],
3947
random_from_number=True,
4048
)
49+
50+
response = voice.create_call(call)
51+
52+
body = json.loads(voice.http_client.last_request.body)
53+
assert body['to'][0]['headers'] == {'location': 'New York City'}
54+
assert body['to'][0]['standard_headers'] == {
55+
'User-to-User': '342342ef34;encoding=hex'
56+
}
57+
assert type(response) == CreateCallResponse
58+
assert response.uuid == '106a581a-34d0-432a-a625-220221fd434f'
59+
assert response.status == 'started'
60+
assert response.direction == 'outbound'
61+
assert response.conversation_uuid == 'CON-2be039b2-d0a4-4274-afc8-d7b241c7c044'
62+
63+
64+
@responses.activate
65+
def test_create_call_basic_ncco_from_sip():
66+
build_response(
67+
path, 'POST', 'https://api.nexmo.com/v1/calls', 'create_call.json', 201
68+
)
69+
ncco = [Talk(text='Hello world')]
70+
call = CreateCallRequest(
71+
ncco=ncco,
72+
to=[Sip(uri='sip:test@example.com')],
73+
from_='sip:from_sip_uri@example.com',
74+
)
75+
4176
response = voice.create_call(call)
4277

4378
assert type(response) == CreateCallResponse
@@ -429,6 +464,15 @@ def test_download_recording():
429464
assert file_content.startswith(b'ID3')
430465

431466

467+
def test_download_recording_invalid_url():
468+
with raises(VoiceError) as e:
469+
voice.download_recording(
470+
url='https://invalid.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
471+
file_path='voice/tests/data/file_stream.mp3',
472+
)
473+
assert e.match('The recording URL must be from a Vonage or Nexmo hostname.')
474+
475+
432476
def test_verify_signature():
433477
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2OTc2MzQ2ODAsImV4cCI6MzMyNTQ1NDA4MjgsImF1ZCI6IiIsInN1YiI6IiJ9.88vJc3I2HhuqEDixHXVhc9R30tA6U_HQHZTC29y6CGM'
434478
valid_signature = "qwertyuiopasdfghjklzxcvbnm123456"

vonage/CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 4.4.0
2+
Vonage Voice Package:
3+
- Add new `headers` and `standard_headers` options to the `Sip` data model
4+
- Add new `standardHeaders` option to the `SipEndpoint` NCCO model
5+
- Add check for invalid hostnames when downloading a recording with `Voice.download_recording`
6+
- Allow the `CreateCallRequest` model to accept a SIP URI as well as a phone number in the `from_` field
7+
18
# 4.3.0
29
- Make all models originally accessed by `vonage_voice.models.***` available at the top level of the package, i.e. `vonage_voice.***`
310
- Make all models originally accessed by `vonage_video.models.***` available at the top level of the package, i.e. `vonage_video.***`

vonage/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dependencies = [
2121
"vonage-verify>=2.1.0",
2222
"vonage-verify-legacy>=1.0.1",
2323
"vonage-video>=1.2.0",
24-
"vonage-voice>=1.2.0",
24+
"vonage-voice>=1.3.0",
2525
]
2626
classifiers = [
2727
"Programming Language :: Python",

0 commit comments

Comments
 (0)