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.
- vobiz/__init__.py +4 -0
- vobiz/base.py +237 -0
- vobiz/exceptions.py +34 -0
- vobiz/resources/__init__.py +12 -0
- vobiz/resources/accounts.py +59 -0
- vobiz/resources/applications.py +138 -0
- vobiz/resources/calls_vobiz.py +206 -0
- vobiz/resources/cdrs.py +46 -0
- vobiz/resources/credentials.py +104 -0
- vobiz/resources/endpoints.py +101 -0
- vobiz/resources/ip_access_control_lists.py +100 -0
- vobiz/resources/numbers.py +134 -0
- vobiz/resources/origination_uris.py +109 -0
- vobiz/resources/recordings.py +91 -0
- vobiz/resources/sip_trunks.py +99 -0
- vobiz/resources/subaccounts.py +101 -0
- vobiz/rest/__init__.py +2 -0
- vobiz/rest/client.py +277 -0
- vobiz/utils/__init__.py +72 -0
- vobiz/utils/interactive.py +50 -0
- vobiz/utils/jwt.py +97 -0
- vobiz/utils/location.py +6 -0
- vobiz/utils/signature_v3.py +111 -0
- vobiz/utils/template.py +50 -0
- vobiz/utils/validators.py +280 -0
- vobiz/version.py +2 -0
- vobiz/xml/ConferenceElement.py +485 -0
- vobiz/xml/DTMFElement.py +41 -0
- vobiz/xml/DialElement.py +371 -0
- vobiz/xml/MultiPartyCallElement.py +711 -0
- vobiz/xml/ResponseElement.py +414 -0
- vobiz/xml/VobizXMLElement.py +48 -0
- vobiz/xml/__init__.py +31 -0
- vobiz/xml/breakElement.py +62 -0
- vobiz/xml/contElement.py +190 -0
- vobiz/xml/emphasisElement.py +174 -0
- vobiz/xml/getDigitsElement.py +294 -0
- vobiz/xml/getInputElement.py +369 -0
- vobiz/xml/hangupElement.py +57 -0
- vobiz/xml/langElement.py +197 -0
- vobiz/xml/messageElement.py +115 -0
- vobiz/xml/numberElement.py +77 -0
- vobiz/xml/pElement.py +164 -0
- vobiz/xml/phonemeElement.py +62 -0
- vobiz/xml/playElement.py +41 -0
- vobiz/xml/preAnswerElement.py +148 -0
- vobiz/xml/prosodyElement.py +227 -0
- vobiz/xml/recordElement.py +337 -0
- vobiz/xml/redirectElement.py +42 -0
- vobiz/xml/sElement.py +154 -0
- vobiz/xml/sayAsElement.py +61 -0
- vobiz/xml/speakElement.py +249 -0
- vobiz/xml/streamElement.py +123 -0
- vobiz/xml/subElement.py +43 -0
- vobiz/xml/userElement.py +79 -0
- vobiz/xml/wElement.py +144 -0
- vobiz/xml/waitElement.py +93 -0
- vobiz/xml/xmlUtils.py +6 -0
- vobiz_python-0.1.0.dist-info/METADATA +640 -0
- vobiz_python-0.1.0.dist-info/RECORD +63 -0
- vobiz_python-0.1.0.dist-info/WHEEL +5 -0
- vobiz_python-0.1.0.dist-info/licenses/LICENSE.txt +19 -0
- vobiz_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
VOBIZ_API_V1 = "https://api.vobiz.ai/api/v1"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SipTrunks:
|
|
7
|
+
"""
|
|
8
|
+
Vobiz SIP Trunks resource.
|
|
9
|
+
|
|
10
|
+
Uses the `/api/v1/account/{account_id}/trunks/...` paths described in
|
|
11
|
+
the Vobiz SIP trunks documentation.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, client):
|
|
15
|
+
self.client = client
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def _account_id(self) -> str:
|
|
19
|
+
# For Vobiz, we treat the RestClient auth_id as the account_id
|
|
20
|
+
return self.client.auth_id
|
|
21
|
+
|
|
22
|
+
def create(
|
|
23
|
+
self,
|
|
24
|
+
name: str,
|
|
25
|
+
inbound_uri: Optional[str] = None,
|
|
26
|
+
outbound_uri: Optional[str] = None,
|
|
27
|
+
**extra: Any,
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
POST /api/v1/account/{account_id}/trunks
|
|
31
|
+
"""
|
|
32
|
+
url = f"{VOBIZ_API_V1}/account/{self._account_id}/trunks"
|
|
33
|
+
body: Dict[str, Any] = {"name": name}
|
|
34
|
+
if inbound_uri is not None:
|
|
35
|
+
body["inbound_uri"] = inbound_uri
|
|
36
|
+
if outbound_uri is not None:
|
|
37
|
+
body["outbound_uri"] = outbound_uri
|
|
38
|
+
body.update(extra)
|
|
39
|
+
|
|
40
|
+
resp = self.client.session.post(
|
|
41
|
+
url, json=body, timeout=self.client.timeout, proxies=self.client.proxies
|
|
42
|
+
)
|
|
43
|
+
return self.client.process_response("POST", resp)
|
|
44
|
+
|
|
45
|
+
def list(
|
|
46
|
+
self,
|
|
47
|
+
page: Optional[int] = None,
|
|
48
|
+
size: Optional[int] = None,
|
|
49
|
+
**filters: Any,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
GET /api/v1/account/{account_id}/trunks
|
|
53
|
+
"""
|
|
54
|
+
url = f"{VOBIZ_API_V1}/account/{self._account_id}/trunks"
|
|
55
|
+
params: Dict[str, Any] = {}
|
|
56
|
+
if page is not None:
|
|
57
|
+
params["page"] = page
|
|
58
|
+
if size is not None:
|
|
59
|
+
params["size"] = size
|
|
60
|
+
params.update(filters)
|
|
61
|
+
|
|
62
|
+
resp = self.client.session.get(
|
|
63
|
+
url, params=params, timeout=self.client.timeout, proxies=self.client.proxies
|
|
64
|
+
)
|
|
65
|
+
return self.client.process_response("GET", resp)
|
|
66
|
+
|
|
67
|
+
def get(self, trunk_id: str):
|
|
68
|
+
"""
|
|
69
|
+
GET /api/v1/account/{account_id}/trunks/{trunk_id}
|
|
70
|
+
"""
|
|
71
|
+
url = f"{VOBIZ_API_V1}/account/{self._account_id}/trunks/{trunk_id}"
|
|
72
|
+
resp = self.client.session.get(
|
|
73
|
+
url, timeout=self.client.timeout, proxies=self.client.proxies
|
|
74
|
+
)
|
|
75
|
+
return self.client.process_response("GET", resp)
|
|
76
|
+
|
|
77
|
+
def update(self, trunk_id: str, **params: Any):
|
|
78
|
+
"""
|
|
79
|
+
PUT /api/v1/account/{account_id}/trunks/{trunk_id}
|
|
80
|
+
"""
|
|
81
|
+
url = f"{VOBIZ_API_V1}/account/{self._account_id}/trunks/{trunk_id}"
|
|
82
|
+
body: Dict[str, Any] = dict(params)
|
|
83
|
+
resp = self.client.session.put(
|
|
84
|
+
url, json=body, timeout=self.client.timeout, proxies=self.client.proxies
|
|
85
|
+
)
|
|
86
|
+
return self.client.process_response("PUT", resp)
|
|
87
|
+
|
|
88
|
+
def delete(self, trunk_id: str):
|
|
89
|
+
"""
|
|
90
|
+
DELETE /api/v1/account/{account_id}/trunks/{trunk_id}
|
|
91
|
+
|
|
92
|
+
Permanently delete a SIP trunk and its associated resources.
|
|
93
|
+
"""
|
|
94
|
+
url = f"{VOBIZ_API_V1}/account/{self._account_id}/trunks/{trunk_id}"
|
|
95
|
+
resp = self.client.session.delete(
|
|
96
|
+
url, timeout=self.client.timeout, proxies=self.client.proxies
|
|
97
|
+
)
|
|
98
|
+
return self.client.process_response("DELETE", resp)
|
|
99
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
VOBIZ_API_V1 = "https://api.vobiz.ai/api/v1"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Subaccounts:
|
|
7
|
+
def __init__(self, client):
|
|
8
|
+
self.client = client
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def _account_id(self) -> str:
|
|
12
|
+
# For Vobiz, we treat the RestClient auth_id as the account_id
|
|
13
|
+
return self.client.auth_id
|
|
14
|
+
|
|
15
|
+
def create(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
email: str,
|
|
19
|
+
rate_limit: int,
|
|
20
|
+
permissions: Any,
|
|
21
|
+
password: str,
|
|
22
|
+
phone: Optional[str] = None,
|
|
23
|
+
description: Optional[str] = None,
|
|
24
|
+
enabled: bool = True,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
POST /api/v1/accounts/{account_id}/sub-accounts/
|
|
28
|
+
"""
|
|
29
|
+
url = f"{VOBIZ_API_V1}/accounts/{self._account_id}/sub-accounts/"
|
|
30
|
+
body: Dict[str, Any] = {
|
|
31
|
+
"name": name,
|
|
32
|
+
"email": email,
|
|
33
|
+
"rate_limit": rate_limit,
|
|
34
|
+
"permissions": permissions,
|
|
35
|
+
"password": password,
|
|
36
|
+
"enabled": enabled,
|
|
37
|
+
}
|
|
38
|
+
if phone is not None:
|
|
39
|
+
body["phone"] = phone
|
|
40
|
+
if description is not None:
|
|
41
|
+
body["description"] = description
|
|
42
|
+
|
|
43
|
+
resp = self.client.session.post(
|
|
44
|
+
url, json=body, timeout=self.client.timeout, proxies=self.client.proxies
|
|
45
|
+
)
|
|
46
|
+
return self.client.process_response("POST", resp)
|
|
47
|
+
|
|
48
|
+
def list(
|
|
49
|
+
self,
|
|
50
|
+
page: Optional[int] = None,
|
|
51
|
+
size: Optional[int] = None,
|
|
52
|
+
active_only: Optional[bool] = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
GET /api/v1/accounts/{account_id}/sub-accounts/
|
|
56
|
+
"""
|
|
57
|
+
url = f"{VOBIZ_API_V1}/accounts/{self._account_id}/sub-accounts/"
|
|
58
|
+
params: Dict[str, Any] = {}
|
|
59
|
+
if page is not None:
|
|
60
|
+
params["page"] = page
|
|
61
|
+
if size is not None:
|
|
62
|
+
params["size"] = size
|
|
63
|
+
if active_only is not None:
|
|
64
|
+
params["active_only"] = active_only
|
|
65
|
+
|
|
66
|
+
resp = self.client.session.get(
|
|
67
|
+
url, params=params, timeout=self.client.timeout, proxies=self.client.proxies
|
|
68
|
+
)
|
|
69
|
+
return self.client.process_response("GET", resp)
|
|
70
|
+
|
|
71
|
+
def get(self, sub_account_id: str):
|
|
72
|
+
"""
|
|
73
|
+
GET /api/v1/accounts/{account_id}/sub-accounts/{sub_account_id}
|
|
74
|
+
"""
|
|
75
|
+
url = f"{VOBIZ_API_V1}/accounts/{self._account_id}/sub-accounts/{sub_account_id}"
|
|
76
|
+
resp = self.client.session.get(
|
|
77
|
+
url, timeout=self.client.timeout, proxies=self.client.proxies
|
|
78
|
+
)
|
|
79
|
+
return self.client.process_response("GET", resp)
|
|
80
|
+
|
|
81
|
+
def update(self, sub_account_id: str, **params: Any):
|
|
82
|
+
"""
|
|
83
|
+
PUT /api/v1/accounts/{account_id}/sub-accounts/{sub_account_id}
|
|
84
|
+
"""
|
|
85
|
+
url = f"{VOBIZ_API_V1}/accounts/{self._account_id}/sub-accounts/{sub_account_id}"
|
|
86
|
+
body: Dict[str, Any] = dict(params)
|
|
87
|
+
resp = self.client.session.put(
|
|
88
|
+
url, json=body, timeout=self.client.timeout, proxies=self.client.proxies
|
|
89
|
+
)
|
|
90
|
+
return self.client.process_response("PUT", resp)
|
|
91
|
+
|
|
92
|
+
def delete(self, sub_account_id: str):
|
|
93
|
+
"""
|
|
94
|
+
DELETE /api/v1/accounts/{account_id}/sub-accounts/{sub_account_id}
|
|
95
|
+
"""
|
|
96
|
+
url = f"{VOBIZ_API_V1}/accounts/{self._account_id}/sub-accounts/{sub_account_id}"
|
|
97
|
+
resp = self.client.session.delete(
|
|
98
|
+
url, timeout=self.client.timeout, proxies=self.client.proxies
|
|
99
|
+
)
|
|
100
|
+
return self.client.process_response("DELETE", resp)
|
|
101
|
+
|
vobiz/rest/__init__.py
ADDED
vobiz/rest/client.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Core client, used for all API requests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
from vobiz.base import ResponseObject
|
|
14
|
+
from vobiz.exceptions import (
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
InvalidRequestError,
|
|
17
|
+
VobizRestError,
|
|
18
|
+
VobizServerError,
|
|
19
|
+
GeoPermissionError,
|
|
20
|
+
ResourceNotFoundError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
ForbiddenError,
|
|
23
|
+
)
|
|
24
|
+
from vobiz.resources import (
|
|
25
|
+
Calls,
|
|
26
|
+
Accounts,
|
|
27
|
+
Subaccounts,
|
|
28
|
+
Applications,
|
|
29
|
+
Recordings,
|
|
30
|
+
CDRs,
|
|
31
|
+
PhoneNumbers,
|
|
32
|
+
Endpoints,
|
|
33
|
+
SipTrunks,
|
|
34
|
+
Credentials,
|
|
35
|
+
IpAccessControlLists,
|
|
36
|
+
OriginationUris,
|
|
37
|
+
)
|
|
38
|
+
from vobiz.version import __version__
|
|
39
|
+
from requests import Request, Session
|
|
40
|
+
|
|
41
|
+
AuthenticationCredentials = namedtuple("AuthenticationCredentials", "auth_id auth_token")
|
|
42
|
+
|
|
43
|
+
VOBIZ_API = "https://api.vobiz.ai/api"
|
|
44
|
+
VOBIZ_API_BASE_URI = "/".join([VOBIZ_API, "v1/Account"])
|
|
45
|
+
|
|
46
|
+
GEO_PERMISSION_ENDPOINTS = ["/Call/"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_user_agent():
|
|
50
|
+
return "vobiz-python/%s (Python: %s)" % (__version__, platform.python_version())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fetch_credentials(auth_id, auth_token):
|
|
54
|
+
"""Fetch credentials from arguments"""
|
|
55
|
+
|
|
56
|
+
if not (auth_id and auth_token):
|
|
57
|
+
try:
|
|
58
|
+
auth_id = os.environ["VOBIZ_AUTH_ID"]
|
|
59
|
+
auth_token = os.environ["VOBIZ_AUTH_TOKEN"]
|
|
60
|
+
except KeyError:
|
|
61
|
+
raise AuthenticationError(
|
|
62
|
+
"The Vobiz Python SDK could not find your auth credentials."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not isinstance(auth_id, str) or not auth_id:
|
|
66
|
+
raise AuthenticationError("Invalid auth_id supplied: %s" % auth_id)
|
|
67
|
+
|
|
68
|
+
if not isinstance(auth_token, str) or not auth_token:
|
|
69
|
+
raise AuthenticationError("Invalid auth_token supplied.")
|
|
70
|
+
|
|
71
|
+
return AuthenticationCredentials(auth_id=auth_id, auth_token=auth_token)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Client(object):
|
|
75
|
+
def __init__(self, auth_id=None, auth_token=None, proxies=None, timeout=5):
|
|
76
|
+
"""
|
|
77
|
+
The Vobiz API client.
|
|
78
|
+
|
|
79
|
+
Deals with all the API requests to be made.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
self.base_uri = VOBIZ_API_BASE_URI
|
|
83
|
+
self.session = Session()
|
|
84
|
+
self.session.headers.update(
|
|
85
|
+
{
|
|
86
|
+
"User-Agent": get_user_agent(),
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
"Accept": "application/json",
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
creds = fetch_credentials(auth_id, auth_token)
|
|
92
|
+
self.auth_id = creds.auth_id
|
|
93
|
+
self.auth_token = creds.auth_token
|
|
94
|
+
self.session.headers.update(
|
|
95
|
+
{"X-Auth-ID": self.auth_id, "X-Auth-Token": self.auth_token}
|
|
96
|
+
)
|
|
97
|
+
self.multipart_session = Session()
|
|
98
|
+
self.multipart_session.headers.update(
|
|
99
|
+
{
|
|
100
|
+
"User-Agent": get_user_agent(),
|
|
101
|
+
"Cache-Control": "no-cache",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
self.multipart_session.headers.update(
|
|
105
|
+
{"X-Auth-ID": self.auth_id, "X-Auth-Token": self.auth_token}
|
|
106
|
+
)
|
|
107
|
+
self.proxies = proxies
|
|
108
|
+
self.timeout = timeout
|
|
109
|
+
self.calls = Calls(self)
|
|
110
|
+
self.accounts = Accounts(self)
|
|
111
|
+
self.subaccounts = Subaccounts(self)
|
|
112
|
+
self.applications = Applications(self)
|
|
113
|
+
self.recordings = Recordings(self)
|
|
114
|
+
self.cdrs = CDRs(self)
|
|
115
|
+
self.phone_numbers = PhoneNumbers(self)
|
|
116
|
+
self.endpoints = Endpoints(self)
|
|
117
|
+
self.sip_trunks = SipTrunks(self)
|
|
118
|
+
self.credentials = Credentials(self)
|
|
119
|
+
self.ip_access_control_lists = IpAccessControlLists(self)
|
|
120
|
+
self.origination_uris = OriginationUris(self)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def __enter__(self):
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
127
|
+
self.session.close()
|
|
128
|
+
self.multipart_session.close()
|
|
129
|
+
|
|
130
|
+
def process_response(self,
|
|
131
|
+
method,
|
|
132
|
+
response,
|
|
133
|
+
response_type=None,
|
|
134
|
+
objects_type=None):
|
|
135
|
+
"""Processes the API response based on the status codes and method used
|
|
136
|
+
to access the API
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
response_json = response.json(
|
|
140
|
+
object_hook=lambda x: ResponseObject(x) if isinstance(x, dict) else x)
|
|
141
|
+
if response_type:
|
|
142
|
+
r = response_type(self, response_json.__dict__)
|
|
143
|
+
response_json = r
|
|
144
|
+
|
|
145
|
+
if 'objects' in response_json and objects_type:
|
|
146
|
+
response_json.objects = [
|
|
147
|
+
objects_type(self, obj.__dict__)
|
|
148
|
+
for obj in response_json.objects
|
|
149
|
+
]
|
|
150
|
+
except ValueError:
|
|
151
|
+
response_json = None
|
|
152
|
+
|
|
153
|
+
if response.status_code == 400:
|
|
154
|
+
if response_json is not None and 'error' in response_json:
|
|
155
|
+
raise ValidationError(response_json.error)
|
|
156
|
+
raise ValidationError(
|
|
157
|
+
'A parameter is missing or is invalid while accessing resource'
|
|
158
|
+
'at: {url}'.format(url=response.url))
|
|
159
|
+
|
|
160
|
+
if response.status_code == 401:
|
|
161
|
+
if response_json and 'error' in response_json:
|
|
162
|
+
raise AuthenticationError(response_json.error)
|
|
163
|
+
raise AuthenticationError(
|
|
164
|
+
'Failed to authenticate while accessing resource at: '
|
|
165
|
+
'{url}'.format(url=response.url))
|
|
166
|
+
|
|
167
|
+
if response.status_code == 403:
|
|
168
|
+
error = ForbiddenError
|
|
169
|
+
if method == "POST" and any(response.url.endswith(endpoint) for endpoint in GEO_PERMISSION_ENDPOINTS):
|
|
170
|
+
error = GeoPermissionError
|
|
171
|
+
if response_json and 'error' in response_json:
|
|
172
|
+
raise error(response_json.error)
|
|
173
|
+
raise error(
|
|
174
|
+
'Request Failed : '
|
|
175
|
+
'{url}'.format(url=response.url))
|
|
176
|
+
|
|
177
|
+
if response.status_code == 404:
|
|
178
|
+
if response_json and 'error' in response_json:
|
|
179
|
+
raise ResourceNotFoundError(response_json.error)
|
|
180
|
+
raise ResourceNotFoundError(
|
|
181
|
+
'Resource not found at: {url}'.format(url=response.url))
|
|
182
|
+
|
|
183
|
+
if response.status_code == 405:
|
|
184
|
+
if response_json and 'error' in response_json:
|
|
185
|
+
raise InvalidRequestError(response_json.error)
|
|
186
|
+
raise InvalidRequestError(
|
|
187
|
+
'HTTP method "{method}" not allowed to access resource at: '
|
|
188
|
+
'{url}'.format(method=method, url=response.url))
|
|
189
|
+
|
|
190
|
+
if response.status_code == 409:
|
|
191
|
+
if response_json and 'error' in response_json:
|
|
192
|
+
raise InvalidRequestError(response_json.error)
|
|
193
|
+
raise InvalidRequestError(
|
|
194
|
+
'Conflict: '
|
|
195
|
+
'{url}'.format(url=response.url))
|
|
196
|
+
|
|
197
|
+
if response.status_code == 422:
|
|
198
|
+
if response_json and 'error' in response_json:
|
|
199
|
+
raise InvalidRequestError(response_json.error)
|
|
200
|
+
raise InvalidRequestError(
|
|
201
|
+
'Unprocessable Entity: '
|
|
202
|
+
'{url}'.format(url=response.url))
|
|
203
|
+
|
|
204
|
+
if response.status_code == 500:
|
|
205
|
+
if response_json and 'error' in response_json:
|
|
206
|
+
raise VobizServerError(response_json.error)
|
|
207
|
+
raise VobizServerError(
|
|
208
|
+
'A server error occurred while accessing resource at: '
|
|
209
|
+
'{url}'.format(url=response.url))
|
|
210
|
+
|
|
211
|
+
if method == 'DELETE':
|
|
212
|
+
if response.status_code not in [200, 202, 204]:
|
|
213
|
+
raise VobizRestError('Resource at {url} could not be '
|
|
214
|
+
'deleted'.format(url=response.url))
|
|
215
|
+
|
|
216
|
+
elif response.status_code not in [200, 201, 202, 204, 206, 207]:
|
|
217
|
+
raise VobizRestError(
|
|
218
|
+
'Received status code {status_code} for the HTTP method '
|
|
219
|
+
'"{method}"'.format(
|
|
220
|
+
status_code=response.status_code, method=method))
|
|
221
|
+
|
|
222
|
+
return response_json
|
|
223
|
+
|
|
224
|
+
def create_request(self, method, path=None, data=None, **kwargs):
|
|
225
|
+
path = path or []
|
|
226
|
+
url = '/'.join([self.base_uri, self.auth_id] + list([str(p) for p in path])) + '/'
|
|
227
|
+
req = Request(
|
|
228
|
+
method,
|
|
229
|
+
url,
|
|
230
|
+
**({'params': data} if method == 'GET' else {'json': data}),
|
|
231
|
+
)
|
|
232
|
+
return self.session.prepare_request(req)
|
|
233
|
+
|
|
234
|
+
def create_multipart_request(self,
|
|
235
|
+
method,
|
|
236
|
+
path=None,
|
|
237
|
+
data=None,
|
|
238
|
+
files=None):
|
|
239
|
+
path = path or []
|
|
240
|
+
|
|
241
|
+
data_args = {}
|
|
242
|
+
if method == 'GET':
|
|
243
|
+
data_args['params'] = data
|
|
244
|
+
else:
|
|
245
|
+
data_args['data'] = data
|
|
246
|
+
if files:
|
|
247
|
+
data_args['files'] = files
|
|
248
|
+
url = '/'.join([self.base_uri, self.auth_id] + list([str(p) for p in path])) + '/'
|
|
249
|
+
req = Request(method, url, **data_args)
|
|
250
|
+
return self.multipart_session.prepare_request(req)
|
|
251
|
+
|
|
252
|
+
def send_request(self, request, **kwargs):
|
|
253
|
+
if 'session' in kwargs:
|
|
254
|
+
session = kwargs['session']
|
|
255
|
+
del kwargs['session']
|
|
256
|
+
else:
|
|
257
|
+
session = self.session
|
|
258
|
+
return session.send(
|
|
259
|
+
request, proxies=self.proxies, timeout=self.timeout, **kwargs)
|
|
260
|
+
|
|
261
|
+
def request(self,
|
|
262
|
+
method,
|
|
263
|
+
path=None,
|
|
264
|
+
data=None,
|
|
265
|
+
response_type=None,
|
|
266
|
+
objects_type=None,
|
|
267
|
+
files=None,
|
|
268
|
+
**kwargs):
|
|
269
|
+
if files is not None:
|
|
270
|
+
req = self.create_multipart_request(method, path, data, files)
|
|
271
|
+
session = self.multipart_session
|
|
272
|
+
else:
|
|
273
|
+
req = self.create_request(method, path, data)
|
|
274
|
+
session = self.session
|
|
275
|
+
kwargs['session'] = session
|
|
276
|
+
res = self.send_request(req, **kwargs)
|
|
277
|
+
return self.process_response(method, res, response_type, objects_type)
|
vobiz/utils/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import inspect
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from hmac import new as hnew
|
|
7
|
+
from hashlib import sha256
|
|
8
|
+
from urllib.parse import urlparse, urlunparse
|
|
9
|
+
from base64 import encodebytes as base64_encode
|
|
10
|
+
from inspect import getfullargspec as getargspec
|
|
11
|
+
|
|
12
|
+
from .signature_v3 import validate_v3_signature
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_signature(uri, nonce, signature, auth_token=''):
|
|
16
|
+
"""
|
|
17
|
+
Validates requests made by Vobiz to your servers.
|
|
18
|
+
|
|
19
|
+
:param uri: Your server URL
|
|
20
|
+
:param nonce: X-Vobiz-Signature-V2-Nonce
|
|
21
|
+
:param signature: X-Vobiz-Signature-V2 header
|
|
22
|
+
:param auth_token: Vobiz Auth token
|
|
23
|
+
:return: True if the request matches signature, False otherwise
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
auth_token = bytes(auth_token.encode('utf-8'))
|
|
27
|
+
nonce = bytes(nonce.encode('utf-8'))
|
|
28
|
+
signature = bytes(signature.encode('utf-8'))
|
|
29
|
+
|
|
30
|
+
parsed_uri = urlparse(uri.encode('utf-8'))
|
|
31
|
+
base_url = urlunparse((parsed_uri.scheme.decode('utf-8'),
|
|
32
|
+
parsed_uri.netloc.decode('utf-8'),
|
|
33
|
+
parsed_uri.path.decode('utf-8'), '', '',
|
|
34
|
+
'')).encode('utf-8')
|
|
35
|
+
|
|
36
|
+
return base64_encode(hnew(auth_token, base_url + nonce, sha256)
|
|
37
|
+
.digest()).strip() == signature
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_valid_time_comparison(time):
|
|
41
|
+
if isinstance(time, datetime):
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_valid_subaccount(subaccount):
|
|
47
|
+
subaccount_string = str(subaccount)
|
|
48
|
+
if len(subaccount_string) == 20 and subaccount_string[:2] == 'SA':
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_valid_mainaccount(mainaccount):
|
|
54
|
+
mainaccount_string = str(mainaccount)
|
|
55
|
+
if len(mainaccount_string) == 20 and mainaccount_string[:2] == 'MA':
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def to_param_dict(func, vals, exclude_none=True, func_args_check=True):
|
|
61
|
+
args = getargspec(func)[0]
|
|
62
|
+
arg_names = list(args)
|
|
63
|
+
# The bit of regex magic below is for arguments that are keywords in
|
|
64
|
+
# Python, like from. These can't be used directly, so our convention is to
|
|
65
|
+
# add "_" suffixes to them. This strips them out.
|
|
66
|
+
pd = {
|
|
67
|
+
re.sub(r'^(.*)_+$', r'\1', key): value
|
|
68
|
+
for key, value in vals.items()
|
|
69
|
+
if key != 'self' and (key in arg_names or func_args_check == False) and (
|
|
70
|
+
value is not None or exclude_none is False)
|
|
71
|
+
}
|
|
72
|
+
return pd
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class Header:
|
|
2
|
+
def __init__(self, type=None, text=None, media=None):
|
|
3
|
+
self.type = type
|
|
4
|
+
self.text = text
|
|
5
|
+
self.media = media
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Body:
|
|
9
|
+
def __init__(self, text=None):
|
|
10
|
+
self.text = text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Footer:
|
|
14
|
+
def __init__(self, text=None):
|
|
15
|
+
self.text = text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Row:
|
|
19
|
+
def __init__(self, id=None, title=None, description=None):
|
|
20
|
+
self.id = id
|
|
21
|
+
self.title = title
|
|
22
|
+
self.description = description
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Section:
|
|
26
|
+
def __init__(self, title=None, rows=None):
|
|
27
|
+
self.title = title
|
|
28
|
+
self.rows = rows if rows is not None else []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Btn:
|
|
32
|
+
def __init__(self, id=None, title=None, cta_url=None):
|
|
33
|
+
self.id = id
|
|
34
|
+
self.title = title
|
|
35
|
+
self.cta_url = cta_url
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Action:
|
|
39
|
+
def __init__(self, buttons=None, sections=None):
|
|
40
|
+
self.buttons = buttons if buttons is not None else []
|
|
41
|
+
self.sections = sections if sections is not None else []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Interactive:
|
|
45
|
+
def __init__(self, type=None, header=None, body=None, footer=None, action=None):
|
|
46
|
+
self.type = type
|
|
47
|
+
self.header = header
|
|
48
|
+
self.body = body
|
|
49
|
+
self.footer = footer
|
|
50
|
+
self.action = action
|
vobiz/utils/jwt.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
import jwt
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from vobiz.utils.validators import *
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AccessToken:
|
|
9
|
+
"""
|
|
10
|
+
Vobiz JWT AccessToken for SIP endpoint authentication.
|
|
11
|
+
|
|
12
|
+
Used to generate short-lived JWT tokens for WebRTC/SIP clients.
|
|
13
|
+
Not used for REST API authentication (which uses X-Auth-ID / X-Auth-Token headers).
|
|
14
|
+
"""
|
|
15
|
+
auth_id = ''
|
|
16
|
+
username = ''
|
|
17
|
+
valid_from = 0
|
|
18
|
+
lifetime = 86400
|
|
19
|
+
key = ''
|
|
20
|
+
grants = {}
|
|
21
|
+
uid = 0
|
|
22
|
+
|
|
23
|
+
@validate_args(
|
|
24
|
+
auth_id=[is_account_id()],
|
|
25
|
+
auth_token=[optional(of_type(str))],
|
|
26
|
+
username=[all_of(
|
|
27
|
+
of_type(str),
|
|
28
|
+
check(lambda username: len(username) > 0, 'empty username')
|
|
29
|
+
)],
|
|
30
|
+
valid_from=[optional(of_type(int))],
|
|
31
|
+
lifetime=[
|
|
32
|
+
optional(
|
|
33
|
+
all_of(
|
|
34
|
+
of_type(int),
|
|
35
|
+
check(lambda lifetime: 180 <= lifetime <= 86400,
|
|
36
|
+
'180 < lifetime <= 86400')))
|
|
37
|
+
],
|
|
38
|
+
valid_till=[optional(of_type(int))],
|
|
39
|
+
)
|
|
40
|
+
def __init__(self,
|
|
41
|
+
auth_id,
|
|
42
|
+
auth_token,
|
|
43
|
+
username,
|
|
44
|
+
valid_from=None,
|
|
45
|
+
lifetime=None,
|
|
46
|
+
valid_till=None,
|
|
47
|
+
uid=None):
|
|
48
|
+
self.auth_id = auth_id
|
|
49
|
+
self.username = username
|
|
50
|
+
if valid_from:
|
|
51
|
+
self.valid_from = int(valid_from)
|
|
52
|
+
else:
|
|
53
|
+
self.valid_from = int(time.time())
|
|
54
|
+
if lifetime:
|
|
55
|
+
self.lifetime = int(lifetime)
|
|
56
|
+
if valid_till is not None:
|
|
57
|
+
raise ValidationError("use either lifetime or valid_till")
|
|
58
|
+
elif valid_till:
|
|
59
|
+
self.lifetime = valid_till - self.valid_from
|
|
60
|
+
if self.lifetime < 0:
|
|
61
|
+
raise ValidationError(
|
|
62
|
+
"validity expires %s seconds before it starts" %
|
|
63
|
+
self.lifetime)
|
|
64
|
+
if self.lifetime < 180 or self.lifetime > 86400:
|
|
65
|
+
raise ValidationError(
|
|
66
|
+
"validity of %s seconds is out of permitted range [180, 86400]" %
|
|
67
|
+
self.lifetime)
|
|
68
|
+
|
|
69
|
+
self.key = auth_token
|
|
70
|
+
|
|
71
|
+
if uid:
|
|
72
|
+
self.uid = uid
|
|
73
|
+
else:
|
|
74
|
+
self.uid = "%s-%s" % (username, time.time())
|
|
75
|
+
|
|
76
|
+
@validate_args(
|
|
77
|
+
incoming=[optional(of_type_exact(bool))],
|
|
78
|
+
outgoing=[optional(of_type_exact(bool))],
|
|
79
|
+
)
|
|
80
|
+
def add_voice_grants(self, incoming=False, outgoing=False):
|
|
81
|
+
self.grants['voice'] = {
|
|
82
|
+
'incoming_allow': incoming,
|
|
83
|
+
'outgoing_allow': outgoing
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def to_jwt(self):
|
|
87
|
+
headers = {'typ': 'JWT', 'cty': 'vobiz;v=1'}
|
|
88
|
+
algorithm = 'HS256'
|
|
89
|
+
claims = {
|
|
90
|
+
'jti': self.uid,
|
|
91
|
+
'iss': self.auth_id,
|
|
92
|
+
'sub': self.username,
|
|
93
|
+
'nbf': self.valid_from,
|
|
94
|
+
'exp': self.valid_from + self.lifetime,
|
|
95
|
+
'grants': self.grants
|
|
96
|
+
}
|
|
97
|
+
return jwt.encode(claims, self.key, algorithm=algorithm, headers=headers)
|