Skip to content

Commit 20bfcd9

Browse files
committed
- refactored package & unlinked versions API and package
1 parent 8e89450 commit 20bfcd9

File tree

4 files changed

+355
-333
lines changed

4 files changed

+355
-333
lines changed

qsAPI/__init__.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
1+
# -*- coding: UTF-8 -*-
2+
3+
'''
4+
@author: Rafael Sanz
5+
@contact: rafael.sanz@selab.es
6+
@Copyright 2016 <Rafael Sanz - (R)SELAB>
7+
8+
This software is MIT licensed (see terms below)
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
11+
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
12+
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
13+
Software is furnished to do so, subject to the following conditions:
14+
15+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
20+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
'''
22+
123
__version__ = "2.1.0"
2-
__updated__ = '16/09/2020'
3-
__all__ = ['QPS','QRS']
24+
__updated__ = '16/10/2020'
25+
426

5-
from qsAPI.qsAPI import QPS, QRS
27+
from ._interfaces import QPS, QRS
628

29+
__all__ = ('QPS','QRS')

qsAPI/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import sys
1+
# -*- coding: UTF-8 -*-
22
from qsAPI import QRS, QPS, __version__
33

44

@@ -11,10 +11,10 @@ def main():
1111
1212
'''
1313
from argparse import ArgumentParser
14-
import inspect
14+
import sys, inspect
1515
from pprint import pprint
1616

17-
parser = ArgumentParser(description='qsAPI for QlikSense, Rafael Sanz (SELAB)')
17+
parser = ArgumentParser(description='qsAPI {} for QlikSense, Rafael Sanz (SELAB)'.format(__version__))
1818
parser.add_argument('-s', dest='server', required=True,
1919
help='server hostname | hostname:port | https://hostname:port')
2020
parser.add_argument('-u', dest='user', required=False,

qsAPI/_controller.py

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
# -*- coding: UTF-8 -*-
2+
3+
'''
4+
@author: Rafael Sanz
5+
@contact: rafael.sanz@selab.es
6+
@Copyright: 2016 <Rafael Sanz - (R)SELAB>
7+
8+
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
9+
'''
10+
11+
import sys, os.path
12+
import requests as req
13+
import urllib.parse as up
14+
import random, string, json, re
15+
import logging
16+
17+
18+
19+
class _Controller(object):
20+
""" Handler REST-API QRS"""
21+
22+
_referer='Mozilla/5.0 (Windows NT 6.3; Win64; x64) qsAPI APIREST (QSense)'
23+
24+
try:
25+
from requests_ntlm import HttpNtlmAuth as _ntlm
26+
except ImportError:
27+
_ntlm=None
28+
29+
def __init__(self, schema, proxy, port, vproxy, certificate, verify, user, verbosity, logName):
30+
'''
31+
@Function setup: Setup the connection and initialize handlers
32+
@param schema: http/https
33+
@param proxy: hostname to connect
34+
@param port: port number
35+
@param vproxy: virtual proxy conf. {preffix:'proxy', path: '^/qrs/', template:'/{}/qrs/'})
36+
@param certificate: path to .pem client certificate
37+
@param verify: false to trust in self-signed certificates
38+
@param user: dict with keys {userDirectory:, userID:, password:} or tuple
39+
@param verbosity: debug level
40+
@param logger: logger instance name
41+
'''
42+
self.proxy = proxy
43+
self.port = str(port)
44+
self.proxy = proxy;
45+
self.vproxy = None;
46+
self.baseurl = None
47+
self.request = None
48+
self.response = None
49+
self.session = None
50+
51+
if vproxy:
52+
self.setVProxy(**vproxy)
53+
54+
self.setUser(**user) if isinstance(user, dict) else self.setUser(*user)
55+
56+
self.chunk_size = 512 #Kb
57+
58+
self.log=logging.getLogger(logName)
59+
if not self.log.hasHandlers():
60+
self.log.addHandler(logging.StreamHandler(sys.stdout))
61+
self.log.setLevel(verbosity)
62+
63+
self.baseurl= '{schema}://{host}:{port}'.format(schema=schema, host=proxy, port=str(port))
64+
65+
if isinstance(certificate, str):
66+
(base,ext)=os.path.splitext(certificate)
67+
self.cafile=(base+ext, base+'_key'+ext)
68+
self.log.debug('CERTKEY: %s%s', base, ext)
69+
elif isinstance(certificate, tuple):
70+
self.cafile=certificate
71+
self.log.debug('CERT: %s',certificate)
72+
else:
73+
self.cafile=False
74+
75+
self._verify=bool(verify)
76+
77+
if not self._verify:
78+
req.packages.urllib3.disable_warnings()
79+
80+
self.session=req.Session()
81+
82+
if self._ntlm and not self.cafile:
83+
self.log.debug('NTLM authentication enabled')
84+
self.session.auth = self._ntlm('{domain}\\{user}'.format(domain=self.UserDirectory, user=self.UserId), self.Password)
85+
86+
87+
def setVProxy(self, preffix, path, template):
88+
self.vproxy={}
89+
self.vproxy['preffix'] =preffix # proxy
90+
self.vproxy['path'] =re.compile(path) # ^/qrs/
91+
self.vproxy['template']=template # /{}/qrs/
92+
self.vproxy['pxpath'] =template.format(preffix)
93+
94+
95+
def setUser(self, userDirectory, userID, password=None):
96+
self.UserDirectory=userDirectory
97+
self.UserId = userID
98+
self.Password=password
99+
100+
101+
@staticmethod
102+
def normalize(schema, proxy, port, certificate):
103+
104+
if '://' in proxy:
105+
schema, proxy = proxy.split('://')
106+
if not certificate and isinstance(port, int):
107+
port=443
108+
if ':' in proxy:
109+
proxy, port = proxy.split(':')
110+
111+
return(schema, proxy, port)
112+
113+
114+
def _params_prepare(self, param, xhd={}):
115+
116+
par=dict({'Xrfkey': ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))})
117+
if isinstance(param, dict):
118+
for p,v in param.items():
119+
if v is not None:
120+
if isinstance(v, bool):
121+
par[p]=str(v).lower()
122+
else:
123+
par[p]=str(v)
124+
self.log.debug(" >> %s=>%s",p , par[p])
125+
else:
126+
self.log.debug(" >> %s=>(default)", p)
127+
128+
hd= { 'User-agent': self._referer,
129+
'Pragma': 'no-cache',
130+
'X-Qlik-User': 'UserDirectory={directory}; UserId={user}'.format(directory=self.UserDirectory, user=self.UserId),
131+
'x-Qlik-Xrfkey': par.get('Xrfkey'),
132+
'Accept': 'application/json',
133+
'Content-Type': 'application/json'}
134+
135+
if self.vproxy:
136+
hd['X-Qlik-Virtual-Proxy-Prefix']=self.vproxy['preffix']
137+
138+
hd.update(xhd)
139+
return(par, hd)
140+
141+
142+
143+
def _params_update(self, url, par):
144+
scheme, netloc, path, query, fragment=up.urlsplit(url)
145+
if self.vproxy:
146+
path= self.vproxy['path'].sub(self.vproxy['pxpath'], path)
147+
p=up.parse_qs(query)
148+
p.update(par)
149+
query=up.urlencode(p,doseq=True,quote_via=up.quote)
150+
return up.urlunsplit((scheme, netloc, path, query, fragment))
151+
152+
153+
154+
155+
def call(self, method, apipath, param=None, data=None, files=None):
156+
""" initialize control structure """
157+
158+
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
159+
raise ValueError('invalid method <{0}>'.format(method))
160+
161+
self.log.info('API %s <%s>', method[:3], apipath)
162+
163+
(par,hd)=self._params_prepare(param, {} if files is None else {'Content-Type': 'application/vnd.qlik.sense.app'})
164+
165+
# Build the request
166+
self.response= None
167+
168+
url=self._params_update(up.urljoin(self.baseurl,apipath), par)
169+
self.request=req.Request(method, url, headers=hd, data=data, files=files, auth=self.session.auth)
170+
pr=self.session.prepare_request(self.request)
171+
172+
self.log.debug('SEND: %s', self.request.url)
173+
174+
# Execute the HTTP request
175+
self.response = self.session.send(pr, cert=self.cafile, verify=self._verify, allow_redirects=False)
176+
rc=0
177+
while self.response.is_redirect:
178+
rc+=1
179+
if rc > self.session.max_redirects:
180+
raise req.HTTPError('Too many redirections')
181+
self.session.rebuild_auth(self.response.next, self.response)
182+
self.response.next.prepare_headers(hd)
183+
self.response.next.prepare_cookies(self.response.cookies)
184+
self.response.next.url=self._params_update(self.response.next.url, par)
185+
self.log.debug('REDIR: %s', self.response.next.url)
186+
self.response = self.session.send(self.response.next, verify=self._verify, allow_redirects=False)
187+
188+
self.log.debug('RECV: %s',self.response.text)
189+
190+
return(self.response)
191+
192+
193+
194+
def download(self, apipath, filename, param=None):
195+
""" initialize control structure """
196+
197+
self.log.info('API DOWN <%s>', apipath)
198+
199+
(par,hd)=self._params_prepare(param)
200+
201+
# Build the request
202+
self.response= None
203+
204+
url=self._params_update(up.urljoin(self.baseurl,apipath), par)
205+
206+
self.log.debug('__SEND: %s',url)
207+
208+
# Execute the HTTP request
209+
self.request = self.session.get(url, headers=hd, cert=self.cafile, verify=self._verify, stream=True, auth=self.session.auth)
210+
211+
with open(filename, 'wb') as f:
212+
self.log.info('__Downloading (in %sKb blocks): ', str(self.chunk_size))
213+
214+
#download in 512Kb blocks
215+
for chunk in self.request.iter_content(chunk_size=self.chunk_size << 10):
216+
if chunk: # filter out keep-alive new chunks
217+
f.write(chunk)
218+
219+
self.log.info('__Saved: %s', os.path.abspath(filename))
220+
221+
return(self.request)
222+
223+
224+
225+
def upload(self, apipath, filename, param=None):
226+
""" initialize control structure """
227+
228+
class upload_in_chunks(object):
229+
def __init__(self, filename, chunksize=512):
230+
self.filename = filename
231+
self.chunksize = chunksize << 10
232+
self.totalsize = os.path.getsize(filename)
233+
self.readsofar = 0
234+
235+
def __iter__(self):
236+
with open(self.filename, 'rb') as file:
237+
while True:
238+
data = file.read(self.chunksize)
239+
if not data:
240+
break
241+
self.readsofar += len(data)
242+
yield data
243+
244+
def __len__(self):
245+
return self.totalsize
246+
247+
self.log.info('API UPLO <%s>', apipath)
248+
249+
(par,hd)=self._params_prepare(param, {'Content-Type': 'application/vnd.qlik.sense.app'})
250+
251+
# Build the request
252+
self.response= None
253+
url=self._params_update(up.urljoin(self.baseurl,apipath), par)
254+
self.log.debug('__SEND: %s', url)
255+
256+
# Execute the HTTP request
257+
self.log.info('__Uploading {:,} bytes'.format(os.path.getsize(filename)))
258+
self.request = self.session.post(url, headers=hd, cert=self.cafile, verify=self._verify, \
259+
data=upload_in_chunks(filename, self.chunk_size), auth=self.session.auth)
260+
261+
self.log.info('__Done.')
262+
263+
return(self.request)
264+
265+
266+
267+
268+
def get(self, apipath, param=None):
269+
'''
270+
@Function get: generic purpose call
271+
@param apipath: uri REST path
272+
@param param : whatever other param needed in form a dict
273+
(example: {'filter': "name eq 'myApp'} )
274+
'''
275+
return self.call('GET', apipath, param)
276+
277+
278+
279+
def post(self, apipath, param=None, data=None, files=None):
280+
'''
281+
@Function post: generic purpose call
282+
@param apipath: uri REST path
283+
@param param : whatever other param needed in form a dict
284+
(example: {'filter': "name eq 'myApp'} )
285+
@param data : stream data input (native dict/list structures are json formated)
286+
@param files : metafile input
287+
'''
288+
if isinstance(data,dict) or isinstance(data,list):
289+
data=json.dumps(data)
290+
return self.call('POST', apipath, param, data, files)
291+
292+
293+
294+
def put(self, apipath, param=None, data=None):
295+
'''
296+
@Function put: generic purpose call
297+
@param apipath: uri REST path
298+
@param param : whatever other param needed in form a dict
299+
(example: {'filter': "name eq 'myApp'} )
300+
@param data : stream data input (native dict/list structures are json formated)
301+
'''
302+
if isinstance(data,dict) or isinstance(data,list):
303+
data=json.dumps(data)
304+
return self.call('PUT', apipath, param, data)
305+
306+
307+
308+
def delete(self, apipath, param=None):
309+
'''
310+
@Function delete: generic purpose call
311+
@param apipath: uri REST path
312+
@param param : whatever other param needed in form a dict
313+
(example: {'filter': "name eq 'myApp'} )
314+
'''
315+
return self.call('DELETE', apipath, param)
316+

0 commit comments

Comments
 (0)