-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathzssdk.py
executable file
·426 lines (330 loc) · 13.6 KB
/
zssdk.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
import re
import urllib3
import string
import json
from uuid import uuid4
import time
import threading
import functools
import traceback
CONFIG_HOSTNAME = 'hostname'
CONFIG_PORT = 'port'
CONFIG_POLLING_TIMEOUT = 'default_polling_timeout'
CONFIG_POLLING_INTERVAL = 'default_polling_interval'
CONFIG_WEBHOOK = 'webhook'
CONFIG_READ_TIMEOUT = 'read_timeout'
CONFIG_WRITE_TIMEOUT = 'write_timeout'
HEADER_JOB_UUID = "X-Job-UUID"
HEADER_WEBHOOK = "X-Web-Hook"
HEADER_JOB_SUCCESS = "X-Job-Success"
HEADER_AUTHORIZATION = "Authorization"
OAUTH = "OAuth"
LOCATION = "location"
HTTP_ERROR = "sdk.1000"
POLLING_TIMEOUT_ERROR = "sdk.1001"
INTERNAL_ERROR = "sdk.1002"
__config__ = {}
class SdkError(Exception):
pass
def _exception_safe(func):
@functools.wraps(func)
def wrap(*args, **kwargs):
try:
func(*args, **kwargs)
except:
print traceback.format_exc()
return wrap
def _error_if_not_configured():
if not __config__:
raise SdkError('call configure() before using any APIs')
def _http_error(status, body=None):
err = ErrorCode()
err.code = HTTP_ERROR
err.description = 'the http status code[%s] indicates a failure happened' % status
err.details = body
return {'error': err}
def _error(code, desc, details):
err = ErrorCode()
err.code = code
err.desc = desc
err.details = details
return {'error': err}
def configure(
hostname='127.0.0.1',
port=8080,
polling_timeout=3600*3,
polling_interval=1,
read_timeout=15,
write_timeout=15,
web_hook=None
):
__config__[CONFIG_HOSTNAME] = hostname
__config__[CONFIG_PORT] = port
__config__[CONFIG_POLLING_TIMEOUT] = polling_timeout
__config__[CONFIG_POLLING_INTERVAL] = polling_interval
__config__[CONFIG_WEBHOOK] = web_hook
__config__[CONFIG_READ_TIMEOUT] = read_timeout
__config__[CONFIG_WRITE_TIMEOUT] = write_timeout
class ParamAnnotation(object):
def __init__(
self,
required=False,
valid_values=None,
valid_regex_values=None,
max_length=None,
min_length=None,
non_empty=None,
null_elements=None,
empty_string=None,
number_range=None,
no_trim=False
):
self.required = required
self.valid_values = valid_values
self.valid_regex_values = valid_regex_values
self.max_length = max_length
self.min_length = min_length
self.non_empty = non_empty
self.null_elements = null_elements
self.empty_string = empty_string
self.number_range = number_range
self.no_trim = no_trim
class ErrorCode(object):
def __init__(self):
self.code = None
self.description = None
self.details = None
self.cause = None
class Obj(object):
def __init__(self, d):
for a, b in d.items():
if isinstance(b, (list, tuple)):
setattr(self, a, [Obj(x) if isinstance(x, dict) else x for x in b])
else:
setattr(self, a, Obj(b) if isinstance(b, dict) else b)
class AbstractAction(object):
def __init__(self):
self.apiId = None
self.sessionId = None
self.systemTags = None
self.userTags = None
self.resourceUuid = None
self.timeout = None
self.pollingInterval = None
self._param_descriptors = {
'sessionId': ParamAnnotation(required=self.NEED_SESSION),
'systemTags': ParamAnnotation(),
'userTags': ParamAnnotation(),
'resourceUuid': ParamAnnotation()
}
self._param_descriptors.update(self.PARAMS)
def _check_params(self):
for param_name, annotation in self._param_descriptors.items():
value = getattr(self, param_name, None)
if value is None and annotation.required:
raise SdkError('missing a mandatory parameter[%s]' % param_name)
if value is not None and annotation.valid_values and value not in annotation.valid_values:
raise SdkError('invalid parameter[%s], the value[%s] is not in the valid options%s' % (param_name, value, annotation.valid_values))
if value is not None and isinstance(value, str) and annotation.max_length and len(value) > annotation.max_length:
raise SdkError('invalid length[%s] of the parameter[%s], the max allowed length is %s' % (len(value), param_name, annotation.max_length))
if value is not None and isinstance(value, str) and annotation.min_length and len(value) > annotation.min_length:
raise SdkError('invalid length[%s] of the parameter[%s], the minimal allowed length is %s' % (len(value), param_name, annotation.min_length))
if value is not None and isinstance(value, list) and annotation.non_empty is True and len(value) == 0:
raise SdkError('invalid parameter[%s], it cannot be an empty list' % param_name)
if value is not None and isinstance(value, list) and annotation.null_elements is True and None in value:
raise SdkError('invalid parameter[%s], the list cannot contain a null element' % param_name)
if value is not None and isinstance(value, str) and annotation.empty_string is False and len(value) == 0:
raise SdkError('invalid parameter[%s], it cannot be an empty string' % param_name)
if value is not None and isinstance(value, int) or isinstance(value, long) and len(annotation.number_range) == 2:
low = annotation.number_range[0]
high = annotation.number_range[1]
if value < low or value > high:
raise SdkError('invalid parameter[%s], its value is not in the valid range' % annotation.number_range)
if value is not None and isinstance(value, str) and annotation.no_trim is False:
value = str(value).strip()
setattr(self, param_name, value)
def _params(self):
ret = {}
for k, _ in self._param_descriptors.items():
val = getattr(self, k, None)
if val is not None:
ret[k] = val
return ret
def _url(self):
elements = ['http://', __config__[CONFIG_HOSTNAME], ':', str(__config__[CONFIG_PORT]), '/v1']
path = self.PATH.replace('{', '${')
unresolved = re.findall('${(.+?)}', path)
params = self._params()
if unresolved:
for u in unresolved:
if u in params:
raise SdkError('missing a mandatory parameter[%s]' % u)
path = string.Template(path).substitute(params)
elements.append(path)
if self.HTTP_METHOD == 'GET' or self.HTTP_METHOD == 'DELETE':
elements.append('?')
elements.append('&'.join(['%s=%s' % (k, v) for k, v in params]))
return ''.join(elements), unresolved
def call(self, cb=None):
def _return(result):
if cb:
cb(result)
else:
return result
_error_if_not_configured()
self._check_params()
url, params_in_url = self._url()
headers = {}
if self.apiId is not None:
headers[HEADER_JOB_UUID] = self.apiId
else:
headers[HEADER_JOB_UUID] = _uuid()
if self.NEED_SESSION:
headers[HEADER_AUTHORIZATION] = "%s %s" % (OAUTH, self.sessionId)
web_hook = __config__.get(CONFIG_WEBHOOK, None)
if web_hook is not None:
headers[CONFIG_WEBHOOK] = web_hook
params = self._params()
body = None
if self.HTTP_METHOD == 'POST' or self.HTTP_METHOD == 'PUT':
m = {}
for k, v in params.items():
if v is None:
continue
if k in params_in_url:
continue
m[k] = v
body = {self.PARAM_NAME: m}
if not self.timeout:
self.timeout = __config__[CONFIG_READ_TIMEOUT]
rsp = _json_http(uri=url, body=body, headers=headers, method=self.HTTP_METHOD, timeout=self.timeout)
if rsp.status < 200 or rsp.status >= 300:
return _return(Obj(_http_error(rsp.status, rsp.data)))
elif rsp.status == 200 or rsp.status == 204:
# the API completes
return _return(Obj(self._write_result(rsp)))
elif rsp.status == 202:
# the API needs polling
return self._poll_result(rsp, cb)
else:
raise SdkError('[Internal Error] the server returns an unknown status code[%s], body[%s]' % (rsp.status, rsp.data))
def _write_result(self, rsp):
if rsp.status == 200:
return {"value": json.loads(rsp.data)}
elif rsp.status == 503:
return json.loads(rsp.data)
else:
raise SdkError('unknown status code[%s]' % rsp.status)
def _poll_result(self, rsp, cb):
if not self.NEED_POLL:
raise SdkError('[Internal Error] the api is not an async API but the server returns 202 status code')
m = json.loads(rsp.data)
location = m[LOCATION]
if not location:
raise SdkError("Internal Error] the api[%s] is an async API but the server doesn't return the polling location url")
if cb:
# async polling
self._async_poll(location, cb)
else:
# sync polling
return self._sync_polling(location)
def _fill_timeout_parameters(self):
if self.timeout is None:
self.timeout = __config__.get(CONFIG_POLLING_TIMEOUT)
if self.pollingInterval is None:
self.pollingInterval = __config__.get(CONFIG_POLLING_INTERVAL)
def _async_poll(self, location, cb):
self._fill_timeout_parameters()
timer = None
count = [0]
def _cancel():
timer.cancel()
@_exception_safe
def _polling():
rsp = _json_http(
uri=location,
headers={HEADER_AUTHORIZATION: "%s %s" % (OAUTH, self.sessionId)},
method='GET'
)
if rsp.status not in [200, 503, 202]:
cb(Obj(_http_error(rsp.status, rsp.data)))
_cancel()
elif rsp.status in [200, 503]:
cb(Obj(self._write_result(rsp)))
_cancel()
else:
count[0] += self.pollingInterval
if count[0] >= self.timeout:
cb(Obj(_error(POLLING_TIMEOUT_ERROR, 'polling an API result time out',
'failed to poll the result after %s seconds' % self.timeout)))
_cancel()
timer = threading.Timer(_polling, self.pollingInterval)
timer.start()
def _sync_polling(self, location):
count = 0
self._fill_timeout_parameters()
while count < self.timeout:
rsp = _json_http(
uri=location,
headers={HEADER_AUTHORIZATION: "%s %s" % (OAUTH, self.sessionId)},
method='GET'
)
if rsp.status not in [200, 503, 202]:
return Obj(_http_error(rsp.status, rsp.data))
time.sleep(self.pollingInterval)
count += self.pollingInterval
return Obj(_error(POLLING_TIMEOUT_ERROR, 'polling an API result time out',
'failed to poll the result after %s seconds' % self.timeout))
class QueryAction(AbstractAction):
def __init__(self):
super(QueryAction, self).__init__()
def _uuid():
return str(uuid4()).replace('-', '')
def _json_http(
uri,
body=None,
headers={},
method='POST',
timeout=120.0
):
pool = urllib3.PoolManager(timeout=timeout, retries=urllib3.util.retry.Retry(15))
#pool = urllib3.PoolManager()
headers.update({'Content-Type': 'application/json', 'Connection': 'close'})
if body is not None and not isinstance(body, str):
body = json.dumps(body).encode('utf-8')
print '[Request]: url=%s, headers=%s, body=%s' % (uri, headers, body)
if body:
headers['Content-Length'] = len(body)
rsp = pool.request(method, uri, body=body, headers=headers)
else:
rsp = pool.request(method, uri, headers=headers)
print '[Resposne to %s]: status: %s, body: %s' % (uri, rsp.status, rsp.data)
return rsp
class CreateZoneAction(AbstractAction):
HTTP_METHOD = 'POST'
PATH = '/zones'
NEED_SESSION = True
NEED_POLL = True
PARAM_NAME = 'params'
PARAMS = {
'name': ParamAnnotation(required=True, max_length=255),
'description': ParamAnnotation(max_length=2048)
}
def __init__(self):
super(CreateZoneAction, self).__init__()
self.name = None
self.description = None
class LogInByAccountAction(AbstractAction):
HTTP_METHOD = 'PUT'
PATH = '/accounts/login'
NEED_SESSION = False
NEED_POLL = False
PARAM_NAME = 'logInByAccount'
PARAMS = {
'accountName': ParamAnnotation(required=True),
'password': ParamAnnotation(required=True)
}
def __init__(self):
super(LogInByAccountAction, self).__init__()
self.password = None
self.accountName = None