vobiz-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. vobiz/__init__.py +4 -0
  2. vobiz/base.py +237 -0
  3. vobiz/exceptions.py +34 -0
  4. vobiz/resources/__init__.py +12 -0
  5. vobiz/resources/accounts.py +59 -0
  6. vobiz/resources/applications.py +138 -0
  7. vobiz/resources/calls_vobiz.py +206 -0
  8. vobiz/resources/cdrs.py +46 -0
  9. vobiz/resources/credentials.py +104 -0
  10. vobiz/resources/endpoints.py +101 -0
  11. vobiz/resources/ip_access_control_lists.py +100 -0
  12. vobiz/resources/numbers.py +134 -0
  13. vobiz/resources/origination_uris.py +109 -0
  14. vobiz/resources/recordings.py +91 -0
  15. vobiz/resources/sip_trunks.py +99 -0
  16. vobiz/resources/subaccounts.py +101 -0
  17. vobiz/rest/__init__.py +2 -0
  18. vobiz/rest/client.py +277 -0
  19. vobiz/utils/__init__.py +72 -0
  20. vobiz/utils/interactive.py +50 -0
  21. vobiz/utils/jwt.py +97 -0
  22. vobiz/utils/location.py +6 -0
  23. vobiz/utils/signature_v3.py +111 -0
  24. vobiz/utils/template.py +50 -0
  25. vobiz/utils/validators.py +280 -0
  26. vobiz/version.py +2 -0
  27. vobiz/xml/ConferenceElement.py +485 -0
  28. vobiz/xml/DTMFElement.py +41 -0
  29. vobiz/xml/DialElement.py +371 -0
  30. vobiz/xml/MultiPartyCallElement.py +711 -0
  31. vobiz/xml/ResponseElement.py +414 -0
  32. vobiz/xml/VobizXMLElement.py +48 -0
  33. vobiz/xml/__init__.py +31 -0
  34. vobiz/xml/breakElement.py +62 -0
  35. vobiz/xml/contElement.py +190 -0
  36. vobiz/xml/emphasisElement.py +174 -0
  37. vobiz/xml/getDigitsElement.py +294 -0
  38. vobiz/xml/getInputElement.py +369 -0
  39. vobiz/xml/hangupElement.py +57 -0
  40. vobiz/xml/langElement.py +197 -0
  41. vobiz/xml/messageElement.py +115 -0
  42. vobiz/xml/numberElement.py +77 -0
  43. vobiz/xml/pElement.py +164 -0
  44. vobiz/xml/phonemeElement.py +62 -0
  45. vobiz/xml/playElement.py +41 -0
  46. vobiz/xml/preAnswerElement.py +148 -0
  47. vobiz/xml/prosodyElement.py +227 -0
  48. vobiz/xml/recordElement.py +337 -0
  49. vobiz/xml/redirectElement.py +42 -0
  50. vobiz/xml/sElement.py +154 -0
  51. vobiz/xml/sayAsElement.py +61 -0
  52. vobiz/xml/speakElement.py +249 -0
  53. vobiz/xml/streamElement.py +123 -0
  54. vobiz/xml/subElement.py +43 -0
  55. vobiz/xml/userElement.py +79 -0
  56. vobiz/xml/wElement.py +144 -0
  57. vobiz/xml/waitElement.py +93 -0
  58. vobiz/xml/xmlUtils.py +6 -0
  59. vobiz_python-0.1.0.dist-info/METADATA +640 -0
  60. vobiz_python-0.1.0.dist-info/RECORD +63 -0
  61. vobiz_python-0.1.0.dist-info/WHEEL +5 -0
  62. vobiz_python-0.1.0.dist-info/licenses/LICENSE.txt +19 -0
  63. vobiz_python-0.1.0.dist-info/top_level.txt +1 -0
vobiz/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .rest.client import Client as RestClient
2
+ from . import xml as vobizxml
3
+
4
+ __all__ = ["RestClient", "vobizxml"]
vobiz/base.py ADDED
@@ -0,0 +1,237 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Base classes, used to deal with every Vobiz resource.
4
+ """
5
+
6
+ import pprint
7
+
8
+ from vobiz.exceptions import InvalidRequestError
9
+
10
+
11
+ class Meta:
12
+ def __init__(self):
13
+ self.limit = None
14
+ self.next = None
15
+ self.offset = None
16
+ self.previous = None
17
+ self.total_count = None
18
+
19
+
20
+ class VobizGenericResponse(object):
21
+ """A generic response to cover it all!
22
+
23
+ This provides a generic blanket response based on what is received from
24
+ Vobiz servers.
25
+
26
+ This will be used only during POST and DELETE requests.
27
+ """
28
+
29
+ def __init__(self, params_dict, id_string=None):
30
+ for i in params_dict:
31
+ self.__dict__[i] = params_dict[i]
32
+
33
+ if id_string and id_string in params_dict:
34
+ self.__dict__['id'] = params_dict[id_string]
35
+
36
+
37
+ class ResponseObject(object):
38
+ def __init__(self, dct):
39
+ self.__dict__.update(dct)
40
+
41
+ def __contains__(self, item):
42
+ return item in self.__dict__
43
+
44
+ def __getitem__(self, item):
45
+ try:
46
+ return self.__dict__.__getitem__(item)
47
+ except KeyError:
48
+ return self.objects.__getitem__(item)
49
+
50
+ def __setitem__(self, key, value):
51
+ self.__dict__.__setitem__(key, value)
52
+
53
+ def __delitem__(self, key):
54
+ del self.__dict__[key]
55
+
56
+ def __str__(self):
57
+ return pprint.pformat(self.__dict__)
58
+
59
+ def __repr__(self):
60
+ return pprint.pformat(self.__dict__)
61
+
62
+
63
+ class ListResponseObject(ResponseObject):
64
+ def __init__(self, client, dct):
65
+ super(ListResponseObject, self).__init__(dct)
66
+
67
+ def __iter__(self):
68
+ return self.objects.__iter__()
69
+
70
+ def __len__(self):
71
+ return len(self.objects)
72
+
73
+ def __str__(self):
74
+ return pprint.pformat(self.objects)
75
+
76
+ def __repr__(self):
77
+ return str([object for object in self.objects])
78
+
79
+
80
+ class VobizResource(ResponseObject):
81
+ """The Vobiz resource object
82
+
83
+ This provides an interface to deal with all Vobiz resources and
84
+ sub-resources.
85
+ """
86
+
87
+ _identifier_string = None
88
+
89
+ @property
90
+ def id(self):
91
+ value = self.__dict__.get(self._identifier_string, None)
92
+ if not value:
93
+ raise ValueError('{} must be set'.format(self._identifier_string))
94
+ return value
95
+
96
+ def __init__(self, client, data):
97
+ """Sets up the resource URI along with a hack for Account resource"""
98
+
99
+ super(VobizResource, self).__init__(data)
100
+ self._name = self._name or self.__class__.__name__
101
+ self.client = client
102
+
103
+ def __str__(self):
104
+ return pprint.pformat(self.__dict__)
105
+
106
+ def __repr__(self):
107
+ return self.__str__()
108
+
109
+ def update(self, params, path, **kwargs):
110
+ self.client.request('POST', params, path, **kwargs)
111
+
112
+ def _update(self, params):
113
+ if not self.id:
114
+ raise InvalidRequestError(
115
+ 'Cannot update a {resource_type} resource without an '
116
+ 'identifier'.format(resource_type=self._name))
117
+
118
+ response_json = self.client.send_request(
119
+ self.__resource_uri, method='POST', data=params)
120
+
121
+ for key in params:
122
+ self.__dict__[key] = params[key]
123
+
124
+ for key in response_json:
125
+ self.__dict__[key] = response_json[key]
126
+
127
+ return self
128
+
129
+ def _execute_action(self,
130
+ action=None,
131
+ method='GET',
132
+ params=None,
133
+ parse=False):
134
+ if not action:
135
+ response = self.client.send_request(
136
+ self.__resource_uri, method=method, data=params)
137
+ else:
138
+ response = self.client.send_request(
139
+ self.__resource_uri + action + '/', method=method, data=params)
140
+
141
+ if not parse:
142
+ return response
143
+
144
+ self.__resource_json = response
145
+ self.__parse_json()
146
+
147
+ try:
148
+ self.id = response[self._identifier_string]
149
+ except AttributeError:
150
+ pass
151
+
152
+ if method == 'POST':
153
+ self.__resource_json = params
154
+ self.__parse_json()
155
+ return self
156
+
157
+ def delete(self):
158
+ if not self.id:
159
+ raise InvalidRequestError(
160
+ 'Cannot delete a {resource_type} resource without an '
161
+ 'identifier'.format(resource_type=self._name))
162
+
163
+ return VobizGenericResponse(
164
+ self.client.send_request(self.__resource_uri, method='DELETE'))
165
+
166
+ def get(self):
167
+ if not self.id:
168
+ raise InvalidRequestError(
169
+ 'Cannot get a {resource_type} resource without an '
170
+ 'identifier'.format(resource_type=self._name))
171
+
172
+ self.__resource_json = self.client.send_request(self.__resource_uri)
173
+ self.__parse_json()
174
+ return self
175
+
176
+ def create(self, params):
177
+ if self.id:
178
+ raise InvalidRequestError(
179
+ 'Cannot create a {resource_type} resource because another'
180
+ ' {resource_type} resource exists with the same '
181
+ 'identifier: {identifier}.'.format(
182
+ resource_type=self._name, identifier=self.id))
183
+
184
+ id_string = None
185
+ if self._identifier_string:
186
+ id_string = self._identifier_string
187
+
188
+ return VobizGenericResponse(
189
+ self.client.send_request(
190
+ self.__resource_uri, data=params, method='POST'), id_string)
191
+
192
+
193
+ class SecondaryVobizResource(VobizResource):
194
+ """
195
+ SecondaryVobizResource resource object.
196
+ Provides an interface to deal with resources where the identifier
197
+ has a mid-level parent.
198
+ """
199
+ _secondary_identifier_string = None
200
+
201
+ @property
202
+ def secondary_id(self):
203
+ value = self.__dict__.get(self._secondary_identifier_string, None)
204
+ if not value:
205
+ raise ValueError('{} must be set'.format(self._secondary_identifier_string))
206
+ return value
207
+
208
+ def __init__(self, client, data):
209
+ super(VobizResource, self).__init__(client, data)
210
+ self._name = self._name or self.__class__.__name__
211
+ self.client = client
212
+
213
+
214
+ class VobizResourceInterface(object):
215
+ _iterable = True
216
+
217
+ def __init__(self, client, **kwargs):
218
+ self.client = client
219
+
220
+ def __iter__(self):
221
+ if not getattr(self, 'list') or not self.__class__._iterable:
222
+ raise NotImplementedError(
223
+ 'list is not supported for this resource')
224
+
225
+ def gen():
226
+ limit = 20
227
+ offset = 0
228
+ while True:
229
+ response = self.list(limit=limit, offset=offset)
230
+ if not response.objects:
231
+ return
232
+
233
+ for item in response:
234
+ yield item
235
+ offset += limit
236
+
237
+ return gen()
vobiz/exceptions.py ADDED
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+ class VobizRestError(Exception):
3
+ pass
4
+
5
+
6
+ class AuthenticationError(VobizRestError):
7
+ pass
8
+
9
+
10
+ class InvalidRequestError(VobizRestError):
11
+ pass
12
+
13
+
14
+ class VobizServerError(VobizRestError):
15
+ pass
16
+
17
+
18
+ class VobizXMLError(VobizRestError):
19
+ pass
20
+
21
+
22
+ class ResourceNotFoundError(VobizRestError):
23
+ pass
24
+
25
+
26
+ class ValidationError(VobizRestError):
27
+ pass
28
+
29
+
30
+ class ForbiddenError(VobizRestError):
31
+ pass
32
+
33
+ class GeoPermissionError(VobizRestError):
34
+ pass
@@ -0,0 +1,12 @@
1
+ from .accounts import Accounts
2
+ from .subaccounts import Subaccounts
3
+ from .calls_vobiz import Calls
4
+ from .applications import Applications
5
+ from .recordings import Recordings
6
+ from .cdrs import CDRs
7
+ from .numbers import PhoneNumbers
8
+ from .endpoints import Endpoints
9
+ from .sip_trunks import SipTrunks
10
+ from .credentials import Credentials
11
+ from .ip_access_control_lists import IpAccessControlLists
12
+ from .origination_uris import OriginationUris
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Vobiz Accounts resource.
4
+
5
+ Implements only the endpoints defined in the Vobiz accounts documentation.
6
+ """
7
+
8
+ VOBIZ_API_V1 = "https://api.vobiz.ai/api/v1"
9
+
10
+
11
+ class Accounts:
12
+ def __init__(self, client):
13
+ self.client = client
14
+
15
+ def get(self):
16
+ """
17
+ GET https://api.vobiz.ai/api/v1/auth/me
18
+ """
19
+ url = f"{VOBIZ_API_V1}/auth/me"
20
+ resp = self.client.session.get(
21
+ url, timeout=self.client.timeout, proxies=self.client.proxies
22
+ )
23
+ return self.client.process_response("GET", resp)
24
+
25
+ def get_transactions(self, account_id, limit=None, offset=None):
26
+ """
27
+ GET https://api.vobiz.ai/api/v1/account/{account_id}/transactions
28
+ """
29
+ url = f"{VOBIZ_API_V1}/account/{account_id}/transactions"
30
+ params = {}
31
+ if limit is not None:
32
+ params["limit"] = limit
33
+ if offset is not None:
34
+ params["offset"] = offset
35
+ resp = self.client.session.get(
36
+ url, params=params, timeout=self.client.timeout, proxies=self.client.proxies
37
+ )
38
+ return self.client.process_response("GET", resp)
39
+
40
+ def get_balance(self, account_id, currency):
41
+ """
42
+ GET https://api.vobiz.ai/api/v1/account/{account_id}/balance/{currency}
43
+ """
44
+ url = f"{VOBIZ_API_V1}/account/{account_id}/balance/{currency}"
45
+ resp = self.client.session.get(
46
+ url, timeout=self.client.timeout, proxies=self.client.proxies
47
+ )
48
+ return self.client.process_response("GET", resp)
49
+
50
+ def get_concurrency(self, account_id):
51
+ """
52
+ GET https://api.vobiz.ai/api/v1/account/{account_id}/concurrency
53
+ """
54
+ url = f"{VOBIZ_API_V1}/account/{account_id}/concurrency"
55
+ resp = self.client.session.get(
56
+ url, timeout=self.client.timeout, proxies=self.client.proxies
57
+ )
58
+ return self.client.process_response("GET", resp)
59
+
@@ -0,0 +1,138 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ VOBIZ_API_V1 = "https://api.vobiz.ai/api/v1"
4
+
5
+
6
+ class Applications:
7
+ """
8
+ Vobiz Applications resource.
9
+
10
+ All endpoints are scoped to the authenticated account.
11
+ """
12
+
13
+ def __init__(self, client):
14
+ self.client = client
15
+
16
+ @property
17
+ def _account_id(self) -> str:
18
+ # For Vobiz, we treat the RestClient auth_id as the account_id
19
+ return self.client.auth_id
20
+
21
+ def create(
22
+ self,
23
+ name: str,
24
+ answer_url: str,
25
+ answer_method: str = "POST",
26
+ hangup_url: Optional[str] = None,
27
+ hangup_method: str = "POST",
28
+ fallback_answer_url: Optional[str] = None,
29
+ fallback_method: str = "POST",
30
+ application_type: Optional[str] = None,
31
+ **extra: Any,
32
+ ):
33
+ """
34
+ POST /api/v1/Account/{account_id}/Application/
35
+ """
36
+ url = f"{VOBIZ_API_V1}/Account/{self._account_id}/Application/"
37
+ body: Dict[str, Any] = {
38
+ "name": name,
39
+ "answer_url": answer_url,
40
+ "answer_method": answer_method,
41
+ "hangup_method": hangup_method,
42
+ }
43
+ if hangup_url is not None:
44
+ body["hangup_url"] = hangup_url
45
+ if fallback_answer_url is not None:
46
+ body["fallback_answer_url"] = fallback_answer_url
47
+ if fallback_method is not None:
48
+ body["fallback_method"] = fallback_method
49
+ if application_type is not None:
50
+ body["application_type"] = application_type
51
+ # allow forward-compatible extra keys
52
+ body.update(extra)
53
+
54
+ resp = self.client.session.post(
55
+ url, json=body, timeout=self.client.timeout, proxies=self.client.proxies
56
+ )
57
+ return self.client.process_response("POST", resp)
58
+
59
+ def list(
60
+ self,
61
+ page: Optional[int] = None,
62
+ size: Optional[int] = None,
63
+ application_type: Optional[str] = None,
64
+ ):
65
+ """
66
+ GET /api/v1/Account/{account_id}/Application/
67
+ """
68
+ url = f"{VOBIZ_API_V1}/Account/{self._account_id}/Application/"
69
+ params: Dict[str, Any] = {}
70
+ if page is not None:
71
+ params["page"] = page
72
+ if size is not None:
73
+ params["size"] = size
74
+ if application_type is not None:
75
+ params["application_type"] = application_type
76
+
77
+ resp = self.client.session.get(
78
+ url, params=params, timeout=self.client.timeout, proxies=self.client.proxies
79
+ )
80
+ return self.client.process_response("GET", resp)
81
+
82
+ def get(self, application_id: str):
83
+ """
84
+ GET /api/v1/Account/{account_id}/Application/{application_id}
85
+ """
86
+ url = f"{VOBIZ_API_V1}/Account/{self._account_id}/Application/{application_id}"
87
+ resp = self.client.session.get(
88
+ url, timeout=self.client.timeout, proxies=self.client.proxies
89
+ )
90
+ return self.client.process_response("GET", resp)
91
+
92
+ def update(self, application_id: str, **params: Any):
93
+ """
94
+ POST /api/v1/Account/{account_id}/Application/{application_id}/
95
+ """
96
+ url = f"{VOBIZ_API_V1}/Account/{self._account_id}/Application/{application_id}/"
97
+ body: Dict[str, Any] = dict(params)
98
+ resp = self.client.session.post(
99
+ url, json=body, timeout=self.client.timeout, proxies=self.client.proxies
100
+ )
101
+ return self.client.process_response("POST", resp)
102
+
103
+ def delete(self, application_id: str):
104
+ """
105
+ DELETE /api/v1/Account/{account_id}/Application/{application_id}
106
+ """
107
+ url = f"{VOBIZ_API_V1}/Account/{self._account_id}/Application/{application_id}"
108
+ resp = self.client.session.delete(
109
+ url, timeout=self.client.timeout, proxies=self.client.proxies
110
+ )
111
+ return self.client.process_response("DELETE", resp)
112
+
113
+ def attach_number(self, number: str, application_id: str):
114
+ """
115
+ POST /api/v1/account/{account_id}/numbers/{number}/application
116
+
117
+ Attach a phone number to a voice application.
118
+ number: E.164 format, e.g. "+911234567890"
119
+ """
120
+ url = f"{VOBIZ_API_V1}/account/{self._account_id}/numbers/{number}/application"
121
+ body = {"application_id": application_id}
122
+ resp = self.client.session.post(
123
+ url, json=body, timeout=self.client.timeout, proxies=self.client.proxies
124
+ )
125
+ return self.client.process_response("POST", resp)
126
+
127
+ def detach_number(self, number: str):
128
+ """
129
+ DELETE /api/v1/account/{account_id}/numbers/{number}/application
130
+
131
+ Detach a phone number from its voice application.
132
+ number: E.164 format, e.g. "+911234567890"
133
+ """
134
+ url = f"{VOBIZ_API_V1}/account/{self._account_id}/numbers/{number}/application"
135
+ resp = self.client.session.delete(
136
+ url, timeout=self.client.timeout, proxies=self.client.proxies
137
+ )
138
+ return self.client.process_response("DELETE", resp)
@@ -0,0 +1,206 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+
4
+ class Calls:
5
+ """
6
+ Vobiz Calls resource.
7
+
8
+ Uses the `/api/v1/account/{auth_id}/call/` endpoints for call control and
9
+ live/queued status listing, plus DTMF.
10
+ """
11
+
12
+ def __init__(self, client):
13
+ self.client = client
14
+
15
+ @property
16
+ def _account_id(self) -> str:
17
+ return self.client.auth_id
18
+
19
+ def create(self, from_: str, to_: str, answer_url: str, **params: Any):
20
+ """
21
+ POST /api/v1/Account/{auth_id}/Call/
22
+ """
23
+ body: Dict[str, Any] = {
24
+ "from": from_,
25
+ "to": to_,
26
+ "answer_url": answer_url,
27
+ }
28
+ body.update(params)
29
+ return self.client.request("POST", ("Call",), data=body)
30
+
31
+ def transfer(self, call_uuid: str, **params: Any):
32
+ """
33
+ POST /api/v1/Account/{auth_id}/Call/{call_uuid}/
34
+ """
35
+ return self.client.request("POST", ("Call", call_uuid), data=dict(params))
36
+
37
+ def hangup(self, call_uuid: str):
38
+ """
39
+ DELETE /api/v1/Account/{auth_id}/Call/{call_uuid}/
40
+ """
41
+ return self.client.request("DELETE", ("Call", call_uuid))
42
+
43
+ def list(self, status: Optional[str] = None, **filters: Any):
44
+ """
45
+ GET /api/v1/Account/{auth_id}/Call/
46
+ """
47
+ params: Dict[str, Any] = dict(filters)
48
+ if status is not None:
49
+ params["status"] = status
50
+ return self.client.request(
51
+ "GET",
52
+ ("Call",),
53
+ data=params,
54
+ )
55
+
56
+ def get(self, call_uuid: str, status: Optional[str] = None):
57
+ """
58
+ GET /api/v1/Account/{auth_id}/Call/{call_uuid}/
59
+ """
60
+ params: Dict[str, Any] = {}
61
+ if status is not None:
62
+ params["status"] = status
63
+ return self.client.request("GET", ("Call", call_uuid), data=params)
64
+
65
+ def list_live(self):
66
+ return self.list(status="live")
67
+
68
+ def list_queued(self):
69
+ return self.list(status="queued")
70
+
71
+ def get_live(self, call_uuid: str):
72
+ return self.get(call_uuid, status="live")
73
+
74
+ def get_queued(self, call_uuid: str):
75
+ return self.get(call_uuid, status="queued")
76
+
77
+ def send_digits(self, call_uuid: str, digits: str, leg: str):
78
+ """
79
+ POST /api/v1/Account/{auth_id}/Call/{call_uuid}/DTMF/
80
+ """
81
+ body: Dict[str, Any] = {"digits": digits, "leg": leg}
82
+ return self.client.request("POST", ("Call", call_uuid, "DTMF"), data=body)
83
+
84
+ # ── Recording ──────────────────────────────────────────────────────────────
85
+
86
+ def start_recording(
87
+ self,
88
+ call_uuid: str,
89
+ time_limit: Optional[int] = None,
90
+ file_format: Optional[str] = None,
91
+ callback_url: Optional[str] = None,
92
+ callback_method: Optional[str] = None,
93
+ **params: Any,
94
+ ):
95
+ """
96
+ POST /api/v1/Account/{auth_id}/Call/{call_uuid}/Record/
97
+
98
+ Start recording an active call.
99
+ """
100
+ body: Dict[str, Any] = {}
101
+ if time_limit is not None:
102
+ body["time_limit"] = time_limit
103
+ if file_format is not None:
104
+ body["file_format"] = file_format
105
+ if callback_url is not None:
106
+ body["callback_url"] = callback_url
107
+ if callback_method is not None:
108
+ body["callback_method"] = callback_method
109
+ body.update(params)
110
+ return self.client.request("POST", ("Call", call_uuid, "Record"), data=body)
111
+
112
+ def stop_recording(self, call_uuid: str, url: Optional[str] = None):
113
+ """
114
+ DELETE /api/v1/Account/{auth_id}/Call/{call_uuid}/Record/
115
+
116
+ Stop recording on an active call.
117
+ If url is provided, stops only that specific recording.
118
+ If omitted, stops all recordings on the call.
119
+ """
120
+ body: Dict[str, Any] = {}
121
+ if url is not None:
122
+ body["URL"] = url
123
+ return self.client.request("DELETE", ("Call", call_uuid, "Record"), data=body)
124
+
125
+ # ── Play Audio ─────────────────────────────────────────────────────────────
126
+
127
+ def play_audio(
128
+ self,
129
+ call_uuid: str,
130
+ urls: list,
131
+ length: Optional[int] = None,
132
+ legs: Optional[str] = None,
133
+ loop: Optional[bool] = None,
134
+ mix: Optional[bool] = None,
135
+ **params: Any,
136
+ ):
137
+ """
138
+ POST /api/v1/Account/{auth_id}/Call/{call_uuid}/Play/
139
+
140
+ Play one or more audio files during an active call.
141
+ urls: list of HTTPS URLs to audio files (mp3/wav).
142
+ legs: "aleg", "bleg", or "both" (default "aleg").
143
+ """
144
+ body: Dict[str, Any] = {"urls": urls}
145
+ if length is not None:
146
+ body["length"] = length
147
+ if legs is not None:
148
+ body["legs"] = legs
149
+ if loop is not None:
150
+ body["loop"] = loop
151
+ if mix is not None:
152
+ body["mix"] = mix
153
+ body.update(params)
154
+ return self.client.request("POST", ("Call", call_uuid, "Play"), data=body)
155
+
156
+ def stop_audio(self, call_uuid: str):
157
+ """
158
+ DELETE /api/v1/Account/{auth_id}/Call/{call_uuid}/Play/
159
+
160
+ Stop audio playback on an active call.
161
+ """
162
+ return self.client.request("DELETE", ("Call", call_uuid, "Play"), data={})
163
+
164
+ # ── Speak Text (TTS) ───────────────────────────────────────────────────────
165
+
166
+ def speak_text(
167
+ self,
168
+ call_uuid: str,
169
+ text: str,
170
+ voice: Optional[str] = None,
171
+ language: Optional[str] = None,
172
+ legs: Optional[str] = None,
173
+ loop: Optional[bool] = None,
174
+ mix: Optional[bool] = None,
175
+ **params: Any,
176
+ ):
177
+ """
178
+ POST /api/v1/Account/{auth_id}/Call/{call_uuid}/Speak/
179
+
180
+ Convert text to speech and play it during an active call.
181
+ voice: "WOMAN" or "MAN" (default "WOMAN").
182
+ language: BCP-47 language code, e.g. "en-US" (default).
183
+ legs: "aleg", "bleg", or "both".
184
+ """
185
+ body: Dict[str, Any] = {"text": text}
186
+ if voice is not None:
187
+ body["voice"] = voice
188
+ if language is not None:
189
+ body["language"] = language
190
+ if legs is not None:
191
+ body["legs"] = legs
192
+ if loop is not None:
193
+ body["loop"] = loop
194
+ if mix is not None:
195
+ body["mix"] = mix
196
+ body.update(params)
197
+ return self.client.request("POST", ("Call", call_uuid, "Speak"), data=body)
198
+
199
+ def stop_speak(self, call_uuid: str):
200
+ """
201
+ DELETE /api/v1/Account/{auth_id}/Call/{call_uuid}/Speak/
202
+
203
+ Stop text-to-speech currently playing on an active call.
204
+ """
205
+ return self.client.request("DELETE", ("Call", call_uuid, "Speak"), data={})
206
+