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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kbot-py-client
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Client for Konverso Kbot
5
5
  Home-page: https://www.konverso.ai
6
6
  Author: Konverso
@@ -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='http', api_key=None, verify=True):
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
- self.url = "%s://%s:%s" % (proto, server, port)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kbot-py-client
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Client for Konverso Kbot
5
5
  Home-page: https://www.konverso.ai
6
6
  Author: Konverso
@@ -4,6 +4,7 @@ README.md
4
4
  setup.py
5
5
  kbot_client/__init__.py
6
6
  kbot_client/client.py
7
+ kbot_client/client.recording.py
7
8
  kbot_client/client_group.py
8
9
  kbot_client/folder_sync.py
9
10
  kbot_client/test.py
@@ -5,7 +5,7 @@ with open("README.md", mode="r", encoding="utf-8") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="kbot-py-client",
8
- version="2.0.1",
8
+ version="2.0.2",
9
9
  author="Konverso",
10
10
  author_email="contact@konverso.ai",
11
11
  description="Client for Konverso Kbot",
File without changes
File without changes
File without changes