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
vobiz/utils/location.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from hmac import new as sign
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from urllib.parse import urlparse, urlunparse, parse_qs
|
|
5
|
+
from base64 import encodebytes as encode
|
|
6
|
+
|
|
7
|
+
from .validators import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def string_format(value):
|
|
11
|
+
if isinstance(value, bytes):
|
|
12
|
+
return ''.join(chr(x) for x in bytearray(value))
|
|
13
|
+
if isinstance(value, (int, float, bool)):
|
|
14
|
+
return str(value)
|
|
15
|
+
if isinstance(value, list):
|
|
16
|
+
return [string_format(x) for x in value]
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_map_from_query(query):
|
|
21
|
+
res_map = dict()
|
|
22
|
+
for key, value in parse_qs(query, keep_blank_values=True).items():
|
|
23
|
+
res_map[string_format(key)] = string_format(value)
|
|
24
|
+
return res_map
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_sorted_query_string(params):
|
|
28
|
+
keys = sorted(params.keys())
|
|
29
|
+
res_params = []
|
|
30
|
+
for key in keys:
|
|
31
|
+
value = params[key]
|
|
32
|
+
if isinstance(value, list):
|
|
33
|
+
res_params.append(
|
|
34
|
+
'&'.join(['{}={}'.format(string_format(key), val) for val in sorted(string_format(value))]))
|
|
35
|
+
else:
|
|
36
|
+
res_params.append('{}={}'.format(string_format(key), string_format(value)))
|
|
37
|
+
return '&'.join(res_params)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_sorted_params_string(params):
|
|
41
|
+
keys = sorted(params.keys())
|
|
42
|
+
res_params = []
|
|
43
|
+
for key in keys:
|
|
44
|
+
value = params[key]
|
|
45
|
+
if isinstance(value, list):
|
|
46
|
+
res_params.append(
|
|
47
|
+
''.join(['{}{}'.format(string_format(key), val) for val in sorted(string_format(value))]))
|
|
48
|
+
elif isinstance(value, dict):
|
|
49
|
+
res_params.append('{}{}'.format(string_format(key), get_sorted_params_string(value)))
|
|
50
|
+
else:
|
|
51
|
+
res_params.append('{}{}'.format(string_format(key), string_format(value)))
|
|
52
|
+
return ''.join(res_params)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def construct_get_url(uri, params, empty_post_params=True):
|
|
56
|
+
parsed_uri = urlparse(uri.encode('utf-8'))
|
|
57
|
+
base_url = urlunparse((parsed_uri.scheme.decode('utf-8'),
|
|
58
|
+
parsed_uri.netloc.decode('utf-8'),
|
|
59
|
+
parsed_uri.path.decode('utf-8'), '', '',
|
|
60
|
+
'')).encode('utf-8')
|
|
61
|
+
|
|
62
|
+
params.update(get_map_from_query(parsed_uri.query))
|
|
63
|
+
query_params = get_sorted_query_string(params)
|
|
64
|
+
if len(query_params) > 0 or not empty_post_params:
|
|
65
|
+
base_url = base_url + bytearray('?' + query_params, 'utf-8')
|
|
66
|
+
if len(query_params) > 0 and not empty_post_params:
|
|
67
|
+
base_url = base_url + bytearray('.', 'utf-8')
|
|
68
|
+
return base_url
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def construct_post_url(uri, params):
|
|
72
|
+
base_url = construct_get_url(uri, dict(), True if len(params) == 0 else False)
|
|
73
|
+
return base_url + bytearray(get_sorted_params_string(params), 'utf-8')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_signature_v3(auth_token, base_url, nonce):
|
|
77
|
+
base_url = bytearray('{}.{}'.format(string_format(base_url), string_format(nonce)), 'utf-8')
|
|
78
|
+
try:
|
|
79
|
+
return encode(sign(auth_token, base_url, sha256).digest()).strip()
|
|
80
|
+
except TypeError:
|
|
81
|
+
return encode(sign(bytearray(auth_token, 'utf-8'), base_url, sha256).digest()).strip()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@validate_args(
|
|
85
|
+
method=[all_of(of_type(str), is_in(('POST', 'GET'), case_sensitive=False))],
|
|
86
|
+
uri=[is_url()],
|
|
87
|
+
params=[optional(of_type(dict))],
|
|
88
|
+
nonce=[of_type(str)],
|
|
89
|
+
auth_token=[of_type(str)],
|
|
90
|
+
v3_signature=[of_type(str)],
|
|
91
|
+
)
|
|
92
|
+
def validate_v3_signature(method, uri, nonce, auth_token, v3_signature, params=None):
|
|
93
|
+
"""
|
|
94
|
+
Validates V3 Signature received from Vobiz to your server.
|
|
95
|
+
|
|
96
|
+
:param method: Your callback method (GET or POST)
|
|
97
|
+
:param uri: Your callback URL
|
|
98
|
+
:param params: Params received in callback from Vobiz
|
|
99
|
+
:param nonce: X-Vobiz-Signature-V3-Nonce header
|
|
100
|
+
:param v3_signature: X-Vobiz-Signature-V3 header
|
|
101
|
+
:param auth_token: (Sub)Account auth token
|
|
102
|
+
:return: True if the request matches signature, False otherwise
|
|
103
|
+
"""
|
|
104
|
+
if params is None:
|
|
105
|
+
params = dict()
|
|
106
|
+
auth_token = bytes(auth_token.encode('utf-8'))
|
|
107
|
+
nonce = bytes(nonce.encode('utf-8'))
|
|
108
|
+
v3_signature = bytes(v3_signature.encode('utf-8'))
|
|
109
|
+
base_url = construct_get_url(uri, params).decode('utf-8') if method == 'GET' else construct_post_url(uri, params).decode('utf-8')
|
|
110
|
+
signature = get_signature_v3(auth_token, base_url, nonce)
|
|
111
|
+
return signature in v3_signature.split(b',')
|
vobiz/utils/template.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from vobiz.utils.location import Location
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Parameter:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
type,
|
|
8
|
+
text=None,
|
|
9
|
+
media=None,
|
|
10
|
+
payload=None,
|
|
11
|
+
currency=None,
|
|
12
|
+
date_time=None,
|
|
13
|
+
location=None,
|
|
14
|
+
parameter_name=None,
|
|
15
|
+
):
|
|
16
|
+
self.type = type
|
|
17
|
+
self.text = text
|
|
18
|
+
self.media = media
|
|
19
|
+
self.payload = payload
|
|
20
|
+
self.currency = Currency(**currency) if currency else None
|
|
21
|
+
self.date_time = DateTime(**date_time) if date_time else None
|
|
22
|
+
self.location = location
|
|
23
|
+
self.parameter_name = parameter_name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Component:
|
|
27
|
+
def __init__(self, type, sub_type=None, index=None, parameters=None):
|
|
28
|
+
self.type = type
|
|
29
|
+
self.sub_type = sub_type
|
|
30
|
+
self.index = index
|
|
31
|
+
self.parameters = parameters if parameters is not None else []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Template:
|
|
35
|
+
def __init__(self, name, language, components=None):
|
|
36
|
+
self.name = name
|
|
37
|
+
self.language = language
|
|
38
|
+
self.components = components if components is not None else []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Currency:
|
|
42
|
+
def __init__(self, fallback_value, currency_code, amount_1000):
|
|
43
|
+
self.fallback_value = fallback_value
|
|
44
|
+
self.currency_code = currency_code
|
|
45
|
+
self.amount_1000 = amount_1000
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DateTime:
|
|
49
|
+
def __init__(self, fallback_value):
|
|
50
|
+
self.fallback_value = fallback_value
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import functools
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import decorator
|
|
8
|
+
|
|
9
|
+
from vobiz.exceptions import ValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def regex(regex):
|
|
13
|
+
rexp = re.compile(regex)
|
|
14
|
+
|
|
15
|
+
def f(name, value):
|
|
16
|
+
if not rexp.match(value):
|
|
17
|
+
return None, [
|
|
18
|
+
'{} should match format {} (actual value: {})'.format(
|
|
19
|
+
name, regex, value)
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
return value, []
|
|
23
|
+
|
|
24
|
+
return f
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def all_of(*validators):
|
|
28
|
+
def f(name, value):
|
|
29
|
+
for validator in validators:
|
|
30
|
+
value, errs = validator(name, value)
|
|
31
|
+
if errs:
|
|
32
|
+
return None, errs
|
|
33
|
+
return value, []
|
|
34
|
+
|
|
35
|
+
return f
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def one_of(*validators):
|
|
39
|
+
def f(name, value):
|
|
40
|
+
for validator in validators:
|
|
41
|
+
new_val, errs = validator(name, value)
|
|
42
|
+
if errs:
|
|
43
|
+
continue
|
|
44
|
+
else:
|
|
45
|
+
return new_val, []
|
|
46
|
+
return None, '{} did not satisfy any of the required types'.format(
|
|
47
|
+
name)
|
|
48
|
+
|
|
49
|
+
return f
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def check(checker, message=None):
|
|
53
|
+
def f(name, value):
|
|
54
|
+
inr = checker(value)
|
|
55
|
+
msg = message or '{} should be in range'.format(name)
|
|
56
|
+
if inr:
|
|
57
|
+
return value, []
|
|
58
|
+
else:
|
|
59
|
+
return None, ['{} (actual value: {})'.format(msg, value)]
|
|
60
|
+
|
|
61
|
+
return f
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_in(iterable, message=None, case_sensitive=True, case_type='upper'):
|
|
65
|
+
def f(name, value):
|
|
66
|
+
actual_value = value
|
|
67
|
+
if not case_sensitive:
|
|
68
|
+
if case_type == 'upper':
|
|
69
|
+
value = str(value).upper()
|
|
70
|
+
elif case_type == 'lower':
|
|
71
|
+
value = str(value).lower()
|
|
72
|
+
elif case_type == 'title':
|
|
73
|
+
value = str(value).title()
|
|
74
|
+
|
|
75
|
+
msg = message or '{} should be in {}'.format(name, iterable)
|
|
76
|
+
if value in iterable:
|
|
77
|
+
return value, []
|
|
78
|
+
else:
|
|
79
|
+
return None, ['{} (actual value: {})'.format(msg, actual_value)]
|
|
80
|
+
return f
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def multi_is_in(iterable, message=None, case_sensitive=True, make_lower_case=False, separator=','):
|
|
84
|
+
def f(name, value):
|
|
85
|
+
actual_value = value
|
|
86
|
+
if not case_sensitive:
|
|
87
|
+
if make_lower_case:
|
|
88
|
+
value = str(value).lower()
|
|
89
|
+
else:
|
|
90
|
+
value = str(value).upper()
|
|
91
|
+
msg = message or '{} should be among {}. multiple values should be COMMA(,) separated'.format(name, iterable)
|
|
92
|
+
for val in value.split(separator):
|
|
93
|
+
if val not in iterable:
|
|
94
|
+
return None, ['{} (actual value: {})'.format(msg, actual_value)]
|
|
95
|
+
return value, []
|
|
96
|
+
|
|
97
|
+
return f
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def optional(*validators):
|
|
101
|
+
def f(name, value):
|
|
102
|
+
if value is None:
|
|
103
|
+
return None, []
|
|
104
|
+
return all_of(*validators)(name, value)
|
|
105
|
+
|
|
106
|
+
return f
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def required(validate):
|
|
110
|
+
def f(name, value):
|
|
111
|
+
if not value:
|
|
112
|
+
return None, '{} is required (current value: None)'.format(name)
|
|
113
|
+
return validate(name, value)
|
|
114
|
+
|
|
115
|
+
return f
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def of_type(*args):
|
|
119
|
+
def f(name, value):
|
|
120
|
+
if value is None:
|
|
121
|
+
return None, ['{name} cannot be None'.format(name=name)]
|
|
122
|
+
for typ in args:
|
|
123
|
+
if isinstance(typ, str):
|
|
124
|
+
parts = typ.split('.')
|
|
125
|
+
typ = getattr(
|
|
126
|
+
importlib.import_module('.'.join(parts[:-1])), parts[-1])
|
|
127
|
+
try:
|
|
128
|
+
value = typ(value)
|
|
129
|
+
return value, []
|
|
130
|
+
except ValueError:
|
|
131
|
+
pass
|
|
132
|
+
return None, [
|
|
133
|
+
'{name} should be of type: {types}'.format(
|
|
134
|
+
name=name, types=[arg.__name__ for arg in args])
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
return f
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def of_type_exact(*args):
|
|
141
|
+
def f(name, value):
|
|
142
|
+
for typ in args:
|
|
143
|
+
if isinstance(typ, str):
|
|
144
|
+
parts = typ.split('.')
|
|
145
|
+
typ = getattr(
|
|
146
|
+
importlib.import_module('.'.join(parts[:-1])), parts[-1])
|
|
147
|
+
if not isinstance(value, typ):
|
|
148
|
+
continue
|
|
149
|
+
return value, []
|
|
150
|
+
return None, [
|
|
151
|
+
'{name} should be of type: {types}'.format(
|
|
152
|
+
name=name,
|
|
153
|
+
types=[getattr(arg, '__name__', str(arg)) for arg in args])
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
return f
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_iterable(validator, sep=None):
|
|
160
|
+
def f(name, value):
|
|
161
|
+
try:
|
|
162
|
+
l = []
|
|
163
|
+
v, e = validator(name, value)
|
|
164
|
+
if v and not e:
|
|
165
|
+
raise TypeError() # hack
|
|
166
|
+
for i, item in enumerate(iter(value)):
|
|
167
|
+
val, errs = validator('{}[{}]'.format(name, i), item)
|
|
168
|
+
if errs:
|
|
169
|
+
return None, errs
|
|
170
|
+
l.append(val)
|
|
171
|
+
ret = (l, []) if not sep else (sep.join(l), [])
|
|
172
|
+
return ret
|
|
173
|
+
except TypeError:
|
|
174
|
+
val, errs = validator(name, value)
|
|
175
|
+
if errs:
|
|
176
|
+
return None, errs
|
|
177
|
+
return ([val], []) if not sep else (val, [])
|
|
178
|
+
|
|
179
|
+
return required(f)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def multiple_valid_integers():
|
|
183
|
+
def f(name, value):
|
|
184
|
+
if isinstance(value, str):
|
|
185
|
+
values = value.split('<')
|
|
186
|
+
for i in values:
|
|
187
|
+
try:
|
|
188
|
+
int(i)
|
|
189
|
+
except ValueError:
|
|
190
|
+
return None, ['{} destination value must be integer'.format(name)]
|
|
191
|
+
return value, []
|
|
192
|
+
return value, []
|
|
193
|
+
return f
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def validate_args(**to_validate):
|
|
197
|
+
def outer(wrapped):
|
|
198
|
+
@functools.wraps(wrapped)
|
|
199
|
+
def wrapper(self, *args, **kwargs):
|
|
200
|
+
params = inspect.getcallargs(wrapped, *args, **kwargs)
|
|
201
|
+
for arg_name, validators in to_validate.items():
|
|
202
|
+
for validator in validators:
|
|
203
|
+
params[arg_name], errs = validator(arg_name,
|
|
204
|
+
params.get(
|
|
205
|
+
arg_name, None))
|
|
206
|
+
if errs:
|
|
207
|
+
raise ValidationError(errs)
|
|
208
|
+
return wrapped(**params)
|
|
209
|
+
|
|
210
|
+
return decorator.decorate(wrapped, wrapper)
|
|
211
|
+
|
|
212
|
+
return outer
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def validate_list_items(instance_type):
|
|
216
|
+
def f(arg_name, value):
|
|
217
|
+
if not isinstance(value, list):
|
|
218
|
+
return [], ["{} must be a list".format(arg_name)]
|
|
219
|
+
|
|
220
|
+
errors = []
|
|
221
|
+
validated_items = []
|
|
222
|
+
for idx, item in enumerate(value):
|
|
223
|
+
flag = 0
|
|
224
|
+
if isinstance(item, dict):
|
|
225
|
+
try:
|
|
226
|
+
instance_type(**item)
|
|
227
|
+
except Exception as exception:
|
|
228
|
+
errors.append(str(exception))
|
|
229
|
+
flag = 1
|
|
230
|
+
else:
|
|
231
|
+
err = "Invalid item at index {} in {}: should be of type {}".format(idx, arg_name, instance_type.__name__)
|
|
232
|
+
if not isinstance(item, instance_type):
|
|
233
|
+
errors.append(err)
|
|
234
|
+
flag = 1
|
|
235
|
+
|
|
236
|
+
if not flag:
|
|
237
|
+
validated_items.append(item)
|
|
238
|
+
|
|
239
|
+
return validated_items, errors
|
|
240
|
+
|
|
241
|
+
return f
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def validate_dict_items(instance_type):
|
|
245
|
+
def validator(arg_name, value):
|
|
246
|
+
if not isinstance(value, dict):
|
|
247
|
+
return None, ["{} must be a dictionary".format(arg_name)]
|
|
248
|
+
|
|
249
|
+
errors = []
|
|
250
|
+
try:
|
|
251
|
+
instance_type(**value)
|
|
252
|
+
except TypeError as e:
|
|
253
|
+
errors.append(str(e))
|
|
254
|
+
|
|
255
|
+
if errors:
|
|
256
|
+
return None, errors
|
|
257
|
+
return value, []
|
|
258
|
+
|
|
259
|
+
return validator
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# Type validator helpers — Python 3 only, no six dependency
|
|
263
|
+
is_valid_date = functools.partial(of_type, str)
|
|
264
|
+
is_phonenumber = functools.partial(of_type, str)
|
|
265
|
+
is_subaccount_id = functools.partial(all_of, of_type(str),
|
|
266
|
+
regex(r'^SA[A-Z0-9]{18}$'))
|
|
267
|
+
is_mainaccount_id = functools.partial(all_of, of_type(str),
|
|
268
|
+
regex(r'^MA[A-Z0-9]{18}$'))
|
|
269
|
+
is_account_id = functools.partial(all_of, of_type(str),
|
|
270
|
+
regex(r'^(M|S)A[A-Z0-9]{18}$'))
|
|
271
|
+
is_subaccount = functools.partial(
|
|
272
|
+
one_of, of_type_exact('vobiz.resources.accounts.Subaccount'),
|
|
273
|
+
is_subaccount_id())
|
|
274
|
+
is_url = functools.partial(
|
|
275
|
+
all_of, of_type(str),
|
|
276
|
+
regex(
|
|
277
|
+
r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+|None)'
|
|
278
|
+
))
|
|
279
|
+
is_proper_date_format = functools.partial(all_of, of_type_exact(str),
|
|
280
|
+
regex(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2}(\.\d{1,6})?)?$'))
|
vobiz/version.py
ADDED