kbot-py-client 2.0.1__tar.gz → 2.0.2__tar.gz
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.
- {kbot_py_client-2.0.1/kbot_py_client.egg-info → kbot_py_client-2.0.2}/PKG-INFO +1 -1
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_client/client.py +19 -2
- kbot_py_client-2.0.2/kbot_client/client.recording.py +318 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2/kbot_py_client.egg-info}/PKG-INFO +1 -1
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_py_client.egg-info/SOURCES.txt +1 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/setup.py +1 -1
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/.gitignore +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/LICENSE +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/README.md +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_client/__init__.py +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_client/client_group.py +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_client/folder_sync.py +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_client/test.py +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_py_client.egg-info/dependency_links.txt +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_py_client.egg-info/requires.txt +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/kbot_py_client.egg-info/top_level.txt +0 -0
- {kbot_py_client-2.0.1 → kbot_py_client-2.0.2}/setup.cfg +0 -0
|
@@ -8,7 +8,17 @@ import requests
|
|
|
8
8
|
class Client:
|
|
9
9
|
"""Base representation of Kbot instance"""
|
|
10
10
|
|
|
11
|
-
def __init__(self, server, port=443, proto='
|
|
11
|
+
def __init__(self, server, port=443, proto='https', api_key=None, verify=True):
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
- server: an Host or Ip Address
|
|
17
|
+
- port : default to 443
|
|
18
|
+
- proto: HTTP or HTTPS
|
|
19
|
+
- api_key: A Kbot valid API Key. If provided all requests will be sent with this API Key
|
|
20
|
+
and it is not required to invoke the login method
|
|
21
|
+
"""
|
|
12
22
|
|
|
13
23
|
self.host = server
|
|
14
24
|
self.verify = verify
|
|
@@ -18,7 +28,10 @@ class Client:
|
|
|
18
28
|
else:
|
|
19
29
|
proto = 'http'
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
if port in (80, 443):
|
|
32
|
+
self.url = "%s://%s" % (proto, server)
|
|
33
|
+
else:
|
|
34
|
+
self.url = "%s://%s:%s" % (proto, server, port)
|
|
22
35
|
|
|
23
36
|
self._login = False
|
|
24
37
|
self._headers = {}
|
|
@@ -87,6 +100,10 @@ class Client:
|
|
|
87
100
|
r.raise_for_status()
|
|
88
101
|
|
|
89
102
|
self.__reset_headers(r.json())
|
|
103
|
+
# After we impersonnate, we need to remove the original API KEY
|
|
104
|
+
if "X-API-KEY" in self._headers:
|
|
105
|
+
del self._headers["X-API-KEY"]
|
|
106
|
+
|
|
90
107
|
self.schema()
|
|
91
108
|
|
|
92
109
|
def schema(self):
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Dialog tester"""
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Client:
|
|
9
|
+
"""Base representation of Kbot instance"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, server, port=443, proto='https', api_key=None, verify=True):
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
- server: an Host or Ip Address
|
|
17
|
+
- port : default to 443
|
|
18
|
+
- proto: HTTP or HTTPS
|
|
19
|
+
- api_key: A Kbot valid API Key. If provided all requests will be sent with this API Key
|
|
20
|
+
and it is not required to invoke the login method
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
self.host = server
|
|
24
|
+
self.verify = verify
|
|
25
|
+
|
|
26
|
+
if str(port).endswith('443'):
|
|
27
|
+
proto = 'https'
|
|
28
|
+
else:
|
|
29
|
+
proto = 'http'
|
|
30
|
+
|
|
31
|
+
if proto == "https" and port == 443:
|
|
32
|
+
self.url = "%s://%s" % (proto, server)
|
|
33
|
+
elif proto == "https" and port == 443:
|
|
34
|
+
self.url = "%s://%s" % (proto, server)
|
|
35
|
+
else:
|
|
36
|
+
self.url = "%s://%s:%s" % (proto, server, port)
|
|
37
|
+
|
|
38
|
+
self._login = False
|
|
39
|
+
self._headers = {}
|
|
40
|
+
|
|
41
|
+
self._recording_fd = None
|
|
42
|
+
|
|
43
|
+
if api_key:
|
|
44
|
+
self._headers = {
|
|
45
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
46
|
+
'X-API-KEY': api_key
|
|
47
|
+
}
|
|
48
|
+
self.schema()
|
|
49
|
+
|
|
50
|
+
# Variable populated by the login:
|
|
51
|
+
self._user_id = None
|
|
52
|
+
|
|
53
|
+
# Variable populated by the conversation
|
|
54
|
+
self._cid = None
|
|
55
|
+
|
|
56
|
+
# The refresh token may be used in case
|
|
57
|
+
# the access token as expired
|
|
58
|
+
self.__refresh_token = None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def admin_url(self):
|
|
62
|
+
"""Returns the URL of the Kbot Administration view"""
|
|
63
|
+
return "%s/admin" % self.url
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def chat_url(self):
|
|
67
|
+
"""Returns the default URL of the Kbot chat view"""
|
|
68
|
+
return "%s" % self.url
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def avatar_url(self):
|
|
72
|
+
return "%s/images/kbot_avatar.png" % self.url
|
|
73
|
+
|
|
74
|
+
def record_start(self, file_descriptor):
|
|
75
|
+
self._recording_fd = file_descriptor
|
|
76
|
+
self._recording_fd.write("import requests\n\n")
|
|
77
|
+
|
|
78
|
+
def record_stop(self):
|
|
79
|
+
self._recording_fd = None
|
|
80
|
+
|
|
81
|
+
def login(self, username, password=None, timeout=5):
|
|
82
|
+
"""Login to local channel"""
|
|
83
|
+
data = {}
|
|
84
|
+
data['username'] = username
|
|
85
|
+
data['usertype'] = 'local'
|
|
86
|
+
if password is not None:
|
|
87
|
+
data['password'] = password
|
|
88
|
+
else:
|
|
89
|
+
print("WARNING!!! Password is not set!!! Trying to use username...")
|
|
90
|
+
data['password'] = username
|
|
91
|
+
|
|
92
|
+
headers = {}
|
|
93
|
+
headers['Content-Type'] = 'application/json; charset=utf-8'
|
|
94
|
+
|
|
95
|
+
url = self.url + '/api/login'
|
|
96
|
+
r = requests.post(url, headers=headers, data=json.dumps(data), timeout=timeout, verify=self.verify)
|
|
97
|
+
|
|
98
|
+
r.raise_for_status()
|
|
99
|
+
|
|
100
|
+
self.__reset_headers(r.json())
|
|
101
|
+
self.schema()
|
|
102
|
+
|
|
103
|
+
def impersonate(self, username, usertype='local', external_auth='', application_uuid='', timeout=5):
|
|
104
|
+
"""Impersonate the given user"""
|
|
105
|
+
|
|
106
|
+
r = self.request("post", "user/impersonate", data={
|
|
107
|
+
"username": username,
|
|
108
|
+
"im_type": usertype,
|
|
109
|
+
"external_auth": external_auth,
|
|
110
|
+
"application_uuid": application_uuid})
|
|
111
|
+
|
|
112
|
+
r.raise_for_status()
|
|
113
|
+
|
|
114
|
+
self.__reset_headers(r.json())
|
|
115
|
+
# After we impersonnate, we need to remove the original API KEY
|
|
116
|
+
if "X-API-KEY" in self._headers:
|
|
117
|
+
del self._headers["X-API-KEY"]
|
|
118
|
+
|
|
119
|
+
self.schema()
|
|
120
|
+
|
|
121
|
+
def schema(self):
|
|
122
|
+
"""Retrieve the public schema of kbot and builds python method for each of the defined end points"""
|
|
123
|
+
r = self.get('schema')
|
|
124
|
+
r.raise_for_status()
|
|
125
|
+
|
|
126
|
+
j = r.json()
|
|
127
|
+
for epoint in j.get('endpoints', []):
|
|
128
|
+
if epoint['name'] != 'schema':
|
|
129
|
+
self.__add_method(epoint['method'], epoint['name'], epoint['path'], epoint['params'], epoint['data'], epoint.get('description', ''))
|
|
130
|
+
|
|
131
|
+
def __reset_headers(self, js):
|
|
132
|
+
"""
|
|
133
|
+
Called when a login or impersonate end point is called. Then expecting to get a json with:
|
|
134
|
+
- access_token
|
|
135
|
+
- refresh_token
|
|
136
|
+
Or in the case of a refresh call. Then we only get a json with:
|
|
137
|
+
- access_token
|
|
138
|
+
|
|
139
|
+
The header content is ajusted with the token information from the json
|
|
140
|
+
"""
|
|
141
|
+
if not self._headers:
|
|
142
|
+
self._headers = {}
|
|
143
|
+
self._headers['Content-Type'] = 'application/json; charset=utf-8'
|
|
144
|
+
|
|
145
|
+
if js.get('access_token'):
|
|
146
|
+
self._headers['Authorization'] = js['access_token']
|
|
147
|
+
|
|
148
|
+
# The refresh token will be used by the refresh_token method later if the access token expires
|
|
149
|
+
if js.get('refresh_token'):
|
|
150
|
+
self.__refresh_token = js['refresh_token']
|
|
151
|
+
|
|
152
|
+
self._login = True
|
|
153
|
+
if js.get("user_id"):
|
|
154
|
+
self._user_id = js['user_id']
|
|
155
|
+
|
|
156
|
+
def __add_method(self, method: str, name: str, path: str, params: list, data: list, description: str):
|
|
157
|
+
"""Method to add a new dynamic method, based on the schema. This provides "python method" wrapper
|
|
158
|
+
on top of the kbot APIs
|
|
159
|
+
"""
|
|
160
|
+
def endpoint(*args, **kwargs):
|
|
161
|
+
"""Invoke request to endpoint"""
|
|
162
|
+
rargs = {}
|
|
163
|
+
for aname, values in (('params', params), ('data', data)):
|
|
164
|
+
rargs[aname] = {}
|
|
165
|
+
for value in values:
|
|
166
|
+
if value['mandatory'] and value['name'] not in kwargs:
|
|
167
|
+
raise RuntimeError("Missed attribute '%s' in '%s'" % (value['name'], aname))
|
|
168
|
+
# pylint: disable=eval-used
|
|
169
|
+
if value['name'] in kwargs:
|
|
170
|
+
if not isinstance(kwargs[value['name']], eval(value['type'])):
|
|
171
|
+
raise RuntimeError("Invalid type of attribute '%s'" % (value['name']))
|
|
172
|
+
v = kwargs[value['name']]
|
|
173
|
+
elif value['name'] not in kwargs and value['default'] is not None:
|
|
174
|
+
v = value['default']
|
|
175
|
+
else:
|
|
176
|
+
continue
|
|
177
|
+
rargs[aname][value['name']] = v
|
|
178
|
+
return self.__request(method, uri=path % args, **rargs)
|
|
179
|
+
endpoint.__doc__ = description
|
|
180
|
+
endpoint.__name__ = name
|
|
181
|
+
setattr(self, name, endpoint)
|
|
182
|
+
|
|
183
|
+
def __refresh(self):
|
|
184
|
+
r = requests.post(self.url + "/api/refresh",
|
|
185
|
+
data=json.dumps({"refresh_token": self.__refresh_token}),
|
|
186
|
+
headers=self._headers)
|
|
187
|
+
r.raise_for_status()
|
|
188
|
+
self.__reset_headers(r.json())
|
|
189
|
+
|
|
190
|
+
def __request(self, method: str, uri : str = None, data: dict = None, params: dict = None, files: dict = None, attempt=0):
|
|
191
|
+
dump_data = json.dumps(data or {})
|
|
192
|
+
|
|
193
|
+
if files:
|
|
194
|
+
headers = self._headers.copy()
|
|
195
|
+
del headers['Content-Type']
|
|
196
|
+
headers['Accept'] = "*/*"
|
|
197
|
+
else:
|
|
198
|
+
headers = self._headers
|
|
199
|
+
|
|
200
|
+
r = requests.request(method.upper(), self.url + '/api/%s/' % (uri), params=params, data=dump_data, headers=headers, verify=self.verify)
|
|
201
|
+
|
|
202
|
+
# The recording if configured to do so
|
|
203
|
+
if self._recording_fd:
|
|
204
|
+
self._recording_fd.write("\n## {uri}".format(uri='/api/%s/' % (uri)))
|
|
205
|
+
self._recording_fd.write("\nheaders = {headers}".format(headers=json.dumps(headers)))
|
|
206
|
+
self._recording_fd.write("\nparams = {params}".format(params=json.dumps(params)))
|
|
207
|
+
self._recording_fd.write("\ndata = {data}".format(data=json.dumps(data)))
|
|
208
|
+
self._recording_fd.write("\nr = requests.request({method}, {url}, params=params, data=data)".format(
|
|
209
|
+
method=method.upper(), url=self.url + '/api/%s/' % (uri)))
|
|
210
|
+
self._recording_fd.write("\nr.raise_for_status()")
|
|
211
|
+
|
|
212
|
+
if r.status_code == 401:
|
|
213
|
+
# Refresh the token
|
|
214
|
+
try:
|
|
215
|
+
self.__refresh()
|
|
216
|
+
except Exception as e:
|
|
217
|
+
|
|
218
|
+
# We try 3 times at the most
|
|
219
|
+
if attempt == 3:
|
|
220
|
+
raise e
|
|
221
|
+
|
|
222
|
+
time.sleep(3)
|
|
223
|
+
|
|
224
|
+
# Re-invoke the request
|
|
225
|
+
r = self.__request(method, uri=uri, data=data, params=params, attempt=attempt+1)
|
|
226
|
+
|
|
227
|
+
return r
|
|
228
|
+
|
|
229
|
+
def request(self, method: str, uri: str, data: dict = None, params: dict = None):
|
|
230
|
+
return self.__request(method, uri=uri, data=data, params=params)
|
|
231
|
+
|
|
232
|
+
def unit(self, name: str, params=None) -> dict:
|
|
233
|
+
r = self.get(name, params)
|
|
234
|
+
if r:
|
|
235
|
+
return r.json()
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def message(self, cid: int, message: str, timeout: int=60) -> list:
|
|
239
|
+
response = []
|
|
240
|
+
|
|
241
|
+
data = {'type': 'message', 'message': message}
|
|
242
|
+
r = self.request("post", f'conversation/{cid}/message', data)
|
|
243
|
+
|
|
244
|
+
r.raise_for_status()
|
|
245
|
+
|
|
246
|
+
curtime = time.time()
|
|
247
|
+
while timeout > 0:
|
|
248
|
+
r = self.get(f'conversation/{cid}')
|
|
249
|
+
|
|
250
|
+
r.raise_for_status()
|
|
251
|
+
|
|
252
|
+
j = r.json()
|
|
253
|
+
for resp in j:
|
|
254
|
+
if resp['type'] == 'message':
|
|
255
|
+
# It's possible that bot will send several message to one input
|
|
256
|
+
response.append(resp)
|
|
257
|
+
elif resp['type'] in ('stop_topic', 'wait_user_input'):
|
|
258
|
+
# Bot stop to process
|
|
259
|
+
# - stop_topic : bot stop to process message and ready for new topic
|
|
260
|
+
# - wait_user_input : bot asked the question and wait for user answer
|
|
261
|
+
timeout = 0
|
|
262
|
+
timeout = timeout + curtime - time.time()
|
|
263
|
+
return response
|
|
264
|
+
|
|
265
|
+
def logout(self):
|
|
266
|
+
"""Logout from local channel"""
|
|
267
|
+
if self._login:
|
|
268
|
+
|
|
269
|
+
# If we have an open conversation, close it.
|
|
270
|
+
# if self._cid:
|
|
271
|
+
# What for?
|
|
272
|
+
# self._process('logout', 1)
|
|
273
|
+
|
|
274
|
+
# Logout from the APIs
|
|
275
|
+
requests.post(self.url + '/api/logout', headers=self._headers, verify=self.verify)
|
|
276
|
+
|
|
277
|
+
#
|
|
278
|
+
# In addition to the Generated and built in API methods, we have the classic base REST methods
|
|
279
|
+
#
|
|
280
|
+
def get(self, unit, params=None):
|
|
281
|
+
return self.__request("get", unit, params=params)
|
|
282
|
+
|
|
283
|
+
def put(self, unit, data=None):
|
|
284
|
+
return self.__request("put", unit, data=data)
|
|
285
|
+
|
|
286
|
+
def post(self, unit, data=None):
|
|
287
|
+
return self.__request("post", unit, data=data)
|
|
288
|
+
|
|
289
|
+
def delete(self, unit, params=None):
|
|
290
|
+
return self.__request("delete", unit, params=params)
|
|
291
|
+
|
|
292
|
+
def post_file(self, unit, data, params=None, files=None):
|
|
293
|
+
"""Attach the given files.
|
|
294
|
+
Sample parameter values:
|
|
295
|
+
unit = "attachment"
|
|
296
|
+
files = {
|
|
297
|
+
"upload_files": (f, fd, "application/pdf")
|
|
298
|
+
}
|
|
299
|
+
params= {
|
|
300
|
+
"override": False
|
|
301
|
+
}
|
|
302
|
+
data = {
|
|
303
|
+
"folder": current_top_folder,
|
|
304
|
+
"name": f,
|
|
305
|
+
}
|
|
306
|
+
"""
|
|
307
|
+
return self.__request("post", unit, params=params, data=data, files=files)
|
|
308
|
+
|
|
309
|
+
class UpKbotClient(Client):
|
|
310
|
+
"""Represents a currently reachable Kbot instance"""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class DownKbotClient(Client):
|
|
314
|
+
"""Represents a currently not reachable Kbot instance"""
|
|
315
|
+
|
|
316
|
+
def __init__(self, server, port=443, proto='http', error=''):
|
|
317
|
+
super().__init__(server, port=port, proto=proto)
|
|
318
|
+
self.error = error
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|